diff --git a/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/AsyncContextMapThreadLocal.java b/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/AsyncContextMapThreadLocal.java index 061dc750ac..c5e42f981c 100644 --- a/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/AsyncContextMapThreadLocal.java +++ b/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/AsyncContextMapThreadLocal.java @@ -17,6 +17,7 @@ import io.servicetalk.context.api.ContextMap; import io.servicetalk.context.api.ContextMapHolder; +import io.servicetalk.context.api.ContextMaps; import static java.lang.ThreadLocal.withInitial; @@ -24,7 +25,7 @@ final class AsyncContextMapThreadLocal { static final ThreadLocal CONTEXT_THREAD_LOCAL = withInitial(AsyncContextMapThreadLocal::newContextMap); private static ContextMap newContextMap() { - return new CopyOnWriteContextMap(); + return ContextMaps.newCopyOnWriteMap(); } ContextMap get() { diff --git a/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/NoopAsyncContextProvider.java b/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/NoopAsyncContextProvider.java index 1ca1ab1528..6f5ed42ccc 100644 --- a/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/NoopAsyncContextProvider.java +++ b/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/NoopAsyncContextProvider.java @@ -18,10 +18,9 @@ import io.servicetalk.concurrent.CompletableSource; import io.servicetalk.concurrent.PublisherSource.Subscriber; import io.servicetalk.concurrent.SingleSource; -import io.servicetalk.concurrent.internal.ContextMapUtils; import io.servicetalk.context.api.ContextMap; +import io.servicetalk.context.api.ContextMaps; -import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; @@ -29,10 +28,8 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.function.BiPredicate; import java.util.function.Consumer; import java.util.function.Function; -import javax.annotation.Nullable; final class NoopAsyncContextProvider implements AsyncContextProvider { static final AsyncContextProvider INSTANCE = new NoopAsyncContextProvider(); @@ -43,7 +40,7 @@ private NoopAsyncContextProvider() { @Override public ContextMap context() { - return NoopContextMap.INSTANCE; + return ContextMaps.emptyMap(); } @Override @@ -154,121 +151,4 @@ public BiFunction wrapBiFunction(final BiFunction fu final ContextMap context) { return func; } - - private static final class NoopContextMap implements ContextMap { - static final ContextMap INSTANCE = new NoopContextMap(); - - private NoopContextMap() { - // Singleton - } - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return true; - } - - @Override - public boolean containsKey(final Key key) { - return false; - } - - @Override - public boolean containsValue(@Nullable final Object value) { - return false; - } - - @Override - public boolean contains(final Key key, @Nullable final T value) { - return false; - } - - @Nullable - @Override - public T get(final Key key) { - return null; - } - - @Override - public T getOrDefault(final Key key, final T defaultValue) { - return defaultValue; - } - - @Nullable - @Override - public T put(final Key key, @Nullable final T value) { - return null; - } - - @Nullable - @Override - public T putIfAbsent(final Key key, @Nullable final T value) { - return null; - } - - @Nullable - @Override - public T computeIfAbsent(final Key key, final Function, T> computeFunction) { - return null; - } - - @Override - public void putAll(final ContextMap map) { - } - - @Override - public void putAll(final Map, Object> map) { - } - - @Nullable - @Override - public T remove(final Key key) { - return null; - } - - @Override - public boolean removeAll(final Iterable> keys) { - return false; - } - - @Override - public void clear() { - } - - @Nullable - @Override - public Key forEach(final BiPredicate, Object> consumer) { - return null; - } - - @Override - public ContextMap copy() { - return this; - } - - @Override - public int hashCode() { - return System.identityHashCode(this); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (!(o instanceof ContextMap)) { - return false; - } - return ((ContextMap) o).isEmpty(); - } - - @Override - public String toString() { - return ContextMapUtils.toString(this); - } - } } diff --git a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/ContextMapUtils.java b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/ContextMapUtils.java index 643b6dde3c..6c9c9b18ad 100644 --- a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/ContextMapUtils.java +++ b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/ContextMapUtils.java @@ -26,8 +26,11 @@ /** * Shared utilities for {@link ContextMap}. + * + * @deprecated This class will be removed in the future releases. */ -public final class ContextMapUtils { +@Deprecated +public final class ContextMapUtils { // FIXME: 0.43 - remove deprecated class private ContextMapUtils() { // no instances } diff --git a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java index a9e5b8a879..f9ca6086a1 100644 --- a/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java +++ b/servicetalk-concurrent-internal/src/main/java/io/servicetalk/concurrent/internal/DefaultContextMap.java @@ -16,6 +16,7 @@ package io.servicetalk.concurrent.internal; import io.servicetalk.context.api.ContextMap; +import io.servicetalk.context.api.ContextMaps; import java.util.HashMap; import java.util.Map; @@ -29,8 +30,11 @@ * Default implementation of {@link ContextMap}. *

* Note: it's not thread-safe! + * + * @deprecated Use {@link ContextMaps#newDefaultMap()}. */ -public final class DefaultContextMap implements ContextMap { +@Deprecated +public final class DefaultContextMap implements ContextMap { // FIXME: 0.43 - remove deprecated class private final Map, Object> theMap; diff --git a/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/ConcurrentContextMap.java b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/ConcurrentContextMap.java similarity index 97% rename from servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/ConcurrentContextMap.java rename to servicetalk-context-api/src/main/java/io/servicetalk/context/api/ConcurrentContextMap.java index 334100966e..a9a993fe18 100644 --- a/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/ConcurrentContextMap.java +++ b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/ConcurrentContextMap.java @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.servicetalk.concurrent.api; - -import io.servicetalk.concurrent.internal.ContextMapUtils; -import io.servicetalk.context.api.ContextMap; +package io.servicetalk.context.api; import java.util.Map; import java.util.Map.Entry; diff --git a/servicetalk-context-api/src/main/java/io/servicetalk/context/api/ContextMapUtils.java b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/ContextMapUtils.java new file mode 100644 index 0000000000..0d54b7e34e --- /dev/null +++ b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/ContextMapUtils.java @@ -0,0 +1,98 @@ +/* + * Copyright © 2021 Apple Inc. and the ServiceTalk project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.servicetalk.context.api; + +import io.servicetalk.context.api.ContextMap.Key; + +import javax.annotation.Nullable; + +import static java.lang.Integer.toHexString; +import static java.lang.System.identityHashCode; +import static java.util.Objects.requireNonNull; + +/** + * Shared utilities for {@link ContextMap}. + */ +final class ContextMapUtils { + private ContextMapUtils() { + // no instances + } + + /** + * {@link Object#toString()} implementation for {@link ContextMap}. + * + * @param map {@link ContextMap} to convert + * @return {@link String} representation of the context map + */ + static String toString(final ContextMap map) { + final String simpleName = map.getClass().getSimpleName(); + final int size = map.size(); + if (size == 0) { + return simpleName + '@' + toHexString(identityHashCode(map)) + ":{}"; + } + // 12 is 1 character for '@' + 8 hash code integer in hex form + 1 character for ':' + 2 characters for "{}". + // Assume size of 90 for each key/value pair: 42 overhead characters for formatting + 16 characters for key + // name + 16 characters for key type + 16 characters for value. + StringBuilder sb = new StringBuilder(simpleName.length() + 12 + size * 90); + sb.append(simpleName) + .append('@') + // There are many copies of these maps around, the content maybe equal but a differentiating factor is + // the object reference. this may help folks understand why state is not visible across AsyncContext + // boundaries. + .append(toHexString(identityHashCode(map))) + .append(":{"); + + map.forEach((key, value) -> { + sb.append(key).append('=').append(value == map ? "(this Map)" : value).append(", "); + return true; + }); + sb.setLength(sb.length() - 2); + return sb.append('}').toString(); + } + + /** + * {@link java.util.Objects#equals(Object, Object)} alternative for {@link ContextMap}. + * + * @param first the first {@link ContextMap} + * @param second the second {@link ContextMap} to compare equality with the first one + * @return {@code true} if both {@link ContextMap}(s) are equal (contains the same elements), {@code false} + * otherwise. + */ + static boolean equals(final ContextMap first, final ContextMap second) { + if (first.size() != second.size()) { + return false; + } + @SuppressWarnings("unchecked") + final Key stopped = first.forEach((key, value) -> second.contains((Key) key, value)); + return stopped == null; + } + + /** + * Make sure that the {@code value} type matches with the {@link Key#type()}. + * + * @param key the {@link Key} to verify + * @param value the value to verify + * @throws NullPointerException if {@code key == null} + * @throws IllegalArgumentException if type of the {@code value} does not match with {@link Key#type()} + */ + static void ensureType(final Key key, @Nullable final Object value) { + requireNonNull(key); + if (value != null && !key.type().isInstance(value)) { + throw new IllegalArgumentException("Type of the value " + value + '(' + value.getClass() + ')' + + " does mot match with " + key); + } + } +} diff --git a/servicetalk-context-api/src/main/java/io/servicetalk/context/api/ContextMaps.java b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/ContextMaps.java new file mode 100644 index 0000000000..2b95b24f0f --- /dev/null +++ b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/ContextMaps.java @@ -0,0 +1,105 @@ +/* + * Copyright © 2024 Apple Inc. and the ServiceTalk project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.servicetalk.context.api; + +import javax.annotation.Nullable; + +import static io.servicetalk.context.api.ContextMap.Key; + +/** + * Utility class to create or operate with {@link ContextMap}s. + */ +public final class ContextMaps { + + private ContextMaps() { + // No instances. + } + + /** + * Returns an empty {@link ContextMap} (immutable). + *

+ * Any attempt to modify the returned {@link ContextMap} DOES NOT result in an + * {@link UnsupportedOperationException}, those are simply ignored. + * + * @return an empty {@link ContextMap}. + */ + public static ContextMap emptyMap() { + return EmptyContextMap.INSTANCE; + } + + /** + * Creates a new {@link ContextMap} backed by a {@link java.util.Map}. + *

+ * Note: it's not thread-safe! + * + * @return a new {@link ContextMap}. + */ + public static ContextMap newDefaultMap() { + return new DefaultContextMap(); + } + + /** + * Creates a new {@link ContextMap} backed by a {@link java.util.concurrent.ConcurrentMap}. + *

+ * Note: this implementation is thread-safe. + * + * @return a new {@link ContextMap}. + */ + public static ContextMap newConcurrentMap() { + return new ConcurrentContextMap(); + } + + /** + * Creates a new {@link ContextMap} that provides a Copy-on-Write map behavior. + *

+ * Note: this implementation is thread-safe. + * + * @return a new {@link ContextMap}. + */ + public static ContextMap newCopyOnWriteMap() { + return new CopyOnWriteContextMap(); + } + + /** + * Creates a new {@link ContextMap} that is immutable and holds only a single {@link Key}-value pair. + *

+ * Any attempt to modify the returned {@link ContextMap} result in an {@link UnsupportedOperationException}. + * + * @param The type of value associated with a {@link Key}. + * @param key the {@link ContextMap.Key} to be stored. + * @param value the value to be stored. + * @return a new {@link ContextMap}. + */ + public static ContextMap singletonMap(final ContextMap.Key key, final @Nullable T value) { + return new SingletonContextMap(key, value); + } + + /** + * Returns an unmodifiable view of the specified {@link ContextMap}. + *

+ * Query operations on the returned map "read through" to the specified map. Any attempt to modify the returned + * map result in an {@link UnsupportedOperationException}. + * + * @param map the {@link ContextMap} for which an unmodifiable view is to be returned. + * @return an unmodifiable view of the specified {@link ContextMap}. + */ + public static ContextMap unmodifiableMap(final ContextMap map) { + if (map instanceof UnmodifiableContextMap) { + return map; + } + return new UnmodifiableContextMap(map); + } +} diff --git a/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/CopyOnWriteContextMap.java b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/CopyOnWriteContextMap.java similarity index 99% rename from servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/CopyOnWriteContextMap.java rename to servicetalk-context-api/src/main/java/io/servicetalk/context/api/CopyOnWriteContextMap.java index 6788769fdc..31f9c1c3f0 100644 --- a/servicetalk-concurrent-api/src/main/java/io/servicetalk/concurrent/api/CopyOnWriteContextMap.java +++ b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/CopyOnWriteContextMap.java @@ -13,10 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.servicetalk.concurrent.api; - -import io.servicetalk.concurrent.internal.ContextMapUtils; -import io.servicetalk.context.api.ContextMap; +package io.servicetalk.context.api; import java.util.Arrays; import java.util.Collection; @@ -29,7 +26,6 @@ import java.util.function.Function; import javax.annotation.Nullable; -import static io.servicetalk.concurrent.internal.ContextMapUtils.ensureType; import static java.lang.System.arraycopy; import static java.util.Objects.requireNonNull; @@ -2923,7 +2919,7 @@ int addPair(final Key key, @Nullable final Object value) { @Override public void accept(final Key key, @Nullable final Object value) { - ensureType(key, value); + ContextMapUtils.ensureType(key, value); test(key, value); } diff --git a/servicetalk-context-api/src/main/java/io/servicetalk/context/api/DefaultContextMap.java b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/DefaultContextMap.java new file mode 100644 index 0000000000..62c4b044b0 --- /dev/null +++ b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/DefaultContextMap.java @@ -0,0 +1,165 @@ +/* + * Copyright © 2021-2022 Apple Inc. and the ServiceTalk project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.servicetalk.context.api; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.Function; +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +/** + * Default implementation of {@link ContextMap} backed by {@link HashMap}. + *

+ * Note: it's not thread-safe! + */ +final class DefaultContextMap implements ContextMap { + + private final Map, Object> theMap; + + DefaultContextMap() { + theMap = new HashMap<>(4); // start with a smaller table + } + + private DefaultContextMap(DefaultContextMap other) { + theMap = new HashMap<>(other.theMap); + } + + @Override + public int size() { + return theMap.size(); + } + + @Override + public boolean isEmpty() { + return theMap.isEmpty(); + } + + @Override + public boolean containsKey(final Key key) { + return theMap.containsKey(requireNonNull(key, "key")); + } + + @Override + public boolean containsValue(@Nullable final Object value) { + return theMap.containsValue(value); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T get(final Key key) { + return (T) theMap.get(requireNonNull(key, "key")); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T getOrDefault(final Key key, final T defaultValue) { + return (T) theMap.getOrDefault(requireNonNull(key, "key"), defaultValue); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T put(final Key key, @Nullable final T value) { + return (T) theMap.put(requireNonNull(key, "key"), value); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T putIfAbsent(final Key key, @Nullable final T value) { + return (T) theMap.putIfAbsent(requireNonNull(key, "key"), value); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T computeIfAbsent(final Key key, final Function, T> computeFunction) { + return (T) theMap.computeIfAbsent(requireNonNull(key, "key"), k -> computeFunction.apply((Key) k)); + } + + @Override + public void putAll(final ContextMap map) { + if (map instanceof DefaultContextMap) { + final DefaultContextMap dcm = (DefaultContextMap) map; + theMap.putAll(dcm.theMap); + } else { + ContextMap.super.putAll(map); + } + } + + @Override + public void putAll(final Map, Object> map) { + map.forEach(ContextMapUtils::ensureType); + theMap.putAll(map); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T remove(final Key key) { + return (T) theMap.remove(requireNonNull(key, "key")); + } + + @Override + public void clear() { + theMap.clear(); + } + + @Nullable + @Override + public Key forEach(final BiPredicate, Object> consumer) { + for (Map.Entry, Object> entry : theMap.entrySet()) { + if (!consumer.test(entry.getKey(), entry.getValue())) { + return entry.getKey(); + } + } + return null; + } + + @Override + public ContextMap copy() { + return new DefaultContextMap(this); + } + + @Override + public int hashCode() { + return theMap.hashCode(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ContextMap)) { + return false; + } + if (o instanceof DefaultContextMap) { + return theMap.equals(((DefaultContextMap) o).theMap); + } + return ContextMapUtils.equals(this, (ContextMap) o); + } + + @Override + public String toString() { + return ContextMapUtils.toString(this); + } +} diff --git a/servicetalk-context-api/src/main/java/io/servicetalk/context/api/EmptyContextMap.java b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/EmptyContextMap.java new file mode 100644 index 0000000000..3f7c40c748 --- /dev/null +++ b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/EmptyContextMap.java @@ -0,0 +1,138 @@ +/* + * Copyright © 2021 Apple Inc. and the ServiceTalk project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.servicetalk.context.api; + +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.Function; +import javax.annotation.Nullable; + +final class EmptyContextMap implements ContextMap { + static final ContextMap INSTANCE = new EmptyContextMap(); + + private EmptyContextMap() { + // Singleton + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean containsKey(final Key key) { + return false; + } + + @Override + public boolean containsValue(@Nullable final Object value) { + return false; + } + + @Override + public boolean contains(final Key key, @Nullable final T value) { + return false; + } + + @Nullable + @Override + public T get(final Key key) { + return null; + } + + @Override + public T getOrDefault(final Key key, final T defaultValue) { + return defaultValue; + } + + @Nullable + @Override + public T put(final Key key, @Nullable final T value) { + return null; + } + + @Nullable + @Override + public T putIfAbsent(final Key key, @Nullable final T value) { + return null; + } + + @Nullable + @Override + public T computeIfAbsent(final Key key, final Function, T> computeFunction) { + return null; + } + + @Override + public void putAll(final ContextMap map) { + } + + @Override + public void putAll(final Map, Object> map) { + } + + @Nullable + @Override + public T remove(final Key key) { + return null; + } + + @Override + public boolean removeAll(final Iterable> keys) { + return false; + } + + @Override + public void clear() { + } + + @Nullable + @Override + public Key forEach(final BiPredicate, Object> consumer) { + return null; + } + + @Override + public ContextMap copy() { + return this; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ContextMap)) { + return false; + } + return ((ContextMap) o).isEmpty(); + } + + @Override + public String toString() { + return ContextMapUtils.toString(this); + } +} diff --git a/servicetalk-context-api/src/main/java/io/servicetalk/context/api/SingletonContextMap.java b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/SingletonContextMap.java new file mode 100644 index 0000000000..ccb7848726 --- /dev/null +++ b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/SingletonContextMap.java @@ -0,0 +1,155 @@ +/* + * Copyright © 2024 Apple Inc. and the ServiceTalk project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.servicetalk.context.api; + +import java.util.Map; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Function; +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +final class SingletonContextMap implements ContextMap { + + private final Key key; + @Nullable + private final Object value; + + SingletonContextMap(final Key key, final @Nullable T value) { + this.key = requireNonNull(key); + this.value = value; + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean containsKey(final Key key) { + return this.key.equals(key); + } + + @Override + public boolean containsValue(@Nullable final Object value) { + return Objects.equals(this.value, value); + } + + @Override + public boolean contains(final Key key, @Nullable final T value) { + return containsKey(key) && containsValue(value); + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T get(final Key key) { + return this.key.equals(key) ? (T) value : null; + } + + @Nullable + @Override + @SuppressWarnings("unchecked") + public T getOrDefault(final Key key, final T defaultValue) { + return this.key.equals(key) ? (T) value : defaultValue; + } + + @Nullable + @Override + public T put(final Key key, @Nullable final T value) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T putIfAbsent(final Key key, @Nullable final T value) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T computeIfAbsent(final Key key, final Function, T> computeFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(final ContextMap map) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(final Map, Object> map) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T remove(final Key key) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(final Iterable> keys) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public Key forEach(final BiPredicate, Object> consumer) { + consumer.test(key, value); + return null; + } + + @Override + public ContextMap copy() { + return this; + } + + @Override + public int hashCode() { + int result = key.hashCode(); + result = 31 * result + Objects.hashCode(value); + return result; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SingletonContextMap that = (SingletonContextMap) o; + return key.equals(that.key) && Objects.equals(value, that.value); + } + + @Override + public String toString() { + return ContextMapUtils.toString(this); + } +} diff --git a/servicetalk-context-api/src/main/java/io/servicetalk/context/api/UnmodifiableContextMap.java b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/UnmodifiableContextMap.java new file mode 100644 index 0000000000..bcf2551ee2 --- /dev/null +++ b/servicetalk-context-api/src/main/java/io/servicetalk/context/api/UnmodifiableContextMap.java @@ -0,0 +1,136 @@ +/* + * Copyright © 2024 Apple Inc. and the ServiceTalk project authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.servicetalk.context.api; + +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.Function; +import javax.annotation.Nullable; + +import static java.util.Objects.requireNonNull; + +final class UnmodifiableContextMap implements ContextMap { + + private final ContextMap map; + + UnmodifiableContextMap(final ContextMap map) { + this.map = requireNonNull(map); + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(final Key key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(@Nullable final Object value) { + return map.containsValue(value); + } + + @Override + public boolean contains(final Key key, @Nullable final T value) { + return map.contains(key, value); + } + + @Nullable + @Override + public T get(final Key key) { + return map.get(key); + } + + @Nullable + @Override + public T getOrDefault(final Key key, final T defaultValue) { + return map.getOrDefault(key, defaultValue); + } + + @Nullable + @Override + public T put(final Key key, @Nullable final T value) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T putIfAbsent(final Key key, @Nullable final T value) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T computeIfAbsent(final Key key, final Function, T> computeFunction) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(final ContextMap map) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(final Map, Object> map) { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public T remove(final Key key) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(final Iterable> keys) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Nullable + @Override + public Key forEach(final BiPredicate, Object> consumer) { + return map.forEach(consumer); + } + + @Override + public ContextMap copy() { + return new UnmodifiableContextMap(map.copy()); + } + + public boolean equals(Object o) { + return o == this || map.equals(o); + } + + public int hashCode() { + return map.hashCode(); + } + + public String toString() { + return map.toString(); + } +} diff --git a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcMetadata.java b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcMetadata.java index 4dc790e211..11b28ae3e6 100644 --- a/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcMetadata.java +++ b/servicetalk-grpc-api/src/main/java/io/servicetalk/grpc/api/DefaultGrpcMetadata.java @@ -15,8 +15,8 @@ */ package io.servicetalk.grpc.api; -import io.servicetalk.concurrent.internal.DefaultContextMap; import io.servicetalk.context.api.ContextMap; +import io.servicetalk.context.api.ContextMaps; import java.util.function.Supplier; import javax.annotation.Nullable; @@ -81,7 +81,7 @@ static final class LazyContextMapSupplier implements Supplier { @Override public ContextMap get() { if (context == null) { - context = new DefaultContextMap(); + context = ContextMaps.newDefaultMap(); } return context; } diff --git a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/customtransport/Utils.java b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/customtransport/Utils.java index d85c25a3c3..7d727ed838 100644 --- a/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/customtransport/Utils.java +++ b/servicetalk-grpc-netty/src/test/java/io/servicetalk/grpc/customtransport/Utils.java @@ -23,8 +23,8 @@ import io.servicetalk.concurrent.api.ListenableAsyncCloseable; import io.servicetalk.concurrent.api.Publisher; import io.servicetalk.concurrent.api.internal.SubscribableCompletable; -import io.servicetalk.concurrent.internal.DefaultContextMap; import io.servicetalk.context.api.ContextMap; +import io.servicetalk.context.api.ContextMaps; import io.servicetalk.encoding.api.ContentCodec; import io.servicetalk.grpc.api.GrpcExecutionContext; import io.servicetalk.grpc.api.GrpcExecutionStrategy; @@ -104,8 +104,8 @@ public GrpcExecutionStrategy executionStrategy() { static final class ChannelGrpcServiceContext implements GrpcServiceContext { private final ListenableAsyncCloseable closeAsync; private final GrpcExecutionContext ctx; - private final ContextMap requestContext = new DefaultContextMap(); - private final ContextMap responseContext = new DefaultContextMap(); + private final ContextMap requestContext = ContextMaps.newDefaultMap(); + private final ContextMap responseContext = ContextMaps.newDefaultMap(); ChannelGrpcServiceContext(Channel channel, GrpcExecutionContext ctx) { closeAsync = toListenableAsyncCloseable(new AsyncCloseable() { diff --git a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java index adb1668ec7..d0fde30610 100644 --- a/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java +++ b/servicetalk-http-api/src/main/java/io/servicetalk/http/api/AbstractHttpMetaData.java @@ -15,8 +15,8 @@ */ package io.servicetalk.http.api; -import io.servicetalk.concurrent.internal.DefaultContextMap; import io.servicetalk.context.api.ContextMap; +import io.servicetalk.context.api.ContextMaps; import io.servicetalk.encoding.api.ContentCodec; import javax.annotation.Nonnull; @@ -84,7 +84,7 @@ final ContextMap context0() { @Override public final ContextMap context() { if (context == null) { - context = new DefaultContextMap(); + context = ContextMaps.newDefaultMap(); } return context; } diff --git a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectConnectionFactoryFilter.java b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectConnectionFactoryFilter.java index b7d463f8b4..29844fcdd8 100644 --- a/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectConnectionFactoryFilter.java +++ b/servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/ProxyConnectConnectionFactoryFilter.java @@ -19,8 +19,8 @@ import io.servicetalk.client.api.ConnectionFactoryFilter; import io.servicetalk.client.api.DelegatingConnectionFactory; import io.servicetalk.concurrent.api.Single; -import io.servicetalk.concurrent.internal.DefaultContextMap; import io.servicetalk.context.api.ContextMap; +import io.servicetalk.context.api.ContextMaps; import io.servicetalk.http.api.FilterableStreamingHttpConnection; import io.servicetalk.http.api.HttpContextKeys; import io.servicetalk.http.api.HttpExecutionStrategies; @@ -76,7 +76,7 @@ public Single newConnection(final ResolvedAddress resolvedAddress, @Nullable ContextMap context, @Nullable final TransportObserver observer) { return Single.defer(() -> { - final ContextMap contextMap = context != null ? context : new DefaultContextMap(); + final ContextMap contextMap = context != null ? context : ContextMaps.newDefaultMap(); logUnexpectedAddress(contextMap.put(HTTP_TARGET_ADDRESS_BEHIND_PROXY, connectAddress), connectAddress, LOGGER); // The rest of the logic was moved to ProxyConnectLBHttpConnectionFactory diff --git a/servicetalk-loadbalancer-experimental/src/main/java/io/servicetalk/loadbalancer/DefaultHost.java b/servicetalk-loadbalancer-experimental/src/main/java/io/servicetalk/loadbalancer/DefaultHost.java index e10a81be09..8047df1765 100644 --- a/servicetalk-loadbalancer-experimental/src/main/java/io/servicetalk/loadbalancer/DefaultHost.java +++ b/servicetalk-loadbalancer-experimental/src/main/java/io/servicetalk/loadbalancer/DefaultHost.java @@ -24,9 +24,9 @@ import io.servicetalk.concurrent.api.ListenableAsyncCloseable; import io.servicetalk.concurrent.api.Single; import io.servicetalk.concurrent.api.TerminalSignalConsumer; -import io.servicetalk.concurrent.internal.DefaultContextMap; import io.servicetalk.concurrent.internal.DelayedCancellable; import io.servicetalk.context.api.ContextMap; +import io.servicetalk.context.api.ContextMaps; import io.servicetalk.loadbalancer.LoadBalancerObserver.HostObserver; import io.servicetalk.transport.api.TransportObserver; @@ -186,7 +186,7 @@ public Single newConnection( return Single.defer(() -> { ContextMap actualContext = context; if (actualContext == null) { - actualContext = new DefaultContextMap(); + actualContext = ContextMaps.newDefaultMap(); } // We need to put our address latency tracker in the context for consumption.