diff --git a/core/src/main/java/com/linecorp/armeria/common/CompositeHttpHeadersBase.java b/core/src/main/java/com/linecorp/armeria/common/CompositeHttpHeadersBase.java new file mode 100644 index 00000000000..3d85ebf9f37 --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/CompositeHttpHeadersBase.java @@ -0,0 +1,212 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.common; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.linecorp.armeria.common.StringMultimap.DEFAULT_SIZE_HINT; +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableList; + +import com.linecorp.armeria.common.annotation.Nullable; + +import io.netty.util.AsciiString; + +/** + * The composite container implementation which wraps {@link HttpHeadersBase}s + * to avoid expensive copy operations. + */ +class CompositeHttpHeadersBase + extends CompositeStringMultimap + implements HttpHeaderGetters { + + private static final Supplier> DEFAULT_APPENDER_SUPPLIER = + () -> new HttpHeadersBase(DEFAULT_SIZE_HINT); + + CompositeHttpHeadersBase(HttpHeaderGetters... parents) { + super(from(parents), DEFAULT_APPENDER_SUPPLIER); + } + + CompositeHttpHeadersBase(@Nullable List additionals, + List parents, + @Nullable List defaults) { + super(additionals != null ? from(additionals) : null, + from(parents), + defaults != null ? from(defaults) : null, + DEFAULT_APPENDER_SUPPLIER); + } + + private static List> from(HttpHeaderGetters... headers) { + if (headers.length == 0) { + return ImmutableList.of(); + } + + final ImmutableList.Builder> builder = + ImmutableList.builder(); + for (HttpHeaderGetters header : headers) { + if (header instanceof HttpHeadersBase) { + builder.add((HttpHeadersBase) header); + } else { + builder.add(new HttpHeadersBase(header)); + } + } + + return builder.build(); + } + + private static List> from(List headers) { + if (headers.isEmpty()) { + return ImmutableList.of(); + } + + final ImmutableList.Builder> builder = + ImmutableList.builder(); + for (HttpHeaderGetters header : headers) { + if (header instanceof HttpHeadersBase) { + builder.add((HttpHeadersBase) header); + } else { + builder.add(new HttpHeadersBase(header)); + } + } + + return builder.build(); + } + + final void contentLength(long contentLength) { + checkArgument(contentLength >= 0, "contentLength: %s (expected: >= 0)", contentLength); + remove0(HttpHeaderNames.CONTENT_LENGTH); + ((HttpHeadersBase) appender()).contentLength(contentLength); + } + + @Override + public long contentLength() { + return getLong(HttpHeaderNames.CONTENT_LENGTH, -1); + } + + final void contentLengthUnknown() { + remove0(HttpHeaderNames.CONTENT_LENGTH); + ((HttpHeadersBase) appender()).contentLengthUnknown(); + } + + @Override + public boolean isContentLengthUnknown() { + return ((HttpHeaderGetters) appender()).isContentLengthUnknown(); + } + + final void contentType(MediaType contentType) { + requireNonNull(contentType, "contentType"); + remove0(HttpHeaderNames.CONTENT_TYPE); + ((HttpHeadersBase) appender()).contentType(contentType); + } + + @Override + public @Nullable MediaType contentType() { + final String contentTypeString = get(HttpHeaderNames.CONTENT_TYPE); + if (contentTypeString == null) { + return null; + } + + try { + return MediaType.parse(contentTypeString); + } catch (IllegalArgumentException ex) { + return null; + } + } + + final void contentDisposition(ContentDisposition contentDisposition) { + requireNonNull(contentDisposition, "contentDisposition"); + remove0(HttpHeaderNames.CONTENT_DISPOSITION); + ((HttpHeadersBase) appender()).contentDisposition(contentDisposition); + } + + @Override + public @Nullable ContentDisposition contentDisposition() { + final String contentDispositionString = get(HttpHeaderNames.CONTENT_DISPOSITION); + if (contentDispositionString == null) { + return null; + } + + try { + return ContentDisposition.parse(contentDispositionString); + } catch (IllegalArgumentException ex) { + return null; + } + } + + final void endOfStream(boolean endOfStream) { + ((HttpHeadersBase) appender()).endOfStream(endOfStream); + } + + @Override + public boolean isEndOfStream() { + for (StringMultimapGetters delegate : delegates()) { + if (((HttpHeaderGetters) delegate).isEndOfStream()) { + return true; + } + } + return false; + } + + @Override + public final int hashCode() { + final int hashCode = super.hashCode(); + return isEndOfStream() ? ~hashCode : hashCode; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof HttpHeaderGetters)) { + return false; + } + + // `contentLengthUnknown` is excluded from the comparison since it is not a field expressing headers + // data. + return isEndOfStream() == ((HttpHeaderGetters) o).isEndOfStream() && super.equals(o); + } + + @Override + public final String toString() { + final int size = size(); + final boolean isEndOfStream = isEndOfStream(); + if (size == 0) { + return isEndOfStream ? "[EOS]" : "[]"; + } + + final StringBuilder sb = new StringBuilder(7 + size * 20); + if (isEndOfStream) { + sb.append("[EOS, "); + } else { + sb.append('['); + } + + for (Map.Entry e : this) { + sb.append(e.getKey()).append('=').append(e.getValue()).append(", "); + } + + final int length = sb.length(); + sb.setCharAt(length - 2, ']'); + return sb.substring(0, length - 1); + } +} diff --git a/core/src/main/java/com/linecorp/armeria/common/CompositeStringMultimap.java b/core/src/main/java/com/linecorp/armeria/common/CompositeStringMultimap.java new file mode 100644 index 00000000000..fb3ea6902a6 --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/CompositeStringMultimap.java @@ -0,0 +1,1254 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.common; + +import static com.linecorp.armeria.common.StringMultimap.HASH_CODE_SEED; +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterators; + +import com.linecorp.armeria.common.annotation.Nullable; + +import io.netty.handler.codec.DateFormatter; + +/** + * The composite container implementation which wraps {@link StringMultimap}s + * to avoid expensive copy operations. + * + *

Adds entries only on {@code appender} and marks entries as removed only on {@code removed} + * without modifying wrapped {@link StringMultimap}s to avoid expensive copy operations. + * + *

So any further modifications to this don't break the immutability of wrapped {@link StringMultimap}s. + * + * @param the type of the user-specified names, which may be more permissive than {@link NAME} + * @param the actual type of the names + * + * @see CompositeHttpHeadersBase + */ +abstract class CompositeStringMultimap + implements StringMultimapGetters { + + /** + * The {@link StringMultimap}s have priority in the following order. + *

{@code
+     * additionals (highest priority) > parents > defaults (lowest priority)
+     * }
+ */ + @Nullable + private final List> additionals; + private final List> parents; + @Nullable + private final List> defaults; + + @Nullable + private StringMultimap appender; + private final Supplier> appenderSupplier; + + @Nullable + private List> delegates; + + private final Set removed = new HashSet<>(); + + CompositeStringMultimap(List> parents, + Supplier> appenderSupplier) { + this(null, parents, null, appenderSupplier); + } + + CompositeStringMultimap(@Nullable List> additionals, + List> parents, + @Nullable List> defaults, + Supplier> appenderSupplier) { + requireNonNull(parents, "parents"); + requireNonNull(appenderSupplier, "appenderSupplier"); + this.additionals = additionals; + this.parents = parents; + this.defaults = defaults; + this.appenderSupplier = appenderSupplier; + } + + protected final List> delegates() { + if (delegates != null) { + return delegates; + } + + final ImmutableList.Builder> builder = ImmutableList.builder(); + if (additionals != null) { + builder.addAll(additionals); + } + + builder.addAll(parents); + + if (defaults != null) { + builder.addAll(defaults); + } + + return delegates = builder.build(); + } + + protected final StringMultimap appender() { + if (appender != null) { + return appender; + } + appender = requireNonNull(appenderSupplier.get(), "appender"); + + final ImmutableList.Builder> builder = ImmutableList.builder(); + delegates = builder.addAll(delegates()) + .add(appender) + .build(); + return appender; + } + + // Getters + + @Override + @Nullable + public String get(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.get(name) : null; + } + + for (StringMultimapGetters delegate : delegates()) { + final String res = delegate.get(name); + if (res != null) { + return res; + } + } + return null; + } + + @Override + public String get(IN_NAME name, String defaultValue) { + requireNonNull(name, "name"); + requireNonNull(defaultValue, "defaultValue"); + final String get = get(name); + return get != null ? get : defaultValue; + } + + @Override + @Nullable + public String getLast(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getLast(name) : null; + } + + if (appender != null && appender.contains(name)) { + return appender.getLast(name); + } + + String getLast = null; + if (additionals != null) { + for (StringMultimapGetters getter : additionals) { + final String res = getter.getLast(name); + if (res != null) { + getLast = res; + } + } + } + + if (getLast == null) { + for (StringMultimapGetters parent : parents) { + final String res = parent.getLast(name); + if (res != null) { + getLast = res; + } + } + } + + if (getLast == null && defaults != null) { + for (StringMultimapGetters getter : defaults) { + final String res = getter.getLast(name); + if (res != null) { + getLast = res; + } + } + } + return getLast; + } + + @Override + public String getLast(IN_NAME name, String defaultValue) { + requireNonNull(name, "name"); + requireNonNull(defaultValue, "defaultValue"); + final String getLast = getLast(name); + return getLast != null ? getLast : defaultValue; + } + + @Override + public List getAll(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getAll(name) : ImmutableList.of(); + } + + final ImmutableList.Builder builder = ImmutableList.builder(); + boolean additionalsContain = false; + if (additionals != null) { + for (StringMultimapGetters getter : additionals) { + final List res = getter.getAll(name); + if (!res.isEmpty()) { + builder.addAll(res); + additionalsContain = true; + } + } + } + + boolean parentsContain = false; + if (!additionalsContain) { + for (StringMultimapGetters parent : parents) { + final List res = parent.getAll(name); + if (!res.isEmpty()) { + builder.addAll(res); + parentsContain = true; + } + } + } + + if (!additionalsContain && !parentsContain && defaults != null) { + for (StringMultimapGetters getter : defaults) { + final List res = getter.getAll(name); + if (!res.isEmpty()) { + builder.addAll(res); + } + } + } + + if (appender != null) { + builder.addAll(appender.getAll(name)); + } + return builder.build(); + } + + @Override + @Nullable + public Boolean getBoolean(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getBoolean(name) : null; + } + + for (StringMultimapGetters delegate : delegates()) { + final Boolean res = delegate.getBoolean(name); + if (res != null) { + return res; + } + } + return null; + } + + @Override + public boolean getBoolean(IN_NAME name, boolean defaultValue) { + requireNonNull(name, "name"); + final Boolean getBoolean = getBoolean(name); + return getBoolean != null ? getBoolean : defaultValue; + } + + @Override + @Nullable + public Boolean getLastBoolean(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getLastBoolean(name) : null; + } + + Boolean getLastBoolean = null; + for (StringMultimapGetters delegate : delegates()) { + final Boolean res = delegate.getLastBoolean(name); + if (res != null) { + getLastBoolean = res; + } + } + return getLastBoolean; + } + + @Override + public boolean getLastBoolean(IN_NAME name, boolean defaultValue) { + requireNonNull(name, "name"); + final Boolean getLastBoolean = getLastBoolean(name); + return getLastBoolean != null ? getLastBoolean : defaultValue; + } + + @Override + @Nullable + public Integer getInt(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getInt(name) : null; + } + + for (StringMultimapGetters delegate : delegates()) { + final Integer res = delegate.getInt(name); + if (res != null) { + return res; + } + } + return null; + } + + @Override + public int getInt(IN_NAME name, int defaultValue) { + requireNonNull(name, "name"); + final Integer getInt = getInt(name); + return getInt != null ? getInt : defaultValue; + } + + @Override + @Nullable + public Integer getLastInt(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getLastInt(name) : null; + } + + Integer getLastInt = null; + for (StringMultimapGetters delegate : delegates()) { + final Integer res = delegate.getLastInt(name); + if (res != null) { + getLastInt = res; + } + } + return getLastInt; + } + + @Override + public int getLastInt(IN_NAME name, int defaultValue) { + requireNonNull(name, "name"); + final Integer getLastInt = getLastInt(name); + return getLastInt != null ? getLastInt : defaultValue; + } + + @Override + @Nullable + public Long getLong(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getLong(name) : null; + } + + for (StringMultimapGetters delegate : delegates()) { + final Long res = delegate.getLong(name); + if (res != null) { + return res; + } + } + return null; + } + + @Override + public long getLong(IN_NAME name, long defaultValue) { + requireNonNull(name, "name"); + final Long getLong = getLong(name); + return getLong != null ? getLong : defaultValue; + } + + @Override + @Nullable + public Long getLastLong(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getLastLong(name) : null; + } + + Long getLastLong = null; + for (StringMultimapGetters delegate : delegates()) { + final Long res = delegate.getLastLong(name); + if (res != null) { + getLastLong = res; + } + } + return getLastLong; + } + + @Override + public long getLastLong(IN_NAME name, long defaultValue) { + requireNonNull(name, "name"); + final Long getLastLong = getLastLong(name); + return getLastLong != null ? getLastLong : defaultValue; + } + + @Override + @Nullable + public Float getFloat(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getFloat(name) : null; + } + + for (StringMultimapGetters delegate : delegates()) { + final Float res = delegate.getFloat(name); + if (res != null) { + return res; + } + } + return null; + } + + @Override + public float getFloat(IN_NAME name, float defaultValue) { + requireNonNull(name, "name"); + final Float getFloat = getFloat(name); + return getFloat != null ? getFloat : defaultValue; + } + + @Override + @Nullable + public Float getLastFloat(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getLastFloat(name) : null; + } + + Float getLastFloat = null; + for (StringMultimapGetters delegate : delegates()) { + final Float res = delegate.getLastFloat(name); + if (res != null) { + getLastFloat = res; + } + } + return getLastFloat; + } + + @Override + public float getLastFloat(IN_NAME name, float defaultValue) { + requireNonNull(name, "name"); + final Float getLastFloat = getLastFloat(name); + return getLastFloat != null ? getLastFloat : defaultValue; + } + + @Override + @Nullable + public Double getDouble(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getDouble(name) : null; + } + + for (StringMultimapGetters delegate : delegates()) { + final Double res = delegate.getDouble(name); + if (res != null) { + return res; + } + } + return null; + } + + @Override + public double getDouble(IN_NAME name, double defaultValue) { + requireNonNull(name, "name"); + final Double getDouble = getDouble(name); + return getDouble != null ? getDouble : defaultValue; + } + + @Override + @Nullable + public Double getLastDouble(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getLastDouble(name) : null; + } + + Double getLastDouble = null; + for (StringMultimapGetters delegate : delegates()) { + final Double res = delegate.getLastDouble(name); + if (res != null) { + getLastDouble = res; + } + } + return getLastDouble; + } + + @Override + public double getLastDouble(IN_NAME name, double defaultValue) { + requireNonNull(name, "name"); + final Double getLastDouble = getLastDouble(name); + return getLastDouble != null ? getLastDouble : defaultValue; + } + + @Override + @Nullable + public Long getTimeMillis(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getTimeMillis(name) : null; + } + + for (StringMultimapGetters delegate : delegates()) { + final Long res = delegate.getTimeMillis(name); + if (res != null) { + return res; + } + } + return null; + } + + @Override + public long getTimeMillis(IN_NAME name, long defaultValue) { + requireNonNull(name, "name"); + final Long getTimeMillis = getTimeMillis(name); + return getTimeMillis != null ? getTimeMillis : defaultValue; + } + + @Override + @Nullable + public Long getLastTimeMillis(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null ? appender.getLastTimeMillis(name) : null; + } + + Long getLastTimeMillis = null; + for (StringMultimapGetters delegate : delegates()) { + final Long res = delegate.getLastTimeMillis(name); + if (res != null) { + getLastTimeMillis = res; + } + } + return getLastTimeMillis; + } + + @Override + public long getLastTimeMillis(IN_NAME name, long defaultValue) { + requireNonNull(name, "name"); + final Long getLastTimeMillis = getLastTimeMillis(name); + return getLastTimeMillis != null ? getLastTimeMillis : defaultValue; + } + + @Override + public boolean contains(IN_NAME name) { + requireNonNull(name, "name"); + if (removed.contains(name)) { + return appender != null && appender.contains(name); + } + + for (StringMultimapGetters delegate : delegates()) { + if (delegate.contains(name)) { + return true; + } + } + return false; + } + + @Override + public boolean contains(IN_NAME name, String value) { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + if (removed.contains(name)) { + return appender != null && appender.contains(name, value); + } + + return contains(name, delegate -> delegate.contains(name, value)); + } + + private boolean contains(IN_NAME name, Predicate> containsPredicate) { + if (removed.contains(name)) { + return appender != null && containsPredicate.test(appender); + } + + boolean additionalsContain = false; + if (additionals != null) { + for (StringMultimapGetters getter : additionals) { + if (containsPredicate.test(getter)) { + return true; + } + if (getter.contains(name)) { + additionalsContain = true; + } + } + } + + boolean parentsContain = false; + if (!additionalsContain) { + for (StringMultimapGetters parent : parents) { + if (containsPredicate.test(parent)) { + return true; + } + if (parent.contains(name)) { + parentsContain = true; + } + } + } + + if (!additionalsContain && !parentsContain && defaults != null) { + for (StringMultimapGetters getter : defaults) { + if (containsPredicate.test(getter)) { + return true; + } + } + } + + return appender != null ? containsPredicate.test(appender) : false; + } + + @Override + public boolean containsObject(IN_NAME name, Object value) { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + return contains(name, delegate -> delegate.containsObject(name, value)); + } + + @Override + public boolean containsBoolean(IN_NAME name, boolean value) { + requireNonNull(name, "name"); + return contains(name, delegate -> delegate.containsBoolean(name, value)); + } + + @Override + public boolean containsInt(IN_NAME name, int value) { + requireNonNull(name, "name"); + return contains(name, delegate -> delegate.containsInt(name, value)); + } + + @Override + public boolean containsLong(IN_NAME name, long value) { + requireNonNull(name, "name"); + return contains(name, delegate -> delegate.containsLong(name, value)); + } + + @Override + public boolean containsFloat(IN_NAME name, float value) { + requireNonNull(name, "name"); + return contains(name, delegate -> delegate.containsFloat(name, value)); + } + + @Override + public boolean containsDouble(IN_NAME name, double value) { + requireNonNull(name, "name"); + return contains(name, delegate -> delegate.containsDouble(name, value)); + } + + @Override + public boolean containsTimeMillis(IN_NAME name, long value) { + requireNonNull(name, "name"); + return contains(name, delegate -> delegate.containsTimeMillis(name, value)); + } + + @SuppressWarnings("unchecked") + @Override + public int size() { + int size = 0; + for (NAME name : names()) { + size += getAll((IN_NAME) name.toString()).size(); + } + return size; + } + + @Override + public boolean isEmpty() { + if (appender != null && !appender.isEmpty()) { + return false; + } + + for (StringMultimapGetters delegate : delegates()) { + for (NAME name : delegate.names()) { + if (!removed.contains(name.toString())) { + return false; + } + } + } + return true; + } + + @Override + public final Set names() { + final ImmutableSet.Builder builder = ImmutableSet.builder(); + if (additionals != null) { + for (StringMultimapGetters getters : additionals) { + for (NAME name : getters.names()) { + if (!removed.contains(name.toString())) { + builder.add(name); + } + } + } + } + + for (StringMultimapGetters parent : parents) { + for (NAME name : parent.names()) { + if (!removed.contains(name.toString())) { + builder.add(name); + } + } + } + + if (defaults != null) { + for (StringMultimapGetters getters : defaults) { + for (NAME name : getters.names()) { + if (!removed.contains(name.toString())) { + builder.add(name); + } + } + } + } + + if (appender != null) { + builder.addAll(appender.names()); + } + return builder.build(); + } + + @Override + public Iterator> iterator() { + Iterator> additionalsIterator = null; + if (additionals != null) { + additionalsIterator = Iterators.concat( + additionals.stream() + .map(StringMultimapGetters::iterator) + .map(it -> Iterators.filter( + it, entry -> !removed.contains(entry.getKey().toString()))) + .iterator()); + } + + final Iterator> parentsIterators = Iterators.concat( + parents.stream() + .map(StringMultimapGetters::iterator) + .map(it -> Iterators.filter(it, entry -> { + if (removed.contains(entry.getKey().toString())) { + return false; + } + if (additionals != null) { + for (StringMultimapGetters getter : additionals) { + if (getter.contains(entry.getKey())) { + return false; + } + } + } + return true; + })) + .iterator()); + + Iterator> defaultsIterator = null; + if (defaults != null) { + defaultsIterator = Iterators.concat( + defaults.stream() + .map(StringMultimapGetters::iterator) + .map(it -> Iterators.filter(it, entry -> { + if (removed.contains(entry.getKey().toString())) { + return false; + } + if (additionals != null) { + for (StringMultimapGetters getter : additionals) { + if (getter.contains(entry.getKey())) { + return false; + } + } + } + for (StringMultimapGetters parent : parents) { + if (parent.contains(entry.getKey())) { + return false; + } + } + return true; + })) + .iterator()); + } + + Iterator> appenderIterator = null; + if (appender != null && !appender.isEmpty()) { + appenderIterator = appender.iterator(); + } + + Iterator> res = additionalsIterator; + if (res == null) { + res = parentsIterators; + } else { + res = Iterators.concat(res, parentsIterators); + } + + if (defaultsIterator != null) { + res = Iterators.concat(res, defaultsIterator); + } + + if (appenderIterator != null) { + res = Iterators.concat(res, appenderIterator); + } + + return res; + } + + @Override + public Iterator valueIterator(IN_NAME name) { + if (removed.contains(name)) { + return appender != null ? appender.valueIterator(name) : Collections.emptyIterator(); + } + return getAll(name).iterator(); + } + + @Override + public void forEach(BiConsumer action) { + requireNonNull(action, "action"); + for (Entry entry : this) { + action.accept(entry.getKey(), entry.getValue()); + } + } + + @Override + public void forEachValue(IN_NAME name, Consumer action) { + requireNonNull(name, "name"); + requireNonNull(action, "action"); + if (removed.contains(name)) { + if (appender != null) { + appender.forEachValue(name, action); + } + return; + } + + for (String value : getAll(name)) { + action.accept(value); + } + } + + // Mutators + + @Nullable + final String getAndRemove(IN_NAME name) { + requireNonNull(name, "name"); + final String get = get(name); + remove0(name); + return get; + } + + final String getAndRemove(IN_NAME name, String defaultValue) { + requireNonNull(defaultValue, "defaultValue"); + final String value = getAndRemove(name); + return value != null ? value : defaultValue; + } + + final List getAllAndRemove(IN_NAME name) { + requireNonNull(name, "name"); + final List getAll = getAll(name); + remove0(name); + return getAll; + } + + @Nullable + final Integer getIntAndRemove(IN_NAME name) { + final String v = getAndRemove(name); + return toInteger(v); + } + + final int getIntAndRemove(IN_NAME name, int defaultValue) { + final Integer v = getIntAndRemove(name); + return v != null ? v : defaultValue; + } + + @Nullable + final Long getLongAndRemove(IN_NAME name) { + final String v = getAndRemove(name); + return toLong(v); + } + + final long getLongAndRemove(IN_NAME name, long defaultValue) { + final Long v = getLongAndRemove(name); + return v != null ? v : defaultValue; + } + + @Nullable + final Float getFloatAndRemove(IN_NAME name) { + final String v = getAndRemove(name); + return toFloat(v); + } + + final float getFloatAndRemove(IN_NAME name, float defaultValue) { + final Float v = getFloatAndRemove(name); + return v != null ? v : defaultValue; + } + + @Nullable + final Double getDoubleAndRemove(IN_NAME name) { + final String v = getAndRemove(name); + return toDouble(v); + } + + final double getDoubleAndRemove(IN_NAME name, double defaultValue) { + final Double v = getDoubleAndRemove(name); + return v != null ? v : defaultValue; + } + + @Nullable + final Long getTimeMillisAndRemove(IN_NAME name) { + final String v = getAndRemove(name); + return toTimeMillis(v); + } + + final long getTimeMillisAndRemove(IN_NAME name, long defaultValue) { + final Long v = getTimeMillisAndRemove(name); + return v != null ? v : defaultValue; + } + + final void add(IN_NAME name, String value) { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + appender().add(name, value); + } + + final void add(IN_NAME name, Iterable values) { + requireNonNull(name, "name"); + requireNonNull(values, "values"); + appender().add(name, values); + } + + final void add(IN_NAME name, String... values) { + requireNonNull(name, "name"); + requireNonNull(values, "values"); + appender().add(name, values); + } + + final void add(Iterable> entries) { + requireNonNull(entries, "entries"); + appender().add(entries); + } + + final void addObject(IN_NAME name, Object value) { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + appender().addObject(name, value); + } + + final void addObject(IN_NAME name, Iterable values) { + requireNonNull(name, "name"); + requireNonNull(values, "values"); + appender().addObject(name, values); + } + + final void addObject(IN_NAME name, Object... values) { + requireNonNull(name, "name"); + requireNonNull(values, "values"); + appender().addObject(name, values); + } + + void addObject(Iterable> entries) { + requireNonNull(entries, "entries"); + appender().addObject(entries); + } + + final void addInt(IN_NAME name, int value) { + requireNonNull(name, "name"); + appender().addInt(name, value); + } + + final void addLong(IN_NAME name, long value) { + requireNonNull(name, "name"); + appender().addLong(name, value); + } + + final void addFloat(IN_NAME name, float value) { + requireNonNull(name, "name"); + appender().addFloat(name, value); + } + + final void addDouble(IN_NAME name, double value) { + requireNonNull(name, "name"); + appender().addDouble(name, value); + } + + final void addTimeMillis(IN_NAME name, long value) { + requireNonNull(name, "name"); + appender().addTimeMillis(name, value); + } + + final void set(IN_NAME name, String value) { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + removeAndAppender(name, appender -> appender.set(name, value)); + } + + final void set(IN_NAME name, Iterable values) { + requireNonNull(name, "name"); + requireNonNull(values, "values"); + removeAndAppender(name, appender -> appender.set(name, values)); + } + + final void set(IN_NAME name, String... values) { + requireNonNull(name, "name"); + requireNonNull(values, "values"); + removeAndAppender(name, appender -> appender.set(name, values)); + } + + final void set(Iterable> entries) { + requireNonNull(entries, "entries"); + removeAndAppender(entries, appender -> appender.set(entries)); + } + + final CompositeStringMultimap setIfAbsent( + Iterable> entries) { + requireNonNull(entries, "entries"); + final ImmutableListMultimap.Builder> builder = + ImmutableListMultimap.builder(); + entries.forEach(entry -> builder.put(entry.getKey(), entry)); + + final ImmutableListMultimap> map = builder.build(); + for (IN_NAME name : map.keySet()) { + if (!contains(name)) { + removeAndAppender(name, appender -> appender.add(map.get(name))); + } + } + + return this; + } + + final void setObject(IN_NAME name, Object value) { + requireNonNull(name, "name"); + requireNonNull(value, "value"); + removeAndAppender(name, appender -> appender.setObject(name, value)); + } + + final void setObject(IN_NAME name, Iterable values) { + requireNonNull(name, "name"); + requireNonNull(values, "values"); + removeAndAppender(name, appender -> appender.setObject(name, values)); + } + + final void setObject(IN_NAME name, Object... values) { + requireNonNull(name, "name"); + requireNonNull(values, "values"); + removeAndAppender(name, appender -> appender.setObject(name, values)); + } + + final void setObject(Iterable> entries) { + requireNonNull(entries, "entries"); + removeAndAppender(entries, appender -> appender.setObject(entries)); + } + + final void setInt(IN_NAME name, int value) { + requireNonNull(name, "name"); + removeAndAppender(name, appender -> appender.setInt(name, value)); + } + + final void setLong(IN_NAME name, long value) { + requireNonNull(name, "name"); + removeAndAppender(name, appender -> appender.setLong(name, value)); + } + + final void setFloat(IN_NAME name, float value) { + requireNonNull(name, "name"); + removeAndAppender(name, appender -> appender.setFloat(name, value)); + } + + final void setDouble(IN_NAME name, double value) { + requireNonNull(name, "name"); + removeAndAppender(name, appender -> appender.setDouble(name, value)); + } + + final void setTimeMillis(IN_NAME name, long value) { + requireNonNull(name, "name"); + removeAndAppender(name, appender -> appender.setTimeMillis(name, value)); + } + + final boolean remove(IN_NAME name) { + requireNonNull(name, "name"); + final boolean remove = contains(name); + remove0(name); + return remove; + } + + protected final void remove0(IN_NAME name) { + removed.add(name); + if (appender != null && appender.contains(name)) { + appender.remove(name); + } + } + + private void removeAndAppender(IN_NAME name, Consumer> appenderConsumer) { + remove0(name); + appenderConsumer.accept(appender()); + } + + private void removeAndAppender(Iterable> entries, + Consumer> appenderConsumer) { + for (Map.Entry e : entries) { + remove0(e.getKey()); + } + appenderConsumer.accept(appender()); + } + + final void clear() { + if (additionals != null) { + for (StringMultimap multimap : additionals) { + multimap.clear(); + } + } + + for (StringMultimap parent : parents) { + parent.clear(); + } + + if (defaults != null) { + for (StringMultimap multimap : defaults) { + multimap.clear(); + } + } + + if (appender != null) { + appender.clear(); + } + + removed.clear(); + } + + // Conversion functions + + @Nullable + private static Integer toInteger(@Nullable String v) { + try { + return v != null ? Integer.parseInt(v) : null; + } catch (NumberFormatException ignore) { + return null; + } + } + + @Nullable + private static Long toLong(@Nullable String v) { + try { + return v != null ? Long.parseLong(v) : null; + } catch (NumberFormatException ignore) { + return null; + } + } + + @Nullable + private static Float toFloat(@Nullable String v) { + try { + return v != null ? Float.parseFloat(v) : null; + } catch (NumberFormatException ignore) { + return null; + } + } + + @Nullable + private static Double toDouble(@Nullable String v) { + try { + return v != null ? Double.parseDouble(v) : null; + } catch (NumberFormatException ignore) { + return null; + } + } + + @Nullable + private static Long toTimeMillis(@Nullable String v) { + if (v == null) { + return null; + } + + try { + @SuppressWarnings("UseOfObsoleteDateTimeApi") + final Date date = DateFormatter.parseHttpDate(v); + return date != null ? date.getTime() : null; + } catch (Exception ignore) { + // `parseHttpDate()` can raise an exception rather than returning `null` + // when the given value has more than 64 characters. + return null; + } + } + + // hashCode(), equals() and toString() + + @Override + public int hashCode() { + int result = HASH_CODE_SEED; + int hashCode; + if (additionals != null) { + hashCode = additionals.hashCode(); + result = (result * 31 + hashCode) * 31 + hashCode; + } + + for (StringMultimap parent : parents) { + hashCode = parent.hashCode(); + result = (result * 31 + hashCode) * 31 + hashCode; + } + + if (defaults != null) { + hashCode = defaults.hashCode(); + result = (result * 31 + hashCode) * 31 + hashCode; + } + + if (appender != null) { + hashCode = appender.hashCode(); + result = (result * 31 + hashCode) * 31 + hashCode; + } + return result; + } + + @SuppressWarnings("unchecked") + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof StringMultimapGetters)) { + return false; + } + + final StringMultimapGetters that = (StringMultimapGetters) o; + if (size() != that.size()) { + return false; + } + + if (that instanceof CompositeStringMultimap || + that instanceof StringMultimap) { + for (NAME name : names()) { + if (!getAll((IN_NAME) name.toString()) + .equals(that.getAll((IN_NAME) name.toString()))) { + return false; + } + } + } else { + for (NAME name : names()) { + if (!Iterators.elementsEqual(valueIterator((IN_NAME) name.toString()), + that.valueIterator((IN_NAME) name.toString()))) { + return false; + } + } + } + return true; + } + + @Override + public String toString() { + final int size = size(); + if (size == 0) { + return "[]"; + } + + final StringBuilder sb = new StringBuilder(7 + size * 20); + sb.append('['); + + for (final Entry cur : this) { + sb.append(cur.getKey()).append('=').append(cur.getValue()).append(", "); + } + + final int length = sb.length(); + sb.setCharAt(length - 2, ']'); + return sb.substring(0, length - 1); + } +} diff --git a/core/src/test/java/com/linecorp/armeria/common/CompositeHttpHeadersBaseTest.java b/core/src/test/java/com/linecorp/armeria/common/CompositeHttpHeadersBaseTest.java new file mode 100644 index 00000000000..71424515304 --- /dev/null +++ b/core/src/test/java/com/linecorp/armeria/common/CompositeHttpHeadersBaseTest.java @@ -0,0 +1,440 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableList; + +import io.netty.util.AsciiString; + +class CompositeHttpHeadersBaseTest { + + @SuppressWarnings("unchecked") + @Test + void constructor_shallowCopy() { + final HttpHeaders header1 = HttpHeaders.of("k1", "v1"); + final HttpHeaders header2 = HttpHeaders.of("k2", "v2"); + final CompositeHttpHeadersBase shallowCopy = new CompositeHttpHeadersBase(header1, header2); + assertThat(shallowCopy.get("k1")).isEqualTo("v1"); + assertThat(shallowCopy.get("k2")).isEqualTo("v2"); + + ((StringMultimap) header1).remove("k1"); + assertThat(shallowCopy.get("k1")).isNull(); + assertThat(shallowCopy.get("k2")).isEqualTo("v2"); + + ((StringMultimap) header2).clear(); + assertThat(shallowCopy.isEmpty()).isTrue(); + } + + @Test + void constructor_deepCopy() { + final HttpHeadersBuilder builder1 = HttpHeaders.builder().add("k1", "v1"); + final HttpHeadersBuilder builder2 = HttpHeaders.builder().add("k2", "v2"); + final CompositeHttpHeadersBase deepCopy = new CompositeHttpHeadersBase(builder1, builder2); + assertThat(deepCopy.get("k1")).isEqualTo("v1"); + assertThat(deepCopy.get("k2")).isEqualTo("v2"); + + builder1.remove("k1"); + assertThat(deepCopy.get("k1")).isEqualTo("v1"); + assertThat(deepCopy.get("k2")).isEqualTo("v2"); + + builder2.remove("k2"); + assertThat(deepCopy.get("k1")).isEqualTo("v1"); + assertThat(deepCopy.get("k2")).isEqualTo("v2"); + } + + @Test + void contentLength() { + final CompositeHttpHeadersBase headers = new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(headers.isContentLengthUnknown()).isFalse(); + assertThat(headers.contentLength()).isEqualTo(-1); + assertThat(headers.get(HttpHeaderNames.CONTENT_LENGTH)).isNull(); + + assertThatThrownBy(() -> headers.contentLength(-1)) + .isExactlyInstanceOf(IllegalArgumentException.class); + + headers.contentLength(0); + assertThat(headers.isContentLengthUnknown()).isFalse(); + assertThat(headers.contentLength()).isEqualTo(0); + assertThat(headers.get(HttpHeaderNames.CONTENT_LENGTH)).isEqualTo("0"); + + headers.contentLength(100); + assertThat(headers.isContentLengthUnknown()).isFalse(); + assertThat(headers.contentLength()).isEqualTo(100); + assertThat(headers.get(HttpHeaderNames.CONTENT_LENGTH)).isEqualTo("100"); + + headers.contentLengthUnknown(); + assertThat(headers.isContentLengthUnknown()).isTrue(); + assertThat(headers.contentLength()).isEqualTo(-1); + assertThat(headers.get(HttpHeaderNames.CONTENT_LENGTH)).isNull(); + } + + @Test + void contentLengths() { + final CompositeHttpHeadersBase contentLengths = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.builder().contentLengthUnknown().build(), + HttpHeaders.builder().contentLength(100).build(), + HttpHeaders.builder().contentLength(200).build(), + HttpHeaders.builder().contentLength(300).build(), + HttpHeaders.of()); + assertThat(contentLengths.isContentLengthUnknown()).isFalse(); + assertThat(contentLengths.contentLength()).isEqualTo(100); + + contentLengths.contentLengthUnknown(); + assertThat(contentLengths.isContentLengthUnknown()).isTrue(); + assertThat(contentLengths.contentLength()).isEqualTo(-1); + + contentLengths.contentLength(500); + assertThat(contentLengths.isContentLengthUnknown()).isFalse(); + assertThat(contentLengths.contentLength()).isEqualTo(500); + + final CompositeHttpHeadersBase unknown = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.builder().contentLengthUnknown().build(), + HttpHeaders.of()); + assertThat(unknown.isContentLengthUnknown()).isFalse(); + assertThat(unknown.contentLength()).isEqualTo(-1); + + unknown.contentLength(500); + assertThat(unknown.isContentLengthUnknown()).isFalse(); + assertThat(unknown.contentLength()).isEqualTo(500); + } + + @Test + void contentType() { + final CompositeHttpHeadersBase headers = new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(headers.contentType()).isNull(); + assertThat(headers.get(HttpHeaderNames.CONTENT_TYPE)).isNull(); + + headers.contentType(MediaType.PNG); + assertThat(headers.contentType()).isEqualTo(MediaType.PNG); + assertThat(headers.get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo(MediaType.PNG.toString()); + + headers.contentType(MediaType.JPEG); + assertThat(headers.contentType()).isEqualTo(MediaType.JPEG); + assertThat(headers.get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo(MediaType.JPEG.toString()); + } + + @Test + void contentTypes() { + final CompositeHttpHeadersBase contentTypes = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.builder().contentType(MediaType.PNG).build(), + HttpHeaders.builder().contentType(MediaType.JPEG).build(), + HttpHeaders.builder().contentType(MediaType.GIF).build(), + HttpHeaders.of()); + assertThat(contentTypes.contentType()).isEqualTo(MediaType.PNG); + assertThat(contentTypes.get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo(MediaType.PNG.toString()); + + contentTypes.contentType(MediaType.JPEG); + assertThat(contentTypes.contentType()).isEqualTo(MediaType.JPEG); + assertThat(contentTypes.get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo(MediaType.JPEG.toString()); + + final CompositeHttpHeadersBase unknown = new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(unknown.contentType()).isNull(); + assertThat(unknown.get(HttpHeaderNames.CONTENT_TYPE)).isNull(); + + unknown.contentType(MediaType.GIF); + assertThat(unknown.contentType()).isEqualTo(MediaType.GIF); + assertThat(unknown.get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo(MediaType.GIF.toString()); + } + + @Test + void contentDisposition() { + final CompositeHttpHeadersBase headers = new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(headers.contentDisposition()).isNull(); + assertThat(headers.get(HttpHeaderNames.CONTENT_DISPOSITION)).isNull(); + + final ContentDisposition typeA = ContentDisposition.of("typeA"); + headers.contentDisposition(typeA); + assertThat(headers.contentDisposition()).isEqualTo(typeA); + assertThat(headers.get(HttpHeaderNames.CONTENT_DISPOSITION)).isEqualTo(typeA.toString()); + + final ContentDisposition typeB = ContentDisposition.of("typeB"); + headers.contentDisposition(typeB); + assertThat(headers.contentDisposition()).isEqualTo(typeB); + assertThat(headers.get(HttpHeaderNames.CONTENT_DISPOSITION)).isEqualTo(typeB.toString()); + } + + @Test + void contentDispositions() { + final ContentDisposition typeA = ContentDisposition.of("typeA"); + final ContentDisposition typeB = ContentDisposition.of("typeB"); + final CompositeHttpHeadersBase contentDispositions = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.builder().contentDisposition(typeA).build(), + HttpHeaders.builder().contentDisposition(typeB).build(), + HttpHeaders.of()); + assertThat(contentDispositions.contentDisposition()).isEqualTo(typeA); + assertThat(contentDispositions.get(HttpHeaderNames.CONTENT_DISPOSITION)).isEqualTo(typeA.toString()); + + contentDispositions.contentDisposition(typeB); + assertThat(contentDispositions.contentDisposition()).isEqualTo(typeB); + assertThat(contentDispositions.get(HttpHeaderNames.CONTENT_DISPOSITION)).isEqualTo(typeB.toString()); + + final CompositeHttpHeadersBase unknown = new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(unknown.contentDisposition()).isNull(); + assertThat(unknown.get(HttpHeaderNames.CONTENT_DISPOSITION)).isNull(); + + unknown.contentDisposition(typeB); + assertThat(unknown.contentDisposition()).isEqualTo(typeB); + assertThat(unknown.get(HttpHeaderNames.CONTENT_DISPOSITION)).isEqualTo(typeB.toString()); + } + + @Test + void endOfStream() { + final CompositeHttpHeadersBase headers = new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(headers.isEndOfStream()).isFalse(); + + headers.endOfStream(true); + assertThat(headers.isEndOfStream()).isTrue(); + + headers.endOfStream(false); + assertThat(headers.isEndOfStream()).isFalse(); + } + + @Test + void endOfStreams() { + final CompositeHttpHeadersBase endOfStreams = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.builder().endOfStream(false).build(), + HttpHeaders.builder().endOfStream(false).build(), + HttpHeaders.of()); + assertThat(endOfStreams.isEndOfStream()).isFalse(); + + endOfStreams.endOfStream(true); + assertThat(endOfStreams.isEndOfStream()).isTrue(); + + endOfStreams.endOfStream(false); + assertThat(endOfStreams.isEndOfStream()).isFalse(); + + final CompositeHttpHeadersBase endOfStreamsSomeTrue = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.builder().endOfStream(true).build(), + HttpHeaders.builder().endOfStream(false).build(), + HttpHeaders.of()); + assertThat(endOfStreamsSomeTrue.isEndOfStream()).isTrue(); + + endOfStreamsSomeTrue.endOfStream(false); + assertThat(endOfStreamsSomeTrue.isEndOfStream()).isTrue(); + + final CompositeHttpHeadersBase unknown = new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(unknown.isEndOfStream()).isFalse(); + + unknown.endOfStream(true); + assertThat(unknown.isEndOfStream()).isTrue(); + + unknown.endOfStream(false); + assertThat(unknown.isEndOfStream()).isFalse(); + } + + @Test + void equals_true() { + final CompositeHttpHeadersBase headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.equals(headers)).isTrue(); + + final CompositeHttpHeadersBase other1 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.builder() + .add("dup", "dup1") + .endOfStream(false), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.equals(other1)).isTrue(); + + final CompositeHttpHeadersBase other2 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("diff1", "diff1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("diff2", "diff2"), + HttpHeaders.of("dup", "dup3"), + HttpHeaders.of("dup", "dup3"), + HttpHeaders.of()); + other2.remove("diff1"); + other2.remove("diff2"); + other2.add("k4", "v4"); + other2.add("k5", "v5"); + other2.add("k6", "v6"); + other2.remove("dup"); + other2.add("dup", "dup1"); + other2.add("dup", "dup2"); + assertThat(headers.equals(other2)).isTrue(); + + final HttpHeaders other3 = HttpHeaders.builder() + .add("k1", "v1") + .add("k2", "v2") + .add("k3", "v3") + .add("k4", "v4") + .add("k5", "v5") + .add("k6", "v6") + .add("dup", "dup1") + .add("dup", "dup2") + .build(); + assertThat(headers.equals(other3)).isTrue(); + + final CompositeHttpHeadersBase headersEndOfStream = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1") + .toBuilder() + .endOfStream(true) + .build(), + HttpHeaders.of("k2", "v2", + "k3", "v3")); + final HttpHeaders other4 = HttpHeaders.builder() + .add("k1", "v1") + .add("k2", "v2") + .add("k3", "v3") + .endOfStream(true) + .build(); + assertThat(headersEndOfStream.equals(other4)).isTrue(); + } + + @Test + void equals_false() { + final CompositeHttpHeadersBase headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of()); + assertThat(headers.equals(null)).isFalse(); + + final CompositeHttpHeadersBase other1 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k3", "v3"), + HttpHeaders.of()); + assertThat(headers.equals(other1)).isFalse(); + + final CompositeHttpHeadersBase other2 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of()); + other2.endOfStream(true); + assertThat(headers.equals(other2)).isFalse(); + + final HttpHeaders other3 = HttpHeaders.of("k1", "v1"); + assertThat(headers.equals(other3)).isFalse(); + + final HttpHeaders other4 = HttpHeaders.builder() + .add("k1", "v1") + .add("k2", "v2") + .add("k3", "v3") + .endOfStream(true) + .build(); + assertThat(headers.equals(other4)).isFalse(); + } + + @Test + void testToString() { + final CompositeHttpHeadersBase headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.toString()) + .isEqualTo("[k1=v1, k2=v2, k3=v3, k4=v4, k5=v5, k6=v6, dup=dup1, dup=dup2]"); + + headers.endOfStream(true); + assertThat(headers.toString()) + .isEqualTo("[EOS, k1=v1, k2=v2, k3=v3, k4=v4, k5=v5, k6=v6, dup=dup1, dup=dup2]"); + + headers.clear(); + headers.endOfStream(false); + assertThat(headers.toString()).isEqualTo("[]"); + assertThat(new CompositeHttpHeadersBase(HttpHeaders.of()).toString()).isEqualTo("[]"); + + headers.endOfStream(true); + assertThat(headers.toString()).isEqualTo("[EOS]"); + assertThat(new CompositeHttpHeadersBase(HttpHeaders.of().toBuilder().endOfStream(true)).toString()) + .isEqualTo("[EOS]"); + } + + @Test + void testToString_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeHttpHeadersBase headers = new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.toString()) + .isEqualTo("[k1=additional1, additional=additional2, additional=additional3, " + + "k2=v2, k3=v3, k4=v4, k5=v5, k6=v6, dup=dup1, dup=dup2, " + + "default=default2, default=default3]"); + + headers.endOfStream(true); + assertThat(headers.toString()) + .isEqualTo("[EOS, k1=additional1, additional=additional2, additional=additional3, " + + "k2=v2, k3=v3, k4=v4, k5=v5, k6=v6, dup=dup1, dup=dup2, " + + "default=default2, default=default3]"); + + headers.clear(); + headers.endOfStream(false); + assertThat(headers.toString()).isEqualTo("[]"); + + headers.endOfStream(true); + assertThat(headers.toString()).isEqualTo("[EOS]"); + } +} diff --git a/core/src/test/java/com/linecorp/armeria/common/CompositeStringMultiMapTest.java b/core/src/test/java/com/linecorp/armeria/common/CompositeStringMultiMapTest.java new file mode 100644 index 00000000000..c9e215c1cba --- /dev/null +++ b/core/src/test/java/com/linecorp/armeria/common/CompositeStringMultiMapTest.java @@ -0,0 +1,2405 @@ +/* + * Copyright 2023 LINE Corporation + * + * LINE Corporation licenses this file to you 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: + * + * https://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.linecorp.armeria.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.util.AbstractMap.SimpleEntry; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableList; + +import io.netty.util.AsciiString; + +class CompositeStringMultiMapTest { + + @Test + void get() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.get("k1")).isEqualTo("v1"); + assertThat(headers.get("k2")).isEqualTo("v2"); + assertThat(headers.get("k3")).isEqualTo("v3"); + assertThat(headers.get("k4")).isEqualTo("v4"); + assertThat(headers.get("k5")).isEqualTo("v5"); + assertThat(headers.get("k6")).isEqualTo("v6"); + assertThat(headers.get("dup")).isEqualTo("dup1"); + assertThat(headers.get("not_exist")).isNull(); + assertThat(headers.get("not_exist", "defaultValue")).isEqualTo("defaultValue"); + + headers.add("dup", "dup3"); + assertThat(headers.get("dup")).isEqualTo("dup1"); + + headers.remove("dup"); + assertThat(headers.get("dup")).isNull(); + + headers.add("dup", "dup4"); + headers.add("dup", "dup5"); + assertThat(headers.get("dup")).isEqualTo("dup4"); + + headers.remove("dup"); + assertThat(headers.get("dup")).isNull(); + + headers.add("dup", "dup6"); + headers.add("dup", "dup7"); + assertThat(headers.get("dup")).isEqualTo("dup6"); + } + + @Test + void get_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.get("k1")).isEqualTo("additional1"); + assertThat(headers.get("additional")).isEqualTo("additional"); + assertThat(headers.get("k2")).isEqualTo("v2"); + assertThat(headers.get("k3")).isEqualTo("v3"); + assertThat(headers.get("k4")).isEqualTo("v4"); + assertThat(headers.get("k5")).isEqualTo("v5"); + assertThat(headers.get("k6")).isEqualTo("v6"); + assertThat(headers.get("dup")).isEqualTo("dup1"); + assertThat(headers.get("default")).isEqualTo("default"); + assertThat(headers.get("not_exist")).isNull(); + assertThat(headers.get("not_exist", "defaultValue")).isEqualTo("defaultValue"); + + headers.add("dup", "dup3"); + assertThat(headers.get("dup")).isEqualTo("dup1"); + + headers.remove("dup"); + assertThat(headers.get("dup")).isNull(); + + headers.add("dup", "dup4"); + headers.add("dup", "dup5"); + assertThat(headers.get("dup")).isEqualTo("dup4"); + + headers.remove("dup"); + assertThat(headers.get("dup")).isNull(); + + headers.add("dup", "dup6"); + headers.add("dup", "dup7"); + assertThat(headers.get("dup")).isEqualTo("dup6"); + + headers.remove("additional"); + assertThat(headers.get("additional")).isNull(); + headers.remove("default"); + assertThat(headers.get("default")).isNull(); + } + + @Test + void getLast() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k1", "v2", + "k1", "v3"), + HttpHeaders.of("k1", "v4", + "k1", "v5", + "k1", "v6"), + HttpHeaders.of()); + assertThat(headers.getLast("k1")).isEqualTo("v6"); + assertThat(headers.getLast("not_exist")).isNull(); + assertThat(headers.getLast("not_exist", "defaultValue")).isEqualTo("defaultValue"); + + headers.add("k1", "v7"); + assertThat(headers.getLast("k1")).isEqualTo("v7"); + + headers.remove("k1"); + assertThat(headers.getLast("k1")).isNull(); + + headers.add("k1", "v8"); + headers.add("k1", "v9"); + assertThat(headers.getLast("k1")).isEqualTo("v9"); + + headers.remove("k1"); + assertThat(headers.getLast("k1")).isNull(); + + headers.add("k1", "v10"); + headers.add("k1", "v11"); + assertThat(headers.getLast("k1")).isEqualTo("v11"); + } + + @Test + void getLast_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k1", "v2", + "k1", "v3"), + HttpHeaders.of("k1", "v4", + "k1", "v5", + "k1", "v6"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.getLast("k1")).isEqualTo("additional1"); + assertThat(headers.getLast("additional")).isEqualTo("additional"); + assertThat(headers.getLast("default")).isEqualTo("default"); + assertThat(headers.getLast("k1")).isEqualTo("additional1"); + assertThat(headers.getLast("not_exist")).isNull(); + assertThat(headers.getLast("not_exist", "defaultValue")).isEqualTo("defaultValue"); + + headers.add("k1", "v7"); + assertThat(headers.getLast("k1")).isEqualTo("v7"); + + headers.remove("k1"); + assertThat(headers.getLast("k1")).isNull(); + + headers.add("k1", "v8"); + headers.add("k1", "v9"); + assertThat(headers.getLast("k1")).isEqualTo("v9"); + + headers.remove("k1"); + assertThat(headers.getLast("k1")).isNull(); + + headers.add("k1", "v10"); + headers.add("k1", "v11"); + assertThat(headers.getLast("k1")).isEqualTo("v11"); + + headers.remove("additional"); + assertThat(headers.getLast("additional")).isNull(); + headers.remove("default"); + assertThat(headers.getLast("default")).isNull(); + } + + @Test + void getAll() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1", + "k2", "v2", + "k2", "v3"), + HttpHeaders.of("k3", "v4", + "k3", "v5"), + HttpHeaders.of("k3", "v6"), + HttpHeaders.of()); + headers.add("k3", "v7"); + headers.add("k4", "v8"); + headers.remove("k2"); + headers.add("k0", "v0"); + headers.add("k4", "v9"); + + assertThat(headers.getAll("k0")).isEqualTo(ImmutableList.of("v0")); + assertThat(headers.getAll("k1")).isEqualTo(ImmutableList.of("v1")); + assertThat(headers.getAll("k2")).isEmpty(); + assertThat(headers.getAll("k3")).isEqualTo(ImmutableList.of("v4", "v5", "v6", "v7")); + assertThat(headers.getAll("k4")).isEqualTo(ImmutableList.of("v8", "v9")); + assertThat(headers.getAll("not_exist")).isEqualTo(ImmutableList.of()); + } + + @Test + void getAll_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1", + "k2", "v2", + "k2", "v3"), + HttpHeaders.of("k3", "v4", + "k3", "v5"), + HttpHeaders.of("k3", "v6"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.add("k3", "v7"); + headers.add("k4", "v8"); + headers.remove("k2"); + headers.add("k0", "v0"); + headers.add("k4", "v9"); + + assertThat(headers.getAll("k0")).isEqualTo(ImmutableList.of("v0")); + assertThat(headers.getAll("k1")).isEqualTo(ImmutableList.of("additional1")); + assertThat(headers.getAll("additional")).isEqualTo(ImmutableList.of("additional2", "additional3")); + assertThat(headers.getAll("default")).isEqualTo(ImmutableList.of("default2", "default3")); + assertThat(headers.getAll("k2")).isEmpty(); + assertThat(headers.getAll("k3")).isEqualTo(ImmutableList.of("v4", "v5", "v6", "v7")); + assertThat(headers.getAll("k4")).isEqualTo(ImmutableList.of("v8", "v9")); + assertThat(headers.getAll("not_exist")).isEmpty(); + + headers.remove("additional"); + assertThat(headers.getAll("additional")).isEmpty(); + headers.remove("default"); + assertThat(headers.getAll("default")).isEmpty(); + } + + @Test + void getBoolean() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "true"), + HttpHeaders.of("k2", "true", + "k3", "true"), + HttpHeaders.of("k4", "false", + "k5", "false", + "k6", "false"), + HttpHeaders.of("dup", "true"), + HttpHeaders.of("dup", "false"), + HttpHeaders.of("XXX", "xxx"), + HttpHeaders.of()); + assertThat(headers.getBoolean("k1")).isTrue(); + assertThat(headers.getBoolean("k2")).isTrue(); + assertThat(headers.getBoolean("k3")).isTrue(); + assertThat(headers.getBoolean("k4")).isFalse(); + assertThat(headers.getBoolean("k5")).isFalse(); + assertThat(headers.getBoolean("k6")).isFalse(); + assertThat(headers.getBoolean("dup")).isTrue(); + assertThat(headers.getInt("XXX")).isNull(); + assertThat(headers.getBoolean("not_exist")).isNull(); + assertThat(headers.getBoolean("not_exist", true)).isTrue(); + } + + @Test + void getLastBoolean() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "true"), + HttpHeaders.of("k1", "true", + "k1", "true"), + HttpHeaders.of("k1", "false", + "k1", "false", + "k1", "false"), + HttpHeaders.of()); + assertThat(headers.getLastBoolean("k1")).isFalse(); + assertThat(headers.getLastBoolean("not_exist")).isNull(); + assertThat(headers.getLastBoolean("not_exist", true)).isTrue(); + } + + @Test + void getInt() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "1"), + HttpHeaders.of("k2", "2", + "k3", "3"), + HttpHeaders.of("k4", "4", + "k5", "5", + "k6", "6"), + HttpHeaders.of("dup", "100"), + HttpHeaders.of("dup", "101"), + HttpHeaders.of("XXX", "xxx"), + HttpHeaders.of()); + assertThat(headers.getInt("k1")).isEqualTo(1); + assertThat(headers.getInt("k2")).isEqualTo(2); + assertThat(headers.getInt("k3")).isEqualTo(3); + assertThat(headers.getInt("k4")).isEqualTo(4); + assertThat(headers.getInt("k5")).isEqualTo(5); + assertThat(headers.getInt("k6")).isEqualTo(6); + assertThat(headers.getInt("dup")).isEqualTo(100); + assertThat(headers.getInt("XXX")).isNull(); + assertThat(headers.getInt("not_exist")).isNull(); + assertThat(headers.getInt("not_exist", Integer.MAX_VALUE)).isEqualTo(Integer.MAX_VALUE); + } + + @Test + void getLastInt() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "1"), + HttpHeaders.of("k1", "2", + "k1", "3"), + HttpHeaders.of("k1", "4", + "k1", "5", + "k1", "6"), + HttpHeaders.of()); + assertThat(headers.getLastInt("k1")).isEqualTo(6); + assertThat(headers.getLastInt("not_exist")).isNull(); + assertThat(headers.getLastInt("not_exist", Integer.MAX_VALUE)).isEqualTo(Integer.MAX_VALUE); + } + + @Test + void getLong() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "1"), + HttpHeaders.of("k2", "2", + "k3", "3"), + HttpHeaders.of("k4", "4", + "k5", "5", + "k6", "6"), + HttpHeaders.of("dup", "100"), + HttpHeaders.of("dup", "101"), + HttpHeaders.of("XXX", "xxx"), + HttpHeaders.of()); + assertThat(headers.getLong("k1")).isEqualTo(1L); + assertThat(headers.getLong("k2")).isEqualTo(2L); + assertThat(headers.getLong("k3")).isEqualTo(3L); + assertThat(headers.getLong("k4")).isEqualTo(4L); + assertThat(headers.getLong("k5")).isEqualTo(5L); + assertThat(headers.getLong("k6")).isEqualTo(6L); + assertThat(headers.getLong("dup")).isEqualTo(100L); + assertThat(headers.getLong("XXX")).isNull(); + assertThat(headers.getLong("not_exist")).isNull(); + assertThat(headers.getLong("not_exist", Long.MAX_VALUE)).isEqualTo(Long.MAX_VALUE); + } + + @Test + void getLastLong() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "1"), + HttpHeaders.of("k1", "2", + "k1", "3"), + HttpHeaders.of("k1", "4", + "k1", "5", + "k1", "6"), + HttpHeaders.of()); + assertThat(headers.getLastLong("k1")).isEqualTo(6L); + assertThat(headers.getLastLong("not_exist")).isNull(); + assertThat(headers.getLastLong("not_exist", Long.MAX_VALUE)).isEqualTo(Long.MAX_VALUE); + } + + @Test + void getFloat() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "1.0"), + HttpHeaders.of("k2", "2.0", + "k3", "3.0"), + HttpHeaders.of("k4", "4.0", + "k5", "5.0", + "k6", "6.0"), + HttpHeaders.of("dup", "100.0"), + HttpHeaders.of("dup", "101.0"), + HttpHeaders.of("XXX", "xxx"), + HttpHeaders.of()); + assertThat(headers.getFloat("k1")).isEqualTo(1.0f); + assertThat(headers.getFloat("k2")).isEqualTo(2.0f); + assertThat(headers.getFloat("k3")).isEqualTo(3.0f); + assertThat(headers.getFloat("k4")).isEqualTo(4.0f); + assertThat(headers.getFloat("k5")).isEqualTo(5.0f); + assertThat(headers.getFloat("k6")).isEqualTo(6.0f); + assertThat(headers.getFloat("dup")).isEqualTo(100.0f); + assertThat(headers.getFloat("XXX")).isNull(); + assertThat(headers.getFloat("not_exist")).isNull(); + assertThat(headers.getFloat("not_exist", 123.0f)).isEqualTo(123.0f); + } + + @Test + void getLastFloat() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "1.0"), + HttpHeaders.of("k1", "2.0", + "k1", "3.0"), + HttpHeaders.of("k1", "4.0", + "k1", "5.0", + "k1", "6.0"), + HttpHeaders.of()); + assertThat(headers.getLastFloat("k1")).isEqualTo(6.0f); + assertThat(headers.getLastFloat("not_exist")).isNull(); + assertThat(headers.getLastFloat("not_exist", 123.0f)).isEqualTo(123.0f); + } + + @Test + void getDouble() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "1.0"), + HttpHeaders.of("k2", "2.0", + "k3", "3.0"), + HttpHeaders.of("k4", "4.0", + "k5", "5.0", + "k6", "6.0"), + HttpHeaders.of("dup", "100.0"), + HttpHeaders.of("dup", "101.0"), + HttpHeaders.of("XXX", "xxx"), + HttpHeaders.of()); + assertThat(headers.getDouble("k1")).isEqualTo(1.0); + assertThat(headers.getDouble("k2")).isEqualTo(2.0); + assertThat(headers.getDouble("k3")).isEqualTo(3.0); + assertThat(headers.getDouble("k4")).isEqualTo(4.0); + assertThat(headers.getDouble("k5")).isEqualTo(5.0); + assertThat(headers.getDouble("k6")).isEqualTo(6.0); + assertThat(headers.getDouble("dup")).isEqualTo(100.0); + assertThat(headers.getDouble("XXX")).isNull(); + assertThat(headers.getDouble("not_exist")).isNull(); + assertThat(headers.getDouble("not_exist", 123.0)).isEqualTo(123.0); + } + + @Test + void getLastDouble() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "1.0"), + HttpHeaders.of("k1", "2.0", + "k1", "3.0"), + HttpHeaders.of("k1", "4.0", + "k1", "5.0", + "k1", "6.0"), + HttpHeaders.of()); + assertThat(headers.getLastDouble("k1")).isEqualTo(6.0); + assertThat(headers.getLastDouble("not_exist")).isNull(); + assertThat(headers.getLastDouble("not_exist", 123.0)).isEqualTo(123.0); + } + + @Test + void getTimeMillis() { + final CompositeStringMultimap headers = new CompositeHttpHeadersBase( + HttpHeaders.of(), + HttpHeaders.of("k1", Date.from(Instant.ofEpochMilli(1000))), + HttpHeaders.of("k2", Date.from(Instant.ofEpochMilli(2000)), + "k3", Date.from(Instant.ofEpochMilli(3000))), + HttpHeaders.of("dup", Date.from(Instant.ofEpochMilli(4000))), + HttpHeaders.of("dup", Date.from(Instant.ofEpochMilli(5000))), + HttpHeaders.of("XXX", "xxx"), + HttpHeaders.of() + ); + assertThat(headers.getTimeMillis("k1")).isEqualTo(1000L); + assertThat(headers.getTimeMillis("k2")).isEqualTo(2000L); + assertThat(headers.getTimeMillis("k3")).isEqualTo(3000L); + assertThat(headers.getTimeMillis("dup")).isEqualTo(4000L); + assertThat(headers.getTimeMillis("XXX")).isNull(); + assertThat(headers.getTimeMillis("not_exist")).isNull(); + assertThat(headers.getLong("not_exist", Long.MAX_VALUE)).isEqualTo(Long.MAX_VALUE); + } + + @Test + void getLastTimeMillis() { + final CompositeStringMultimap headers = new CompositeHttpHeadersBase( + HttpHeaders.of(), + HttpHeaders.of("k1", Date.from(Instant.ofEpochMilli(1000))), + HttpHeaders.of("k1", Date.from(Instant.ofEpochMilli(2000)), + "k1", Date.from(Instant.ofEpochMilli(3000))), + HttpHeaders.of() + ); + assertThat(headers.getLastTimeMillis("k1")).isEqualTo(3000L); + assertThat(headers.getLastTimeMillis("not_exist")).isNull(); + assertThat(headers.getLastTimeMillis("not_exist", Long.MAX_VALUE)).isEqualTo(Long.MAX_VALUE); + } + + @Test + void contains() { + final CompositeStringMultimap headers = new CompositeHttpHeadersBase( + HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "object", + "k3", "true"), + HttpHeaders.of("k4", "100", + "k4", "200"), + HttpHeaders.of("k4", "300.0", + "k4", "400.0"), + HttpHeaders.of("k5", Date.from(Instant.ofEpochMilli(1000))), + HttpHeaders.of() + ); + assertThat(headers.contains("k1")).isTrue(); + assertThat(headers.contains("k1", "v1")).isTrue(); + headers.remove("k1"); + assertThat(headers.contains("k1")).isFalse(); + headers.add("k1", "v2"); + assertThat(headers.contains("k1")).isTrue(); + assertThat(headers.contains("k1")).isTrue(); + assertThat(headers.contains("k1", "v1")).isFalse(); + assertThat(headers.contains("k1", "v2")).isTrue(); + assertThat(headers.containsObject("k2", "object")).isTrue(); + assertThat(headers.containsBoolean("k3", true)).isTrue(); + assertThat(headers.containsInt("k4", 100)).isTrue(); + assertThat(headers.containsLong("k4", 200L)).isTrue(); + assertThat(headers.containsFloat("k4", 300.0f)).isTrue(); + assertThat(headers.containsDouble("k4", 400.0)).isTrue(); + assertThat(headers.containsTimeMillis("k5", 1000L)).isTrue(); + } + + @Test + void contains_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = ImmutableList.of( + HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "object", + "k3", "true"), + HttpHeaders.of("k4", "100", + "k4", "200"), + HttpHeaders.of("k4", "300.0", + "k4", "400.0"), + HttpHeaders.of("k5", Date.from(Instant.ofEpochMilli(1000))), + HttpHeaders.of() + ); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.contains("k1")).isTrue(); + assertThat(headers.contains("k1", "additional1")).isTrue(); + assertThat(headers.contains("k1", "v1")).isFalse(); + assertThat(headers.contains("additional")).isTrue(); + assertThat(headers.contains("additional", "additional2")).isTrue(); + assertThat(headers.contains("additional", "additional3")).isTrue(); + assertThat(headers.contains("default")).isTrue(); + assertThat(headers.contains("default", "default2")).isTrue(); + assertThat(headers.contains("default", "default3")).isTrue(); + headers.remove("k1"); + assertThat(headers.contains("k1")).isFalse(); + headers.add("k1", "v2"); + assertThat(headers.contains("k1")).isTrue(); + assertThat(headers.contains("k1", "additional1")).isFalse(); + assertThat(headers.contains("k1", "v1")).isFalse(); + assertThat(headers.contains("k1", "v2")).isTrue(); + assertThat(headers.containsObject("k2", "object")).isTrue(); + assertThat(headers.containsBoolean("k3", true)).isTrue(); + assertThat(headers.containsInt("k4", 100)).isTrue(); + assertThat(headers.containsLong("k4", 200L)).isTrue(); + assertThat(headers.containsFloat("k4", 300.0f)).isTrue(); + assertThat(headers.containsDouble("k4", 400.0)).isTrue(); + assertThat(headers.containsTimeMillis("k5", 1000L)).isTrue(); + + headers.remove("additional"); + assertThat(headers.contains("additional")).isFalse(); + headers.remove("default"); + assertThat(headers.contains("default")).isFalse(); + } + + @Test + void size() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.size()).isEqualTo(8); + + headers.add("k1", "v1.a"); + headers.add("k7", "v7"); + headers.add("dup", "dup3"); + assertThat(headers.size()).isEqualTo(11); + + headers.remove("dup"); + assertThat(headers.size()).isEqualTo(8); + + headers.remove("k1"); + headers.remove("k2"); + headers.remove("k3"); + assertThat(headers.size()).isEqualTo(4); + + headers.remove("k4"); + headers.remove("k5"); + headers.remove("k6"); + headers.remove("k7"); + assertThat(headers.size()).isZero(); + assertThat(headers.isEmpty()).isTrue(); + } + + @Test + void size_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.size()).isEqualTo(12); + + headers.add("additional", "additional4"); + headers.add("default", "default4"); + assertThat(headers.size()).isEqualTo(14); + + headers.remove("additional"); + assertThat(headers.size()).isEqualTo(11); + headers.remove("default"); + assertThat(headers.size()).isEqualTo(8); + + headers.add("k1", "v1.a"); + headers.add("k7", "v7"); + headers.add("dup", "dup3"); + assertThat(headers.size()).isEqualTo(11); + + headers.remove("dup"); + assertThat(headers.size()).isEqualTo(8); + + headers.remove("k1"); + headers.remove("k2"); + headers.remove("k3"); + assertThat(headers.size()).isEqualTo(4); + + headers.remove("k4"); + headers.remove("k5"); + headers.remove("k6"); + headers.remove("k7"); + assertThat(headers.size()).isZero(); + assertThat(headers.isEmpty()).isTrue(); + } + + @Test + void isEmpty() { + final CompositeStringMultimap empty1 = + new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(empty1.isEmpty()).isTrue(); + assertThat(empty1.size()).isZero(); + assertThat(empty1.get("not_exist")).isNull(); + assertThat(empty1.names()).isEmpty(); + + final CompositeStringMultimap empty2 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of(), + HttpHeaders.builder() + .add("k1", "v1") + .removeAndThen("k1") + .build(), + HttpHeaders.builder() + .add("k1", "v1", + "k1", "v2") + .removeAndThen("k1") + .build(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2"), + HttpHeaders.of("k3", "v3"), + HttpHeaders.of()); + assertThat(empty2.isEmpty()).isFalse(); + + empty2.remove("k1"); + empty2.remove("k2"); + empty2.remove("k3"); + assertThat(empty2.isEmpty()).isTrue(); + assertThat(empty2.size()).isZero(); + assertThat(empty2.get("not_exist")).isNull(); + assertThat(empty2.names()).isEmpty(); + } + + @Test + void isEmpty_mergedWithAdditionalsAndDefaults() { + final CompositeStringMultimap empty1 = + new CompositeHttpHeadersBase(HttpHeaders.of()); + assertThat(empty1.isEmpty()).isTrue(); + assertThat(empty1.size()).isZero(); + assertThat(empty1.get("not_exist")).isNull(); + assertThat(empty1.names()).isEmpty(); + + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of(), + HttpHeaders.builder() + .add("k1", "v1") + .removeAndThen("k1") + .build(), + HttpHeaders.builder() + .add("k1", "v1", + "k1", "v2") + .removeAndThen("k1") + .build(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2"), + HttpHeaders.of("k3", "v3"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap empty2 = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(empty2.isEmpty()).isFalse(); + + empty2.remove("additional"); + empty2.remove("default"); + empty2.remove("k1"); + empty2.remove("k2"); + empty2.remove("k3"); + assertThat(empty2.isEmpty()).isTrue(); + assertThat(empty2.size()).isZero(); + assertThat(empty2.get("not_exist")).isNull(); + assertThat(empty2.names()).isEmpty(); + } + + @Test + void names() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final Set expected = ImmutableList + .of("k1", "k2", "k3", "k4", "k5", "k6", "dup") + .stream() + .map(AsciiString::of) + .collect(Collectors.toSet()); + assertThat(headers.names()).isEqualTo(expected); + } + + @Test + void names_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + final Set expected = ImmutableList + .of("additional", "k1", "k2", "k3", "k4", "k5", "k6", "dup", "default") + .stream() + .map(AsciiString::of) + .collect(Collectors.toSet()); + assertThat(headers.names()).isEqualTo(expected); + } + + @Test + void names_removed() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + headers.add("add1", "v1"); + headers.remove("k2"); + headers.remove("k4"); + headers.add("add2", "v1"); + headers.remove("k6"); + headers.remove("dup"); + headers.add("add3", "v1"); + headers.remove("dup"); + + final Set expected = ImmutableList + .of("add1", "add2", "add3", "k1", "k3", "k5") + .stream() + .map(AsciiString::of) + .collect(Collectors.toSet()); + assertThat(headers.names()).isEqualTo(expected); + assertThat(headers.names().size()).isEqualTo(6); + } + + @Test + void names_removed_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.remove("additional"); + headers.add("add1", "v1"); + headers.remove("k2"); + headers.remove("k4"); + headers.add("add2", "v1"); + headers.remove("k6"); + headers.remove("dup"); + headers.add("add3", "v1"); + headers.remove("dup"); + + final Set expected = ImmutableList + .of("add1", "add2", "add3", "k1", "k3", "k5", "default") + .stream() + .map(AsciiString::of) + .collect(Collectors.toSet()); + assertThat(headers.names()).isEqualTo(expected); + } + + @Test + void iterator() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final Iterator> iterator = headers.iterator(); + final ImmutableList.Builder> builder = ImmutableList.builder(); + + final List> expected = builder + .add(new SimpleEntry<>(AsciiString.of("k1"), "v1"), + new SimpleEntry<>(AsciiString.of("k2"), "v2"), + new SimpleEntry<>(AsciiString.of("k3"), "v3"), + new SimpleEntry<>(AsciiString.of("k4"), "v4"), + new SimpleEntry<>(AsciiString.of("k5"), "v5"), + new SimpleEntry<>(AsciiString.of("k6"), "v6"), + new SimpleEntry<>(AsciiString.of("dup"), "dup1"), + new SimpleEntry<>(AsciiString.of("dup"), "dup2")) + .build(); + int i; + for (i = 0; iterator.hasNext(); i++) { + assertThat(iterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + } + + @Test + void iterator_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + final Iterator> iterator = headers.iterator(); + final ImmutableList.Builder> builder = ImmutableList.builder(); + + final List> expected = builder + .add(new SimpleEntry<>(AsciiString.of("k1"), "additional1"), + new SimpleEntry<>(AsciiString.of("additional"), "additional2"), + new SimpleEntry<>(AsciiString.of("additional"), "additional3"), + new SimpleEntry<>(AsciiString.of("k2"), "v2"), + new SimpleEntry<>(AsciiString.of("k3"), "v3"), + new SimpleEntry<>(AsciiString.of("k4"), "v4"), + new SimpleEntry<>(AsciiString.of("k5"), "v5"), + new SimpleEntry<>(AsciiString.of("k6"), "v6"), + new SimpleEntry<>(AsciiString.of("dup"), "dup1"), + new SimpleEntry<>(AsciiString.of("dup"), "dup2"), + new SimpleEntry<>(AsciiString.of("default"), "default2"), + new SimpleEntry<>(AsciiString.of("default"), "default3")) + .build(); + int i; + for (i = 0; iterator.hasNext(); i++) { + assertThat(iterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + } + + @Test + void iterator_removed() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + headers.remove("k2"); + headers.remove("k4"); + headers.add("k7", "v7"); + headers.remove("k6"); + headers.remove("dup"); + final Iterator> iterator = headers.iterator(); + final ImmutableList.Builder> builder = ImmutableList.builder(); + + final List> expected = builder + .add(new SimpleEntry<>(AsciiString.of("k1"), "v1"), + new SimpleEntry<>(AsciiString.of("k3"), "v3"), + new SimpleEntry<>(AsciiString.of("k5"), "v5"), + new SimpleEntry<>(AsciiString.of("k7"), "v7")) + .build(); + int i; + for (i = 0; iterator.hasNext(); i++) { + assertThat(iterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + } + + @Test + void iterator_removed_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.remove("additional"); + headers.remove("k2"); + headers.remove("k4"); + headers.add("k7", "v7"); + headers.remove("k6"); + headers.remove("dup"); + final Iterator> iterator = headers.iterator(); + final ImmutableList.Builder> builder = ImmutableList.builder(); + + final List> expected = builder + .add(new SimpleEntry<>(AsciiString.of("k1"), "additional1"), + new SimpleEntry<>(AsciiString.of("k3"), "v3"), + new SimpleEntry<>(AsciiString.of("k5"), "v5"), + new SimpleEntry<>(AsciiString.of("default"), "default2"), + new SimpleEntry<>(AsciiString.of("default"), "default3"), + new SimpleEntry<>(AsciiString.of("k7"), "v7")) + .build(); + int i; + for (i = 0; iterator.hasNext(); i++) { + assertThat(iterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + } + + @Test + void valueIterator() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("X", "v2", + "k1", "v3"), + HttpHeaders.of("X", "v4", + "k1", "v5", + "X", "v6"), + HttpHeaders.of("k1", "dup1"), + HttpHeaders.of("k1", "dup2"), + HttpHeaders.of()); + final Iterator valueIterator = headers.valueIterator("k1"); + + final List expected = ImmutableList.of("v1", "v3", "v5", "dup1", "dup2"); + for (int i = 0; valueIterator.hasNext(); i++) { + assertThat(valueIterator.next()).isEqualTo(expected.get(i)); + } + } + + @Test + void valueIterator_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("X", "v2", + "k1", "v3"), + HttpHeaders.of("X", "v4", + "k2", "v5", + "X", "v6"), + HttpHeaders.of("k2", "dup1"), + HttpHeaders.of("k2", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + Iterator valueIterator = headers.valueIterator("k1"); + + List expected = ImmutableList.of("additional1"); + int i; + for (i = 0; valueIterator.hasNext(); i++) { + assertThat(valueIterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + + valueIterator = headers.valueIterator("k2"); + expected = ImmutableList.of("v5", "dup1", "dup2"); + for (i = 0; valueIterator.hasNext(); i++) { + assertThat(valueIterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + + valueIterator = headers.valueIterator("additional"); + expected = ImmutableList.of("additional2", "additional3"); + for (i = 0; valueIterator.hasNext(); i++) { + assertThat(valueIterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + + valueIterator = headers.valueIterator("default"); + expected = ImmutableList.of("default2", "default3"); + for (i = 0; valueIterator.hasNext(); i++) { + assertThat(valueIterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + } + + @Test + void valueIterator_removed() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("X", "v2", + "k1", "v3"), + HttpHeaders.of("X", "v4", + "k1", "v5", + "X", "v6"), + HttpHeaders.of("k1", "dup1"), + HttpHeaders.of("k1", "dup2"), + HttpHeaders.of()); + headers.remove("k1"); + final Iterator emptyIterator = headers.valueIterator("k1"); + assertThat(emptyIterator.hasNext()).isFalse(); + + headers.add("k1", "v1"); + headers.add("k1", "v2"); + headers.add("k1", "v3"); + final Iterator valueIterator = headers.valueIterator("k1"); + + final List expected = ImmutableList.of("v1", "v2", "v3"); + for (int i = 0; valueIterator.hasNext(); i++) { + assertThat(valueIterator.next()).isEqualTo(expected.get(i)); + } + } + + @Test + void valueIterator_removed_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("X", "v2", + "k1", "v3"), + HttpHeaders.of("X", "v4", + "k1", "v5", + "X", "v6"), + HttpHeaders.of("k1", "dup1"), + HttpHeaders.of("k1", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.remove("k1"); + final Iterator emptyIterator = headers.valueIterator("k1"); + assertThat(emptyIterator.hasNext()).isFalse(); + + headers.add("k1", "v1"); + headers.add("k1", "v2"); + headers.add("k1", "v3"); + final Iterator valueIterator = headers.valueIterator("k1"); + + final List expected = ImmutableList.of("v1", "v2", "v3"); + int i; + for (i = 0; valueIterator.hasNext(); i++) { + assertThat(valueIterator.next()).isEqualTo(expected.get(i)); + } + assertThat(i).isEqualTo(expected.size()); + } + + @Test + void forEach() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + headers.add("dup", "dup3"); + headers.remove("k2"); + headers.remove("k4"); + headers.remove("k6"); + headers.add("k7", "v7"); + final ImmutableList.Builder> actual = ImmutableList.builder(); + headers.forEach((k, v) -> actual.add(new SimpleEntry<>(k, v))); + + final ImmutableList.Builder> expected = ImmutableList.builder(); + expected.add(new SimpleEntry<>(AsciiString.of("k1"), "v1"), + new SimpleEntry<>(AsciiString.of("k3"), "v3"), + new SimpleEntry<>(AsciiString.of("k5"), "v5"), + new SimpleEntry<>(AsciiString.of("dup"), "dup1"), + new SimpleEntry<>(AsciiString.of("dup"), "dup2"), + new SimpleEntry<>(AsciiString.of("dup"), "dup3"), + new SimpleEntry<>(AsciiString.of("k7"), "v7")); + assertThat(actual.build()).isEqualTo(expected.build()); + } + + @Test + void forEach_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.add("dup", "dup3"); + headers.remove("k2"); + headers.remove("k4"); + headers.remove("k6"); + headers.add("k7", "v7"); + final ImmutableList.Builder> actual = ImmutableList.builder(); + headers.forEach((k, v) -> actual.add(new SimpleEntry<>(k, v))); + + final ImmutableList.Builder> expected = ImmutableList.builder(); + expected.add(new SimpleEntry<>(AsciiString.of("k1"), "additional1"), + new SimpleEntry<>(AsciiString.of("additional"), "additional2"), + new SimpleEntry<>(AsciiString.of("additional"), "additional3"), + new SimpleEntry<>(AsciiString.of("k3"), "v3"), + new SimpleEntry<>(AsciiString.of("k5"), "v5"), + new SimpleEntry<>(AsciiString.of("dup"), "dup1"), + new SimpleEntry<>(AsciiString.of("dup"), "dup2"), + new SimpleEntry<>(AsciiString.of("default"), "default2"), + new SimpleEntry<>(AsciiString.of("default"), "default3"), + new SimpleEntry<>(AsciiString.of("dup"), "dup3"), + new SimpleEntry<>(AsciiString.of("k7"), "v7")); + assertThat(actual.build()).isEqualTo(expected.build()); + } + + @Test + void forEachValue() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("X", "v2", + "k1", "v3"), + HttpHeaders.of("X", "v4", + "k1", "v5", + "X", "v6"), + HttpHeaders.of("k1", "dup1"), + HttpHeaders.of("k1", "dup2"), + HttpHeaders.of()); + final ImmutableList.Builder builder1 = ImmutableList.builder(); + headers.forEachValue("k1", builder1::add); + final List expected1 = ImmutableList.of("v1", "v3", "v5", "dup1", "dup2"); + assertThat(builder1.build()).isEqualTo(expected1); + + headers.remove("k1"); + final ImmutableList.Builder empty = ImmutableList.builder(); + headers.forEachValue("k1", empty::add); + assertThat(empty.build()).isEmpty(); + + headers.add("k1", "v1"); + headers.add("k1", "v2"); + headers.add("k1", "v3"); + final ImmutableList.Builder builder2 = ImmutableList.builder(); + headers.forEachValue("k1", builder2::add); + final List expected2 = ImmutableList.of("v1", "v2", "v3"); + assertThat(builder2.build()).isEqualTo(expected2); + } + + @Test + void forEachValue_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("X", "v2", + "k1", "v3"), + HttpHeaders.of("X", "v4", + "k1", "v5", + "X", "v6"), + HttpHeaders.of("k1", "dup1"), + HttpHeaders.of("k1", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + final ImmutableList.Builder builder1 = ImmutableList.builder(); + headers.forEachValue("k1", builder1::add); + final List expected1 = ImmutableList.of("additional1"); + assertThat(builder1.build()).isEqualTo(expected1); + + final ImmutableList.Builder additionals1 = ImmutableList.builder(); + headers.forEachValue("additional", additionals1::add); + assertThat(additionals1.build()).isEqualTo(ImmutableList.of("additional2", "additional3")); + + final ImmutableList.Builder defaults1 = ImmutableList.builder(); + headers.forEachValue("default", defaults1::add); + assertThat(defaults1.build()).isEqualTo(ImmutableList.of("default2", "default3")); + + headers.remove("k1"); + final ImmutableList.Builder empty = ImmutableList.builder(); + headers.forEachValue("k1", empty::add); + assertThat(empty.build()).isEmpty(); + + headers.add("k1", "v1"); + headers.add("k1", "v2"); + headers.add("k1", "v3"); + final ImmutableList.Builder builder2 = ImmutableList.builder(); + headers.forEachValue("k1", builder2::add); + final List expected2 = ImmutableList.of("v1", "v2", "v3"); + assertThat(builder2.build()).isEqualTo(expected2); + } + + @Test + void getAndRemove() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k1", "v4", + "k2", "v5", + "k3", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.getAndRemove("k1")).isEqualTo("v1"); + assertThat(headers.get("k1")).isNull(); + assertThat(headers.contains("k1")).isFalse(); + assertThat(headers.size()).isEqualTo(6); + + assertThat(headers.getAndRemove("k2")).isEqualTo("v2"); + assertThat(headers.get("k2")).isNull(); + assertThat(headers.contains("k2")).isFalse(); + assertThat(headers.size()).isEqualTo(4); + + assertThat(headers.getAndRemove("k3")).isEqualTo("v3"); + assertThat(headers.get("k3")).isNull(); + assertThat(headers.contains("k3")).isFalse(); + assertThat(headers.size()).isEqualTo(2); + + assertThat(headers.getAndRemove("dup")).isEqualTo("dup1"); + assertThat(headers.get("dup")).isNull(); + assertThat(headers.contains("dup")).isFalse(); + assertThat(headers.size()).isEqualTo(0); + assertThat(headers.isEmpty()).isTrue(); + + assertThat(headers.getAndRemove("k1", "defaultValue")).isEqualTo("defaultValue"); + } + + @Test + void getAndRemove_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k1", "v4", + "k2", "v5", + "k3", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.getAndRemove("k1")).isEqualTo("additional1"); + assertThat(headers.get("k1")).isNull(); + assertThat(headers.contains("k1")).isFalse(); + assertThat(headers.size()).isEqualTo(10); + + assertThat(headers.getAndRemove("k2")).isEqualTo("v2"); + assertThat(headers.get("k2")).isNull(); + assertThat(headers.contains("k2")).isFalse(); + assertThat(headers.size()).isEqualTo(8); + + assertThat(headers.getAndRemove("k3")).isEqualTo("v3"); + assertThat(headers.get("k3")).isNull(); + assertThat(headers.contains("k3")).isFalse(); + assertThat(headers.size()).isEqualTo(6); + + assertThat(headers.getAndRemove("dup")).isEqualTo("dup1"); + assertThat(headers.get("dup")).isNull(); + assertThat(headers.contains("dup")).isFalse(); + assertThat(headers.size()).isEqualTo(4); + + assertThat(headers.getAndRemove("additional")).isEqualTo("additional2"); + assertThat(headers.get("additional")).isNull(); + assertThat(headers.contains("additional")).isFalse(); + assertThat(headers.size()).isEqualTo(2); + + assertThat(headers.getAndRemove("default")).isEqualTo("default2"); + assertThat(headers.get("default")).isNull(); + assertThat(headers.contains("default")).isFalse(); + assertThat(headers.size()).isEqualTo(0); + assertThat(headers.isEmpty()).isTrue(); + + assertThat(headers.getAndRemove("k1", "defaultValue")).isEqualTo("defaultValue"); + } + + @Test + void getAllAndRemove() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k1", "v4", + "k2", "v5", + "k3", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.getAllAndRemove("k1")).isEqualTo(ImmutableList.of("v1", "v4")); + assertThat(headers.get("k1")).isNull(); + assertThat(headers.contains("k1")).isFalse(); + assertThat(headers.size()).isEqualTo(6); + + assertThat(headers.getAllAndRemove("k2")).isEqualTo(ImmutableList.of("v2", "v5")); + assertThat(headers.get("k2")).isNull(); + assertThat(headers.contains("k2")).isFalse(); + assertThat(headers.size()).isEqualTo(4); + + assertThat(headers.getAllAndRemove("k3")).isEqualTo(ImmutableList.of("v3", "v6")); + assertThat(headers.get("k3")).isNull(); + assertThat(headers.contains("k3")).isFalse(); + assertThat(headers.size()).isEqualTo(2); + + assertThat(headers.getAllAndRemove("dup")).isEqualTo(ImmutableList.of("dup1", "dup2")); + assertThat(headers.get("dup")).isNull(); + assertThat(headers.contains("dup")).isFalse(); + assertThat(headers.size()).isEqualTo(0); + assertThat(headers.isEmpty()).isTrue(); + + assertThat(headers.getAllAndRemove("k1")).isEmpty(); + } + + @Test + void getAllAndRemove_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k1", "v4", + "k2", "v5", + "k3", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.getAllAndRemove("k1")).isEqualTo(ImmutableList.of("additional1")); + assertThat(headers.get("k1")).isNull(); + assertThat(headers.contains("k1")).isFalse(); + assertThat(headers.size()).isEqualTo(10); + + assertThat(headers.getAllAndRemove("k2")).isEqualTo(ImmutableList.of("v2", "v5")); + assertThat(headers.get("k2")).isNull(); + assertThat(headers.contains("k2")).isFalse(); + assertThat(headers.size()).isEqualTo(8); + + assertThat(headers.getAllAndRemove("k3")).isEqualTo(ImmutableList.of("v3", "v6")); + assertThat(headers.get("k3")).isNull(); + assertThat(headers.contains("k3")).isFalse(); + assertThat(headers.size()).isEqualTo(6); + + assertThat(headers.getAllAndRemove("dup")).isEqualTo(ImmutableList.of("dup1", "dup2")); + assertThat(headers.get("dup")).isNull(); + assertThat(headers.contains("dup")).isFalse(); + assertThat(headers.size()).isEqualTo(4); + + assertThat(headers.getAllAndRemove("additional")) + .isEqualTo(ImmutableList.of("additional2", "additional3")); + assertThat(headers.get("additional")).isNull(); + assertThat(headers.contains("additional")).isFalse(); + assertThat(headers.size()).isEqualTo(2); + + assertThat(headers.getAllAndRemove("default")).isEqualTo(ImmutableList.of("default2", "default3")); + assertThat(headers.get("default")).isNull(); + assertThat(headers.contains("default")).isFalse(); + assertThat(headers.size()).isEqualTo(0); + assertThat(headers.isEmpty()).isTrue(); + + assertThat(headers.getAllAndRemove("k1")).isEmpty(); + } + + @Test + void getTypeAndRemove() { + final CompositeStringMultimap headers = new CompositeHttpHeadersBase( + HttpHeaders.of(), + HttpHeaders.of("k1", "100", + "k1", "101", + "k2", "200", + "k2", "201"), + HttpHeaders.of("k3", "300.0", + "k3", "301.0", + "k4", "400.0", + "k4", "401.0"), + HttpHeaders.of("k5", Date.from(Instant.ofEpochMilli(1000)), + "k5", Date.from(Instant.ofEpochMilli(2000))), + HttpHeaders.of() + ); + assertThat(headers.getIntAndRemove("k1")).isEqualTo(100); + assertThat(headers.getIntAndRemove("k1", Integer.MAX_VALUE)).isEqualTo(Integer.MAX_VALUE); + assertThat(headers.getLongAndRemove("k2")).isEqualTo(200L); + assertThat(headers.getLongAndRemove("k2", Long.MAX_VALUE)).isEqualTo(Long.MAX_VALUE); + assertThat(headers.getFloatAndRemove("k3")).isEqualTo(300.0f); + assertThat(headers.getFloatAndRemove("k3", 123.0f)).isEqualTo(123.0f); + assertThat(headers.getDoubleAndRemove("k4")).isEqualTo(400.0); + assertThat(headers.getDoubleAndRemove("k4", 123.0)).isEqualTo(123.0); + assertThat(headers.getTimeMillisAndRemove("k5")).isEqualTo(1000); + assertThat(headers.getTimeMillisAndRemove("k5", Long.MAX_VALUE)).isEqualTo(Long.MAX_VALUE); + } + + @Test + void add() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of()); + headers.add("k1", "v1.a"); + headers.add("k1", "v1.b"); + headers.add("k2", ImmutableList.of("v2.a", "v2.b")); + headers.add("k3", "v3.a", "v3.b"); + headers.add(ImmutableList.of(new SimpleEntry<>("k4", "v4.a"), new SimpleEntry<>("k4", "v4.b"))); + headers.addObject("k5", "v5.a"); + headers.addObject("k5", "v5.b"); + headers.addObject("k6", ImmutableList.of("v6.a", "v6.b")); + headers.addObject("k7", "v7.a", "v7.b"); + headers.addObject(ImmutableList.of(new SimpleEntry<>("k8", "v8.a"), new SimpleEntry<>("k8", "v8.b"))); + + assertThat(headers.getAll("k1")).isEqualTo(ImmutableList.of("v1.a", "v1.b")); + assertThat(headers.getAll("k2")).isEqualTo(ImmutableList.of("v2.a", "v2.b")); + assertThat(headers.getAll("k3")).isEqualTo(ImmutableList.of("v3.a", "v3.b")); + assertThat(headers.getAll("k4")).isEqualTo(ImmutableList.of("v4.a", "v4.b")); + assertThat(headers.getAll("k5")).isEqualTo(ImmutableList.of("v5.a", "v5.b")); + assertThat(headers.getAll("k6")).isEqualTo(ImmutableList.of("v6.a", "v6.b")); + assertThat(headers.getAll("k7")).isEqualTo(ImmutableList.of("v7.a", "v7.b")); + assertThat(headers.getAll("k8")).isEqualTo(ImmutableList.of("v8.a", "v8.b")); + assertThat(headers.size()).isEqualTo(16); + } + + @Test + void add_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = ImmutableList.of(HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.add("k1", "v1.a"); + headers.add("k1", "v1.b"); + headers.add("k2", ImmutableList.of("v2.a", "v2.b")); + headers.add("k3", "v3.a", "v3.b"); + headers.add(ImmutableList.of(new SimpleEntry<>("k4", "v4.a"), new SimpleEntry<>("k4", "v4.b"))); + headers.addObject("k5", "v5.a"); + headers.addObject("k5", "v5.b"); + headers.addObject("k6", ImmutableList.of("v6.a", "v6.b")); + headers.addObject("k7", "v7.a", "v7.b"); + headers.addObject(ImmutableList.of(new SimpleEntry<>("k8", "v8.a"), new SimpleEntry<>("k8", "v8.b"))); + + assertThat(headers.getAll("additional")).isEqualTo(ImmutableList.of("additional2", "additional3")); + assertThat(headers.getAll("k1")).isEqualTo(ImmutableList.of("additional1", "v1.a", "v1.b")); + assertThat(headers.getAll("k2")).isEqualTo(ImmutableList.of("v2.a", "v2.b")); + assertThat(headers.getAll("k3")).isEqualTo(ImmutableList.of("v3.a", "v3.b")); + assertThat(headers.getAll("k4")).isEqualTo(ImmutableList.of("v4.a", "v4.b")); + assertThat(headers.getAll("k5")).isEqualTo(ImmutableList.of("v5.a", "v5.b")); + assertThat(headers.getAll("k6")).isEqualTo(ImmutableList.of("v6.a", "v6.b")); + assertThat(headers.getAll("k7")).isEqualTo(ImmutableList.of("v7.a", "v7.b")); + assertThat(headers.getAll("k8")).isEqualTo(ImmutableList.of("v8.a", "v8.b")); + assertThat(headers.getAll("default")).isEqualTo(ImmutableList.of("default2", "default3")); + assertThat(headers.size()).isEqualTo(21); + } + + @Test + void addType() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of()); + headers.addInt("k1", 100); + headers.addLong("k2", 200L); + headers.addFloat("k3", 300.0f); + headers.addDouble("k4", 400.0); + headers.addTimeMillis("k5", Date.from(Instant.ofEpochMilli(1000)).getTime()); + + assertThat(headers.getInt("k1")).isEqualTo(100); + assertThat(headers.getLong("k2")).isEqualTo(200L); + assertThat(headers.getFloat("k3")).isEqualTo(300.0f); + assertThat(headers.getDouble("k4")).isEqualTo(400.0); + assertThat(headers.getTimeMillis("k5")).isEqualTo(1000L); + assertThat(headers.size()).isEqualTo(5); + } + + @Test + void set() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of()); + headers.set("k1", "v1.a"); + headers.set("k2", ImmutableList.of("v2.a", "v2.b")); + headers.set("k3", "v3.a", "v3.b"); + headers.set(ImmutableList.of(new SimpleEntry<>("k4", "v4.a"), new SimpleEntry<>("k4", "v4.b"))); + headers.setObject("k5", "v5.a"); + headers.setObject("k6", ImmutableList.of("v6.a", "v6.b")); + headers.setObject("k7", "v7.a", "v7.b"); + headers.setObject(ImmutableList.of(new SimpleEntry<>("k8", "v8.a"), new SimpleEntry<>("k8", "v8.b"))); + + assertThat(headers.getAll("k1")).isEqualTo(ImmutableList.of("v1.a")); + assertThat(headers.getAll("k2")).isEqualTo(ImmutableList.of("v2.a", "v2.b")); + assertThat(headers.getAll("k3")).isEqualTo(ImmutableList.of("v3.a", "v3.b")); + assertThat(headers.getAll("k4")).isEqualTo(ImmutableList.of("v4.a", "v4.b")); + assertThat(headers.getAll("k5")).isEqualTo(ImmutableList.of("v5.a")); + assertThat(headers.getAll("k6")).isEqualTo(ImmutableList.of("v6.a", "v6.b")); + assertThat(headers.getAll("k7")).isEqualTo(ImmutableList.of("v7.a", "v7.b")); + assertThat(headers.getAll("k8")).isEqualTo(ImmutableList.of("v8.a", "v8.b")); + assertThat(headers.size()).isEqualTo(14); + + headers.set("k1", "v1.b"); + headers.set("k2", ImmutableList.of("v2.c", "v2.d")); + headers.set("k3", "v3.c", "v3.d"); + headers.set(ImmutableList.of(new SimpleEntry<>("k4", "v4.c"), new SimpleEntry<>("k4", "v4.d"))); + headers.setObject("k5", "v5.b"); + headers.setObject("k6", ImmutableList.of("v6.c", "v6.d")); + headers.setObject("k7", "v7.c", "v7.d"); + headers.setObject(ImmutableList.of(new SimpleEntry<>("k8", "v8.c"), new SimpleEntry<>("k8", "v8.d"))); + + assertThat(headers.getAll("k1")).isEqualTo(ImmutableList.of("v1.b")); + assertThat(headers.getAll("k2")).isEqualTo(ImmutableList.of("v2.c", "v2.d")); + assertThat(headers.getAll("k3")).isEqualTo(ImmutableList.of("v3.c", "v3.d")); + assertThat(headers.getAll("k4")).isEqualTo(ImmutableList.of("v4.c", "v4.d")); + assertThat(headers.getAll("k5")).isEqualTo(ImmutableList.of("v5.b")); + assertThat(headers.getAll("k6")).isEqualTo(ImmutableList.of("v6.c", "v6.d")); + assertThat(headers.getAll("k7")).isEqualTo(ImmutableList.of("v7.c", "v7.d")); + assertThat(headers.getAll("k8")).isEqualTo(ImmutableList.of("v8.c", "v8.d")); + assertThat(headers.size()).isEqualTo(14); + } + + @Test + void set_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = ImmutableList.of(HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.set("k1", "v1.a"); + headers.set("k2", ImmutableList.of("v2.a", "v2.b")); + headers.set("k3", "v3.a", "v3.b"); + headers.set(ImmutableList.of(new SimpleEntry<>("k4", "v4.a"), new SimpleEntry<>("k4", "v4.b"))); + headers.setObject("k5", "v5.a"); + headers.setObject("k6", ImmutableList.of("v6.a", "v6.b")); + headers.setObject("k7", "v7.a", "v7.b"); + headers.setObject(ImmutableList.of(new SimpleEntry<>("k8", "v8.a"), new SimpleEntry<>("k8", "v8.b"))); + + assertThat(headers.getAll("additional")).isEqualTo(ImmutableList.of("additional2", "additional3")); + assertThat(headers.getAll("k1")).isEqualTo(ImmutableList.of("v1.a")); + assertThat(headers.getAll("k2")).isEqualTo(ImmutableList.of("v2.a", "v2.b")); + assertThat(headers.getAll("k3")).isEqualTo(ImmutableList.of("v3.a", "v3.b")); + assertThat(headers.getAll("k4")).isEqualTo(ImmutableList.of("v4.a", "v4.b")); + assertThat(headers.getAll("k5")).isEqualTo(ImmutableList.of("v5.a")); + assertThat(headers.getAll("k6")).isEqualTo(ImmutableList.of("v6.a", "v6.b")); + assertThat(headers.getAll("k7")).isEqualTo(ImmutableList.of("v7.a", "v7.b")); + assertThat(headers.getAll("k8")).isEqualTo(ImmutableList.of("v8.a", "v8.b")); + assertThat(headers.getAll("default")).isEqualTo(ImmutableList.of("default2", "default3")); + assertThat(headers.size()).isEqualTo(18); + + headers.set("additional", "additional"); + headers.set("k1", "v1.b"); + headers.set("k2", ImmutableList.of("v2.c", "v2.d")); + headers.set("k3", "v3.c", "v3.d"); + headers.set(ImmutableList.of(new SimpleEntry<>("k4", "v4.c"), new SimpleEntry<>("k4", "v4.d"))); + headers.setObject("k5", "v5.b"); + headers.setObject("k6", ImmutableList.of("v6.c", "v6.d")); + headers.setObject("k7", "v7.c", "v7.d"); + headers.setObject(ImmutableList.of(new SimpleEntry<>("k8", "v8.c"), new SimpleEntry<>("k8", "v8.d"))); + headers.set("default", "default"); + + assertThat(headers.getAll("additional")).isEqualTo(ImmutableList.of("additional")); + assertThat(headers.getAll("k1")).isEqualTo(ImmutableList.of("v1.b")); + assertThat(headers.getAll("k2")).isEqualTo(ImmutableList.of("v2.c", "v2.d")); + assertThat(headers.getAll("k3")).isEqualTo(ImmutableList.of("v3.c", "v3.d")); + assertThat(headers.getAll("k4")).isEqualTo(ImmutableList.of("v4.c", "v4.d")); + assertThat(headers.getAll("k5")).isEqualTo(ImmutableList.of("v5.b")); + assertThat(headers.getAll("k6")).isEqualTo(ImmutableList.of("v6.c", "v6.d")); + assertThat(headers.getAll("k7")).isEqualTo(ImmutableList.of("v7.c", "v7.d")); + assertThat(headers.getAll("k8")).isEqualTo(ImmutableList.of("v8.c", "v8.d")); + assertThat(headers.getAll("default")).isEqualTo(ImmutableList.of("default")); + assertThat(headers.size()).isEqualTo(16); + } + + @Test + void setIfAbsent() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + headers.remove("dup"); + headers.remove("k3"); + headers.add("k3", "v3.a"); + headers.setIfAbsent(ImmutableList.of(new SimpleEntry<>("dup", "dup3"), + new SimpleEntry<>("dup", "dup4"), + new SimpleEntry<>("k1", "v1.a"), + new SimpleEntry<>("k3", "v3.b"), + new SimpleEntry<>("k7", "v7.a"), + new SimpleEntry<>("k8", "v8.a"), + new SimpleEntry<>("k7", "v7.b"), + new SimpleEntry<>("k8", "v8.b"), + new SimpleEntry<>("k9", "v9"))); + + assertThat(headers.size()).isEqualTo(13); + assertThat(headers.get("k1")).isEqualTo("v1"); + assertThat(headers.get("k2")).isEqualTo("v2"); + assertThat(headers.get("k3")).isEqualTo("v3.a"); + assertThat(headers.get("k4")).isEqualTo("v4"); + assertThat(headers.get("k5")).isEqualTo("v5"); + assertThat(headers.get("k6")).isEqualTo("v6"); + assertThat(headers.getAll("dup")).isEqualTo(ImmutableList.of("dup3", "dup4")); + assertThat(headers.getAll("k7")).isEqualTo(ImmutableList.of("v7.a", "v7.b")); + assertThat(headers.getAll("k8")).isEqualTo(ImmutableList.of("v8.a", "v8.b")); + assertThat(headers.get("k9")).isEqualTo("v9"); + } + + @Test + void setIfAbsent_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.remove("dup"); + headers.remove("k3"); + headers.add("k3", "v3.a"); + headers.setIfAbsent(ImmutableList.of(new SimpleEntry<>("dup", "dup3"), + new SimpleEntry<>("dup", "dup4"), + new SimpleEntry<>("k1", "v1.a"), + new SimpleEntry<>("k3", "v3.b"), + new SimpleEntry<>("k7", "v7.a"), + new SimpleEntry<>("k8", "v8.a"), + new SimpleEntry<>("k7", "v7.b"), + new SimpleEntry<>("k8", "v8.b"), + new SimpleEntry<>("k9", "v9"))); + + assertThat(headers.size()).isEqualTo(17); + assertThat(headers.getAll("additional")).isEqualTo(ImmutableList.of("additional2", "additional3")); + assertThat(headers.get("k1")).isEqualTo("additional1"); + assertThat(headers.get("k2")).isEqualTo("v2"); + assertThat(headers.get("k3")).isEqualTo("v3.a"); + assertThat(headers.get("k4")).isEqualTo("v4"); + assertThat(headers.get("k5")).isEqualTo("v5"); + assertThat(headers.get("k6")).isEqualTo("v6"); + assertThat(headers.getAll("dup")).isEqualTo(ImmutableList.of("dup3", "dup4")); + assertThat(headers.getAll("k7")).isEqualTo(ImmutableList.of("v7.a", "v7.b")); + assertThat(headers.getAll("k8")).isEqualTo(ImmutableList.of("v8.a", "v8.b")); + assertThat(headers.get("k9")).isEqualTo("v9"); + assertThat(headers.getAll("default")).isEqualTo(ImmutableList.of("default2", "default3")); + } + + @Test + void setType() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of()); + headers.setInt("k1", 100); + headers.setLong("k2", 200L); + headers.setFloat("k3", 300.0f); + headers.setDouble("k4", 400.0); + headers.setTimeMillis("k5", Date.from(Instant.ofEpochMilli(1000)).getTime()); + + assertThat(headers.getInt("k1")).isEqualTo(100); + assertThat(headers.getLong("k2")).isEqualTo(200L); + assertThat(headers.getFloat("k3")).isEqualTo(300.0f); + assertThat(headers.getDouble("k4")).isEqualTo(400.0); + assertThat(headers.getTimeMillis("k5")).isEqualTo(1000L); + assertThat(headers.size()).isEqualTo(5); + } + + @Test + void remove() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + headers.add("add", "add"); + headers.remove("add"); + assertThat(headers.get("add")).isNull(); + assertThat(headers.contains("add")).isFalse(); + assertThat(headers.size()).isEqualTo(8); + + assertThat(headers.remove("not_exist")).isFalse(); + assertThat(headers.size()).isEqualTo(8); + + assertThat(headers.remove("k1")).isTrue(); + assertThat(headers.remove("k2")).isTrue(); + assertThat(headers.remove("k3")).isTrue(); + assertThat(headers.remove("k4")).isTrue(); + assertThat(headers.remove("k5")).isTrue(); + assertThat(headers.remove("k6")).isTrue(); + assertThat(headers.size()).isEqualTo(2); + + assertThat(headers.remove("dup")).isTrue(); + assertThat(headers.isEmpty()).isTrue(); + + headers.add("k1", "v1"); + headers.remove("k1"); + assertThat(headers.isEmpty()).isTrue(); + + headers.add("k1", "v1"); + headers.remove("k1"); + headers.remove("k1"); + assertThat(headers.isEmpty()).isTrue(); + } + + @Test + void remove_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.add("add", "add"); + headers.remove("add"); + assertThat(headers.remove("additional")).isTrue(); + assertThat(headers.remove("default")).isTrue(); + assertThat(headers.get("add")).isNull(); + assertThat(headers.contains("add")).isFalse(); + assertThat(headers.size()).isEqualTo(8); + + assertThat(headers.remove("not_exist")).isFalse(); + assertThat(headers.size()).isEqualTo(8); + + assertThat(headers.remove("k1")).isTrue(); + assertThat(headers.remove("k2")).isTrue(); + assertThat(headers.remove("k3")).isTrue(); + assertThat(headers.remove("k4")).isTrue(); + assertThat(headers.remove("k5")).isTrue(); + assertThat(headers.remove("k6")).isTrue(); + assertThat(headers.size()).isEqualTo(2); + + assertThat(headers.remove("dup")).isTrue(); + assertThat(headers.isEmpty()).isTrue(); + + headers.add("k1", "v1"); + headers.remove("k1"); + assertThat(headers.isEmpty()).isTrue(); + + headers.add("k1", "v1"); + headers.remove("k1"); + headers.remove("k1"); + assertThat(headers.isEmpty()).isTrue(); + } + + @Test + void clear() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + headers.add("add", "add"); + headers.remove("k1"); + headers.clear(); + assertThat(headers.isEmpty()).isTrue(); + } + + @Test + void clear_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + headers.add("add", "add"); + headers.remove("k1"); + headers.clear(); + assertThat(headers.isEmpty()).isTrue(); + } + + @Test + void equals_true() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.equals(headers)).isTrue(); + + final CompositeStringMultimap other1 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.equals(other1)).isTrue(); + + final CompositeStringMultimap other2 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("diff1", "diff1"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("diff2", "diff2"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of()); + other2.remove("diff1"); + other2.add("dup", "dup2"); + other2.remove("diff2"); + assertThat(headers.equals(other2)).isTrue(); + + final HttpHeaders other3 = HttpHeaders.builder() + .add("k1", "v1") + .add("k2", "v2") + .add("k3", "v3") + .add("k4", "v4") + .add("k5", "v5") + .add("k6", "v6") + .add("dup", "dup1") + .add("dup", "dup2") + .build(); + assertThat(headers.equals(other3)).isTrue(); + + final HttpHeaderGetters other4 = HttpHeaders.builder() + .add("k1", "v1") + .add("k2", "v2") + .add("k3", "v3") + .add("k4", "v4") + .add("k5", "v5") + .add("k6", "v6") + .add("dup", "dup1") + .add("dup", "dup2"); + assertThat(headers.equals(other4)).isTrue(); + } + + @Test + void equals_true_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.equals(headers)).isTrue(); + + final CompositeStringMultimap other1 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "additional1"), + HttpHeaders.of("additional", "additional2"), + HttpHeaders.of("additional", "additional3"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of("default", "default2"), + HttpHeaders.of("default", "default3"), + HttpHeaders.of()); + assertThat(headers.equals(other1)).isTrue(); + + final List additionals2 = + ImmutableList.of(HttpHeaders.of("additional", "additional2", + "additional", "additional3"), + HttpHeaders.of("k1", "additional1", + "k3", "v3")); + final List parents2 = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k2", "v2"), + HttpHeaders.of("k4", "v4", + "k5", "v5"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults2 = + ImmutableList.of(HttpHeaders.of("k6", "v6", + "dup", "dup3", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap other2 = + new CompositeHttpHeadersBase(additionals2, parents2, defaults2); + assertThat(headers.equals(other2)).isTrue(); + } + + @Test + void equals_true_insertionOrder() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.equals(headers)).isTrue(); + + final CompositeStringMultimap other1 = + new CompositeHttpHeadersBase(HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("diff1", "diff1"), + HttpHeaders.of(), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("diff2", "diff2")); + other1.remove("diff1"); + other1.add("dup", "dup2"); + other1.remove("diff2"); + assertThat(headers.equals(other1)).isTrue(); + + final HttpHeaders other2 = HttpHeaders.builder() + .add("k4", "v4") + .add("k5", "v5") + .add("k6", "v6") + .add("k1", "v1") + .add("k2", "v2") + .add("k3", "v3") + .add("dup", "dup1") + .add("dup", "dup2") + .build(); + assertThat(headers.equals(other2)).isTrue(); + + final HttpHeaderGetters other3 = HttpHeaders.builder() + .add("dup", "dup1") + .add("dup", "dup2") + .add("k2", "v2") + .add("k1", "v1") + .add("k4", "v4") + .add("k3", "v3") + .add("k6", "v6") + .add("k5", "v5"); + assertThat(headers.equals(other3)).isTrue(); + } + + @Test + void equals_true_empty() { + final CompositeStringMultimap empty = + new CompositeHttpHeadersBase(HttpHeaders.of(), HttpHeaders.of()); + + assertThat(empty.equals(HttpHeaders.of())).isTrue(); + assertThat(empty.equals(HttpHeaders.builder().build())).isTrue(); + assertThat(empty.equals(HttpHeaders.builder())).isTrue(); + } + + @Test + void equals_false() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.equals(null)).isFalse(); + + final CompositeStringMultimap other1 = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of()); + assertThat(headers.equals(other1)).isFalse(); + + final HttpHeaders other2 = HttpHeaders.builder() + .add("k1", "v1") + .add("k2", "v2") + .add("k3", "v3") + .build(); + assertThat(headers.equals(other2)).isFalse(); + + final HttpHeaderGetters other3 = HttpHeaders.builder() + .add("k1", "v1") + .add("k2", "v2") + .add("k3", "v3"); + assertThat(headers.equals(other3)).isFalse(); + } + + @Test + void equals_false_insertionOrder() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final CompositeStringMultimap other1 = + new CompositeHttpHeadersBase(HttpHeaders.of("dup", "dup2"), + HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("diff1", "diff1"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("diff2", "diff2"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of()); + assertThat(headers.equals(other1)).isFalse(); + + other1.remove("dup"); + other1.remove("diff1"); + other1.remove("diff2"); + other1.add("dup", "dup1"); + other1.add("dup", "dup2"); + assertThat(headers.equals(other1)).isTrue(); + + final HttpHeaders other2 = HttpHeaders.builder() + .add("k1", "v1") + .add("k2", "v2") + .add("dup", "dup2") + .add("k3", "v3") + .add("k4", "v4") + .add("dup", "dup1") + .add("k5", "v5") + .add("k6", "v6") + .build(); + assertThat(headers.equals(other2)).isFalse(); + + final HttpHeaderGetters other3 = HttpHeaders.builder() + .add("k1", "v1") + .add("dup", "dup2") + .add("k2", "v2") + .add("k3", "v3") + .add("k4", "v4") + .add("k5", "v5") + .add("k6", "v6") + .add("dup", "dup1"); + assertThat(headers.equals(other3)).isFalse(); + } + + @Test + void equals_false_empty() { + final CompositeStringMultimap empty = + new CompositeHttpHeadersBase(HttpHeaders.of(), HttpHeaders.of()); + + assertThat(empty.equals(HttpHeaders.of("k1", "v1"))).isFalse(); + assertThat(empty.equals(HttpHeaders.builder().add("k1", "v1").build())).isFalse(); + assertThat(empty.equals(HttpHeaders.builder().add("k1", "v1"))).isFalse(); + } + + @Test + void testToString() { + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + assertThat(headers.toString()) + .isEqualTo("[k1=v1, k2=v2, k3=v3, k4=v4, k5=v5, k6=v6, dup=dup1, dup=dup2]"); + + headers.remove("k2"); + headers.remove("k4"); + headers.add("k7", "v7"); + headers.remove("k6"); + headers.remove("dup"); + assertThat(headers.toString()).isEqualTo("[k1=v1, k3=v3, k5=v5, k7=v7]"); + + headers.clear(); + assertThat(headers.toString()).isEqualTo("[]"); + assertThat(new CompositeHttpHeadersBase(HttpHeaders.of()).toString()).isEqualTo("[]"); + } + + @Test + void testToString_mergedWithAdditionalsAndDefaults() { + final List additionals = + ImmutableList.of(HttpHeaders.of("k1", "additional1", + "additional", "additional2", + "additional", "additional3")); + final List parents = + ImmutableList.of(HttpHeaders.of(), + HttpHeaders.of("k1", "v1"), + HttpHeaders.of("k2", "v2", + "k3", "v3"), + HttpHeaders.of("k4", "v4", + "k5", "v5", + "k6", "v6"), + HttpHeaders.of("dup", "dup1"), + HttpHeaders.of("dup", "dup2"), + HttpHeaders.of()); + final List defaults = + ImmutableList.of(HttpHeaders.of("k1", "default1", + "default", "default2", + "default", "default3")); + final CompositeStringMultimap headers = + new CompositeHttpHeadersBase(additionals, parents, defaults); + assertThat(headers.toString()) + .isEqualTo("[k1=additional1, additional=additional2, additional=additional3, " + + "k2=v2, k3=v3, k4=v4, k5=v5, k6=v6, dup=dup1, dup=dup2, " + + "default=default2, default=default3]"); + + headers.remove("additional"); + headers.remove("k2"); + headers.remove("k4"); + headers.add("k7", "v7"); + headers.remove("k6"); + headers.remove("dup"); + headers.remove("default"); + assertThat(headers.toString()).isEqualTo("[k1=additional1, k3=v3, k5=v5, k7=v7]"); + + headers.clear(); + assertThat(headers.toString()).isEqualTo("[]"); + } +}