From 79f1f532d0b23783259ff905bdd9960e883ced94 Mon Sep 17 00:00:00 2001 From: jansupol Date: Thu, 24 Nov 2022 22:45:36 +0100 Subject: [PATCH] Add caching for speeding up requests handling Signed-off-by: jansupol --- .../GuardianStringKeyMultivaluedMap.java | 208 ++++++++++ .../jersey/internal/util/collection/LRU.java | 76 ++++ .../jersey/message/internal/HeaderUtils.java | 25 +- .../message/internal/HttpHeaderReader.java | 356 ++++++++++++------ .../internal/InboundMessageContext.java | 85 +++-- .../message/internal/MediaTypeProvider.java | 50 ++- .../internal/OutboundMessageContext.java | 52 ++- .../jersey/server/RequestContextBuilder.java | 7 +- .../message/internal/HeaderUtilsTest.java | 17 +- .../header/HeaderDelegateProviderTest.java | 4 +- .../jersey/tests/api/ResponseTest.java | 3 +- 11 files changed, 691 insertions(+), 192 deletions(-) create mode 100644 core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java create mode 100644 core-common/src/main/java/org/glassfish/jersey/internal/util/collection/LRU.java diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java new file mode 100644 index 0000000000..b2605fe49b --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/GuardianStringKeyMultivaluedMap.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal.util.collection; + +import javax.ws.rs.core.MultivaluedMap; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * The {@link MultivaluedMap} wrapper that is able to set guards observing changes of values represented by a key. + * @param The value type of the wrapped {@code MultivaluedMap}. + * + * @since 2.38 + */ +public class GuardianStringKeyMultivaluedMap implements MultivaluedMap { + + private final MultivaluedMap inner; + private final Map guards = new HashMap<>(); + + public GuardianStringKeyMultivaluedMap(MultivaluedMap inner) { + this.inner = inner; + } + + @Override + public void putSingle(String key, V value) { + observe(key); + inner.putSingle(key, value); + } + + @Override + public void add(String key, V value) { + observe(key); + inner.add(key, value); + } + + @Override + public V getFirst(String key) { + return inner.getFirst(key); + } + + @Override + public void addAll(String key, V... newValues) { + observe(key); + inner.addAll(key, newValues); + } + + @Override + public void addAll(String key, List valueList) { + observe(key); + inner.addAll(key, valueList); + } + + @Override + public void addFirst(String key, V value) { + observe(key); + inner.addFirst(key, value); + } + + @Override + public boolean equalsIgnoreValueOrder(MultivaluedMap otherMap) { + return inner.equalsIgnoreValueOrder(otherMap); + } + + @Override + public int size() { + return inner.size(); + } + + @Override + public boolean isEmpty() { + return inner.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return inner.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return inner.containsValue(value); + } + + @Override + public List get(Object key) { + return inner.get(key); + } + + @Override + public List put(String key, List value) { + observe(key); + return inner.put(key, value); + } + + @Override + public List remove(Object key) { + if (key != null) { + observe(key.toString()); + } + return inner.remove(key); + } + + @Override + public void putAll(Map> m) { + for (String key : m.keySet()) { + observe(key); + } + inner.putAll(m); + } + + @Override + public void clear() { + observeAll(); + inner.clear(); + } + + @Override + public Set keySet() { + return inner.keySet(); + } + + @Override + public Collection> values() { + return inner.values(); + } + + @Override + public Set>> entrySet() { + return inner.entrySet(); + } + + /** + * Observe changes of a value represented by the key. + * @param key the key values to observe + */ + public void setGuard(String key) { + guards.put(key, false); + } + + /** + * Get all the guarded keys + * @return a {@link Set} of keys guarded. + */ + public Set getGuards() { + return guards.keySet(); + } + + /** + * Return true when the value represented by the key has changed. Resets any observation - the operation is not idempotent. + * @param key the Key observed. + * @return whether the value represented by the key has changed. + */ + public boolean isObservedAndReset(String key) { + Boolean observed = guards.get(key); + guards.put(key, false); + return observed != null && observed; + } + + private void observe(String key) { + for (Map.Entry guard : guards.entrySet()) { + if (guard.getKey().equals(key)) { + guard.setValue(true); + } + } + } + + private void observeAll() { + for (Map.Entry guard : guards.entrySet()) { + guard.setValue(true); + } + } + + @Override + public String toString() { + return inner.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GuardianStringKeyMultivaluedMap that = (GuardianStringKeyMultivaluedMap) o; + return inner.equals(that.inner) && guards.equals(that.guards); + } + + @Override + public int hashCode() { + return Objects.hash(inner, guards); + } +} diff --git a/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/LRU.java b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/LRU.java new file mode 100644 index 0000000000..3338b00fdb --- /dev/null +++ b/core-common/src/main/java/org/glassfish/jersey/internal/util/collection/LRU.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package org.glassfish.jersey.internal.util.collection; + +import org.glassfish.jersey.internal.guava.Cache; +import org.glassfish.jersey.internal.guava.CacheBuilder; + +import java.util.concurrent.TimeUnit; + +/** + * An abstract LRU interface wrapping an actual LRU implementation. + * @param Key type + * @param Value type + * @Since 2.38 + */ +public abstract class LRU { + + /** + * Returns the value associated with {@code key} in this cache, or {@code null} if there is no + * cached value for {@code key}. + */ + public abstract V getIfPresent(Object key); + + /** + * Associates {@code value} with {@code key} in this cache. If the cache previously contained a + * value associated with {@code key}, the old value is replaced by {@code value}. + */ + public abstract void put(K key, V value); + + /** + * Create new LRU + * @return new LRU + */ + public static LRU create() { + return LRUFactory.createLRU(); + } + + private static class LRUFactory { + // TODO configure via the Configuration + public static final int LRU_CACHE_SIZE = 128; + public static final long TIMEOUT = 5000L; + private static LRU createLRU() { + final Cache CACHE = CacheBuilder.newBuilder() + .maximumSize(LRU_CACHE_SIZE) + .expireAfterAccess(TIMEOUT, TimeUnit.MILLISECONDS) + .build(); + return new LRU() { + @Override + public V getIfPresent(Object key) { + return CACHE.getIfPresent(key); + } + + @Override + public void put(K key, V value) { + CACHE.put(key, value); + } + }; + } + } + + +} diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java index 3c6cfdd9ce..5b47d6b111 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/HeaderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -102,7 +102,7 @@ public static AbstractMultivaluedMap createOutbound() { * if the supplied header value is {@code null}. */ @SuppressWarnings("unchecked") - private static String asString(final Object headerValue, RuntimeDelegate rd) { + public static String asString(final Object headerValue, RuntimeDelegate rd) { if (headerValue == null) { return null; } @@ -149,7 +149,7 @@ public static String asString(final Object headerValue, Configuration configurat * will be called for before element conversion. * @return String view of header values. */ - private static List asStringList(final List headerValues, final RuntimeDelegate rd) { + public static List asStringList(final List headerValues, final RuntimeDelegate rd) { if (headerValues == null || headerValues.isEmpty()) { return Collections.emptyList(); } @@ -191,7 +191,24 @@ public static MultivaluedMap asStringHeaders( return null; } - final RuntimeDelegate rd = RuntimeDelegateDecorator.configured(configuration); + return asStringHeaders(headers, RuntimeDelegateDecorator.configured(configuration)); + } + + /** + * Returns string view of passed headers. Any modifications to the headers are visible to the view, the view also + * supports removal of elements. Does not support other modifications. + * + * @param headers headers. + * @param rd {@link RuntimeDelegate} instance or {@code null} (in that case {@link RuntimeDelegate#getInstance()} + * will be called for before conversion of elements). + * @return String view of headers or {@code null} if {code headers} input parameter is {@code null}. + */ + public static MultivaluedMap asStringHeaders( + final MultivaluedMap headers, RuntimeDelegate rd) { + if (headers == null) { + return null; + } + return new AbstractMultivaluedMap( Views.mapView(headers, input -> HeaderUtils.asStringList(input, rd)) ) { diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpHeaderReader.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpHeaderReader.java index dacd743085..a2586e026c 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpHeaderReader.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/HttpHeaderReader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -23,6 +23,7 @@ import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; @@ -31,6 +32,7 @@ import javax.ws.rs.core.Cookie; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.NewCookie; +import org.glassfish.jersey.internal.util.collection.LRU; /** * An abstract pull-based reader of HTTP headers. @@ -371,61 +373,25 @@ public static Set readMatchingEntityTag(String header) throws return l; } - private static final ListElementCreator MEDIA_TYPE_CREATOR = - new ListElementCreator() { - - @Override - public MediaType create(HttpHeaderReader reader) throws ParseException { - return MediaTypeProvider.valueOf(reader); - } - }; - /** * TODO javadoc. */ public static List readMediaTypes(List l, String header) throws ParseException { - return HttpHeaderReader.readList( - l, - MEDIA_TYPE_CREATOR, - header); + return MEDIA_TYPE_LIST_READER.readList(l, header); } - private static final ListElementCreator ACCEPTABLE_MEDIA_TYPE_CREATOR = - new ListElementCreator() { - - @Override - public AcceptableMediaType create(HttpHeaderReader reader) throws ParseException { - return AcceptableMediaType.valueOf(reader); - } - }; - /** * TODO javadoc. */ public static List readAcceptMediaType(String header) throws ParseException { - return HttpHeaderReader.readQualifiedList( - AcceptableMediaType.COMPARATOR, - ACCEPTABLE_MEDIA_TYPE_CREATOR, - header); + return ACCEPTABLE_MEDIA_TYPE_LIST_READER.readList(header); } - private static final ListElementCreator QUALITY_SOURCE_MEDIA_TYPE_CREATOR = - new ListElementCreator() { - - @Override - public QualitySourceMediaType create(HttpHeaderReader reader) throws ParseException { - return QualitySourceMediaType.valueOf(reader); - } - }; - /** * FIXME use somewhere in production code or remove. */ public static List readQualitySourceMediaType(String header) throws ParseException { - return HttpHeaderReader.readQualifiedList( - QualitySourceMediaType.COMPARATOR, - QUALITY_SOURCE_MEDIA_TYPE_CREATOR, - header); + return QUALITY_SOURCE_MEDIA_TYPE_LIST_READER.readList(header); } /** @@ -454,121 +420,267 @@ public static List readQualitySourceMediaType(String[] h public static List readAcceptMediaType( final String header, final List priorityMediaTypes) throws ParseException { - return HttpHeaderReader.readQualifiedList( - new Comparator() { - - @Override - public int compare(AcceptableMediaType o1, AcceptableMediaType o2) { - // FIXME what is going on here? - boolean q_o1_set = false; - int q_o1 = 0; - boolean q_o2_set = false; - int q_o2 = 0; - for (QualitySourceMediaType priorityType : priorityMediaTypes) { - if (!q_o1_set && MediaTypes.typeEqual(o1, priorityType)) { - q_o1 = o1.getQuality() * priorityType.getQuality(); - q_o1_set = true; - } else if (!q_o2_set && MediaTypes.typeEqual(o2, priorityType)) { - q_o2 = o2.getQuality() * priorityType.getQuality(); - q_o2_set = true; - } - } - int i = q_o2 - q_o1; - if (i != 0) { - return i; - } - - i = o2.getQuality() - o1.getQuality(); - if (i != 0) { - return i; - } - - return MediaTypes.PARTIAL_ORDER_COMPARATOR.compare(o1, o2); - } - }, - ACCEPTABLE_MEDIA_TYPE_CREATOR, - header); + return new AcceptMediaTypeListReader(priorityMediaTypes).readList(header); } - private static final ListElementCreator ACCEPTABLE_TOKEN_CREATOR = - new ListElementCreator() { - - @Override - public AcceptableToken create(HttpHeaderReader reader) throws ParseException { - return new AcceptableToken(reader); - } - }; /** * TODO javadoc. */ public static List readAcceptToken(String header) throws ParseException { - return HttpHeaderReader.readQualifiedList(ACCEPTABLE_TOKEN_CREATOR, header); + return ACCEPTABLE_TOKEN_LIST_READER.readList(header); } - private static final ListElementCreator LANGUAGE_CREATOR = - new ListElementCreator() { - - @Override - public AcceptableLanguageTag create(HttpHeaderReader reader) throws ParseException { - return new AcceptableLanguageTag(reader); - } - }; - /** * TODO javadoc. */ public static List readAcceptLanguage(String header) throws ParseException { - return HttpHeaderReader.readQualifiedList(LANGUAGE_CREATOR, header); + return ACCEPTABLE_LANGUAGE_TAG_LIST_READER.readList(header); + } + + /** + * TODO javadoc. + */ + public static List readStringList(String header) throws ParseException { + return STRING_LIST_READER.readList(header); } - private static List readQualifiedList(ListElementCreator c, String header) - throws ParseException { + private static final MediaTypeListReader MEDIA_TYPE_LIST_READER = new MediaTypeListReader(); + private static final AcceptableMediaTypeListReader ACCEPTABLE_MEDIA_TYPE_LIST_READER = new AcceptableMediaTypeListReader(); + private static final QualitySourceMediaTypeListReader QUALITY_SOURCE_MEDIA_TYPE_LIST_READER = + new QualitySourceMediaTypeListReader(); + private static final AcceptableTokenListReader ACCEPTABLE_TOKEN_LIST_READER = new AcceptableTokenListReader(); + private static final AcceptableLanguageTagListReader ACCEPTABLE_LANGUAGE_TAG_LIST_READER = + new AcceptableLanguageTagListReader(); + private static final StringListReader STRING_LIST_READER = new StringListReader(); - List l = readList(c, header); - Collections.sort(l, Quality.QUALIFIED_COMPARATOR); - return l; + private static class MediaTypeListReader extends ListReader { + private static final ListElementCreator MEDIA_TYPE_CREATOR = + new ListElementCreator() { + + @Override + public MediaType create(HttpHeaderReader reader) throws ParseException { + return MediaTypeProvider.valueOf(reader); + } + }; + + List readList(List l, final String header) throws ParseException { + return super.readList(l, MEDIA_TYPE_CREATOR, header); + } + + @Override + List readList(String header) throws ParseException { + return readList(MEDIA_TYPE_CREATOR, header); + } } - private static List readQualifiedList(final Comparator comparator, ListElementCreator c, String header) - throws ParseException { + private static class AcceptableMediaTypeListReader extends QualifiedListReader { + private static final ListElementCreator ACCEPTABLE_MEDIA_TYPE_CREATOR = + new ListElementCreator() { - List l = readList(c, header); - Collections.sort(l, comparator); - return l; + @Override + public AcceptableMediaType create(HttpHeaderReader reader) throws ParseException { + return AcceptableMediaType.valueOf(reader); + } + }; + + @Override + List readList(String header) throws ParseException { + return readList(AcceptableMediaType.COMPARATOR, header); + } + + @Override + List readList(Comparator comparator, String header) throws ParseException { + return readList(comparator, ACCEPTABLE_MEDIA_TYPE_CREATOR, header); + } } + /* + * TODO not used in production? + */ + private static class QualitySourceMediaTypeListReader extends QualifiedListReader { + private static final ListElementCreator QUALITY_SOURCE_MEDIA_TYPE_CREATOR = + new ListElementCreator() { - /** - * TODO javadoc. + @Override + public QualitySourceMediaType create(HttpHeaderReader reader) throws ParseException { + return QualitySourceMediaType.valueOf(reader); + } + }; + + @Override + List readList(String header) throws ParseException { + return readList(QualitySourceMediaType.COMPARATOR, header); + } + + @Override + List readList(Comparator comparator, String header) + throws ParseException { + return readList(comparator, QUALITY_SOURCE_MEDIA_TYPE_CREATOR, header); + } + } + + /* + * TODO this is used in tests only */ - public static List readStringList(String header) throws ParseException { - return readList(new ListElementCreator() { + private static class AcceptMediaTypeListReader extends QualifiedListReader { + private final List priorityMediaTypes; + + AcceptMediaTypeListReader(List priorityMediaTypes) { + this.priorityMediaTypes = priorityMediaTypes; + } + + private static final ListElementCreator ACCEPTABLE_MEDIA_TYPE_CREATOR = + new ListElementCreator() { + + @Override + public AcceptableMediaType create(HttpHeaderReader reader) throws ParseException { + return AcceptableMediaType.valueOf(reader); + } + }; + + private final Comparator acceptableMediaTypeComparator = + new Comparator() { + @Override + public int compare(AcceptableMediaType o1, AcceptableMediaType o2) { + // FIXME what is going on here? + boolean q_o1_set = false; + int q_o1 = 0; + boolean q_o2_set = false; + int q_o2 = 0; + for (QualitySourceMediaType priorityType : priorityMediaTypes) { + if (!q_o1_set && MediaTypes.typeEqual(o1, priorityType)) { + q_o1 = o1.getQuality() * priorityType.getQuality(); + q_o1_set = true; + } else if (!q_o2_set && MediaTypes.typeEqual(o2, priorityType)) { + q_o2 = o2.getQuality() * priorityType.getQuality(); + q_o2_set = true; + } + } + int i = q_o2 - q_o1; + if (i != 0) { + return i; + } + + i = o2.getQuality() - o1.getQuality(); + if (i != 0) { + return i; + } + + return MediaTypes.PARTIAL_ORDER_COMPARATOR.compare(o1, o2); + } + }; + + @Override + List readList(String header) throws ParseException { + return readList(acceptableMediaTypeComparator, header); + } + + @Override + List readList(Comparator comparator, String header) throws ParseException { + return readList(comparator, ACCEPTABLE_MEDIA_TYPE_CREATOR, header); + } + } + + private static class AcceptableTokenListReader extends QualifiedListReader { + private static final ListElementCreator ACCEPTABLE_TOKEN_CREATOR = + new ListElementCreator() { + + @Override + public AcceptableToken create(HttpHeaderReader reader) throws ParseException { + return new AcceptableToken(reader); + } + }; + + @Override + List readList(final Comparator comparator, String header) throws ParseException { + return readList(comparator, ACCEPTABLE_TOKEN_CREATOR, header); + } + } + + private static class AcceptableLanguageTagListReader extends QualifiedListReader { + private static final ListElementCreator LANGUAGE_CREATOR = + new ListElementCreator() { + + @Override + public AcceptableLanguageTag create(HttpHeaderReader reader) throws ParseException { + return new AcceptableLanguageTag(reader); + } + }; + + @Override + List readList(final Comparator comparator, String header) + throws ParseException { + return readList(comparator, LANGUAGE_CREATOR, header); + } + } + + private abstract static class QualifiedListReader extends ListReader { + + abstract List readList(final Comparator comparator, String header) throws ParseException; + + @Override + List readList(String header) throws ParseException { + return readList((Comparator) Quality.QUALIFIED_COMPARATOR, header); + } + + List readList(final Comparator comparator, ListElementCreator c, String header) throws ParseException { + List l = readList(c, header); + Collections.sort(l, comparator); + return l; + } + } + + private static class StringListReader extends ListReader { + private static final ListElementCreator listElementCreator = new ListElementCreator() { @Override public String create(HttpHeaderReader reader) throws ParseException { reader.hasNext(); return reader.nextToken().toString(); } - }, header); - } + }; - private static List readList(final ListElementCreator c, final String header) throws ParseException { - return readList(new ArrayList(), c, header); + @Override + List readList(String header) throws ParseException { + return readList(listElementCreator, header); + } } - private static List readList(final List l, final ListElementCreator c, final String header) - throws ParseException { + private abstract static class ListReader { + private final LRU> LIST_CACHE = LRU.create(); - HttpHeaderReader reader = new HttpHeaderReaderImpl(header); - HttpHeaderListAdapter adapter = new HttpHeaderListAdapter(reader); + abstract List readList(final String header) throws ParseException; - while (reader.hasNext()) { - l.add(c.create(adapter)); - adapter.reset(); - if (reader.hasNext()) { - reader.next(); - } + protected List readList(final ListElementCreator c, final String header) throws ParseException { + return readList(new ArrayList(), c, header); } - return l; + private List readList(final List l, final ListElementCreator c, final String header) + throws ParseException { + + List list = LIST_CACHE.getIfPresent(header); + + if (list == null) { + synchronized (LIST_CACHE) { + list = LIST_CACHE.getIfPresent(header); + if (list == null) { + HttpHeaderReader reader = new HttpHeaderReaderImpl(header); + HttpHeaderListAdapter adapter = new HttpHeaderListAdapter(reader); + list = new LinkedList<>(); + + while (reader.hasNext()) { + list.add(c.create(adapter)); + adapter.reset(); + if (reader.hasNext()) { + reader.next(); + } + } + LIST_CACHE.put(header, list); + } + } + } + + l.addAll(list); + return l; + } } } diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java index 81b9f76cae..15a00cb261 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/InboundMessageContext.java @@ -50,11 +50,16 @@ import javax.ws.rs.core.NewCookie; import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.RuntimeDelegate; import javax.xml.transform.Source; import org.glassfish.jersey.internal.LocalizationMessages; import org.glassfish.jersey.internal.PropertiesDelegate; import org.glassfish.jersey.internal.RuntimeDelegateDecorator; +import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap; +import org.glassfish.jersey.internal.util.collection.LazyValue; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; import org.glassfish.jersey.message.MessageBodyWorkers; /** @@ -90,11 +95,14 @@ public boolean markSupported() { private static final List WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST = Collections.singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE); - private final MultivaluedMap headers; + private final GuardianStringKeyMultivaluedMap headers; private final EntityContent entityContent; private final boolean translateNce; private MessageBodyWorkers workers; private final Configuration configuration; + private final RuntimeDelegate runtimeDelegateDecorator; + private LazyValue contentTypeCache; + private LazyValue> acceptTypeCache; /** * Input stream and its state. State is represented by the {@link Type Type enum} and @@ -158,10 +166,16 @@ public InboundMessageContext(Configuration configuration) { * as required by JAX-RS specification on the server side. */ public InboundMessageContext(Configuration configuration, boolean translateNce) { - this.headers = HeaderUtils.createInbound(); + this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createInbound()); this.entityContent = new EntityContent(); this.translateNce = translateNce; this.configuration = configuration; + runtimeDelegateDecorator = RuntimeDelegateDecorator.configured(configuration); + + contentTypeCache = contentTypeCache(); + acceptTypeCache = acceptTypeCache(); + headers.setGuard(HttpHeaders.CONTENT_TYPE); + headers.setGuard(HttpHeaders.ACCEPT); } /** @@ -196,7 +210,7 @@ public InboundMessageContext(boolean translateNce) { * @return updated context. */ public InboundMessageContext header(String name, Object value) { - getHeaders().add(name, HeaderUtils.asString(value, configuration)); + getHeaders().add(name, HeaderUtils.asString(value, runtimeDelegateDecorator)); return this; } @@ -208,7 +222,7 @@ public InboundMessageContext header(String name, Object value) { * @return updated context. */ public InboundMessageContext headers(String name, Object... values) { - this.getHeaders().addAll(name, HeaderUtils.asStringList(Arrays.asList(values), configuration)); + this.getHeaders().addAll(name, HeaderUtils.asStringList(Arrays.asList(values), runtimeDelegateDecorator)); return this; } @@ -265,7 +279,7 @@ private List iterableToList(final Iterable values) { final LinkedList linkedList = new LinkedList(); for (Object element : values) { - linkedList.add(HeaderUtils.asString(element, configuration)); + linkedList.add(HeaderUtils.asString(element, runtimeDelegateDecorator)); } return linkedList; @@ -332,7 +346,7 @@ private T singleHeader(String name, Function converter, boolean c } try { - return converter.apply(HeaderUtils.asString(value, configuration)); + return converter.apply(HeaderUtils.asString(value, runtimeDelegateDecorator)); } catch (ProcessingException ex) { throw exception(name, value, ex); } @@ -447,18 +461,26 @@ public Integer apply(String input) { * message entity). */ public MediaType getMediaType() { - return singleHeader(HttpHeaders.CONTENT_TYPE, new Function() { - @Override - public MediaType apply(String input) { - try { - return RuntimeDelegateDecorator.configured(configuration) - .createHeaderDelegate(MediaType.class) - .fromString(input); - } catch (IllegalArgumentException iae) { - throw new ProcessingException(iae); - } - } - }, false); + if (headers.isObservedAndReset(HttpHeaders.CONTENT_TYPE) && contentTypeCache.isInitialized()) { + contentTypeCache = contentTypeCache(); // headers changed -> drop cache + } + return contentTypeCache.get(); + } + + private LazyValue contentTypeCache() { + return Values.lazy((Value) () -> singleHeader( + HttpHeaders.CONTENT_TYPE, new Function() { + @Override + public MediaType apply(String input) { + try { + return runtimeDelegateDecorator + .createHeaderDelegate(MediaType.class) + .fromString(input); + } catch (IllegalArgumentException iae) { + throw new ProcessingException(iae); + } + } + }, false)); } /** @@ -468,17 +490,26 @@ public MediaType apply(String input) { * to their q-value, with highest preference first. */ public List getQualifiedAcceptableMediaTypes() { - final String value = getHeaderString(HttpHeaders.ACCEPT); - - if (value == null || value.isEmpty()) { - return WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST; + if (headers.isObservedAndReset(HttpHeaders.ACCEPT) && acceptTypeCache.isInitialized()) { + acceptTypeCache = acceptTypeCache(); } + return acceptTypeCache.get(); + } - try { - return Collections.unmodifiableList(HttpHeaderReader.readAcceptMediaType(value)); - } catch (ParseException e) { - throw exception(HttpHeaders.ACCEPT, value, e); - } + private LazyValue> acceptTypeCache() { + return Values.lazy((Value>) () -> { + final String value = getHeaderString(HttpHeaders.ACCEPT); + + if (value == null || value.isEmpty()) { + return WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST; + } + + try { + return Collections.unmodifiableList(HttpHeaderReader.readAcceptMediaType(value)); + } catch (ParseException e) { + throw exception(HttpHeaders.ACCEPT, value, e); + } + }); } /** diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/MediaTypeProvider.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/MediaTypeProvider.java index bfd51b410c..55552d418d 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/MediaTypeProvider.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/MediaTypeProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2010, 2022 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -24,6 +24,7 @@ import javax.inject.Singleton; import org.glassfish.jersey.internal.LocalizationMessages; +import org.glassfish.jersey.internal.util.collection.LRU; import org.glassfish.jersey.spi.HeaderDelegateProvider; import static org.glassfish.jersey.message.internal.Utils.throwIllegalArgumentExceptionIfNull; @@ -39,6 +40,8 @@ public class MediaTypeProvider implements HeaderDelegateProvider { private static final String MEDIA_TYPE_IS_NULL = LocalizationMessages.MEDIA_TYPE_IS_NULL(); + private static final LRU FROM_STRING = LRU.create(); + private static final LRU TO_STRING = LRU.create(); @Override public boolean supports(Class type) { return MediaType.class.isAssignableFrom(type); @@ -49,13 +52,25 @@ public String toString(MediaType header) { throwIllegalArgumentExceptionIfNull(header, MEDIA_TYPE_IS_NULL); - StringBuilder b = new StringBuilder(); - b.append(header.getType()).append('/').append(header.getSubtype()); - for (Map.Entry e : header.getParameters().entrySet()) { - b.append(";").append(e.getKey()).append('='); - StringBuilderUtils.appendQuotedIfNonToken(b, e.getValue()); + String cached = TO_STRING.getIfPresent(header); + + if (cached == null) { + synchronized (TO_STRING) { + cached = TO_STRING.getIfPresent(header); + if (cached == null) { + StringBuilder b = new StringBuilder(); + b.append(header.getType()).append('/').append(header.getSubtype()); + for (Map.Entry e : header.getParameters().entrySet()) { + b.append(";").append(e.getKey()).append('='); + StringBuilderUtils.appendQuotedIfNonToken(b, e.getValue()); + } + + cached = b.toString(); + TO_STRING.put(header, cached); + } + } } - return b.toString(); + return cached; } @Override @@ -63,12 +78,23 @@ public MediaType fromString(String header) { throwIllegalArgumentExceptionIfNull(header, MEDIA_TYPE_IS_NULL); - try { - return valueOf(HttpHeaderReader.newInstance(header)); - } catch (ParseException ex) { - throw new IllegalArgumentException( - "Error parsing media type '" + header + "'", ex); + MediaType cached = FROM_STRING.getIfPresent(header); + + if (cached == null) { + synchronized (FROM_STRING) { + cached = FROM_STRING.getIfPresent(header); + if (cached == null) { + try { + cached = valueOf(HttpHeaderReader.newInstance(header)); + } catch (ParseException ex) { + throw new IllegalArgumentException("Error parsing media type '" + header + "'", ex); + } + FROM_STRING.put(header, cached); + } + } } + + return cached; } /** diff --git a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java index 64a89d7e8e..2092ad94af 100644 --- a/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java +++ b/core-common/src/main/java/org/glassfish/jersey/message/internal/OutboundMessageContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2022 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -47,11 +47,16 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.NewCookie; +import javax.ws.rs.ext.RuntimeDelegate; import org.glassfish.jersey.CommonProperties; import org.glassfish.jersey.internal.RuntimeDelegateDecorator; import org.glassfish.jersey.internal.LocalizationMessages; import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.internal.util.collection.GuardianStringKeyMultivaluedMap; +import org.glassfish.jersey.internal.util.collection.LazyValue; +import org.glassfish.jersey.internal.util.collection.Value; +import org.glassfish.jersey.internal.util.collection.Values; /** * Base outbound message context implementation. @@ -63,9 +68,11 @@ public class OutboundMessageContext { private static final List WILDCARD_ACCEPTABLE_TYPE_SINGLETON_LIST = Collections.singletonList(MediaTypes.WILDCARD_ACCEPTABLE_TYPE); - private final MultivaluedMap headers; + private final GuardianStringKeyMultivaluedMap headers; private final CommittingOutputStream committingOutputStream; private Configuration configuration; + private RuntimeDelegate runtimeDelegateDecorator; + private LazyValue mediaTypeCache; private Object entity; private GenericType entityType; @@ -101,9 +108,13 @@ public static interface StreamProvider { */ public OutboundMessageContext(Configuration configuration) { this.configuration = configuration; - this.headers = HeaderUtils.createOutbound(); + this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createOutbound()); this.committingOutputStream = new CommittingOutputStream(); this.entityStream = committingOutputStream; + this.runtimeDelegateDecorator = RuntimeDelegateDecorator.configured(configuration); + this.mediaTypeCache = mediaTypeCache(); + + headers.setGuard(HttpHeaders.CONTENT_LENGTH); } /** @@ -113,7 +124,8 @@ public OutboundMessageContext(Configuration configuration) { * @param original the original outbound message context. */ public OutboundMessageContext(OutboundMessageContext original) { - this.headers = HeaderUtils.createOutbound(); + this.headers = new GuardianStringKeyMultivaluedMap<>(HeaderUtils.createOutbound()); + this.headers.setGuard(HttpHeaders.CONTENT_LENGTH); this.headers.putAll(original.headers); this.committingOutputStream = new CommittingOutputStream(); this.entityStream = committingOutputStream; @@ -122,6 +134,8 @@ public OutboundMessageContext(OutboundMessageContext original) { this.entityType = original.entityType; this.entityAnnotations = original.entityAnnotations; this.configuration = original.configuration; + this.runtimeDelegateDecorator = original.runtimeDelegateDecorator; + this.mediaTypeCache = original.mediaTypeCache(); } /** @@ -153,7 +167,7 @@ public void replaceHeaders(MultivaluedMap headers) { * @return multi-valued map of outbound message header names to their string-converted values. */ public MultivaluedMap getStringHeaders() { - return HeaderUtils.asStringHeaders(headers, configuration); + return HeaderUtils.asStringHeaders(headers, runtimeDelegateDecorator); } /** @@ -173,7 +187,7 @@ public MultivaluedMap getStringHeaders() { * character. */ public String getHeaderString(String name) { - return HeaderUtils.asHeaderString(headers.get(name), RuntimeDelegateDecorator.configured(configuration)); + return HeaderUtils.asHeaderString(headers.get(name), runtimeDelegateDecorator); } /** @@ -209,7 +223,7 @@ private T singleHeader(String name, Class valueType, Function return valueType.cast(value); } else { try { - return converter.apply(HeaderUtils.asString(value, null)); + return converter.apply(HeaderUtils.asString(value, runtimeDelegateDecorator)); } catch (ProcessingException ex) { throw exception(name, value, ex); } @@ -267,8 +281,17 @@ public Locale getLanguage() { * message entity). */ public MediaType getMediaType() { - return singleHeader(HttpHeaders.CONTENT_TYPE, MediaType.class, RuntimeDelegateDecorator.configured(configuration) - .createHeaderDelegate(MediaType.class)::fromString, false); + if (headers.isObservedAndReset(HttpHeaders.CONTENT_TYPE) && mediaTypeCache.isInitialized()) { + mediaTypeCache = mediaTypeCache(); // headers changed -> drop cache + } + return mediaTypeCache.get(); + } + + private LazyValue mediaTypeCache() { + return Values.lazy((Value) () -> + singleHeader(HttpHeaders.CONTENT_TYPE, MediaType.class, RuntimeDelegateDecorator.configured(configuration) + .createHeaderDelegate(MediaType.class)::fromString, false) + ); } /** @@ -294,7 +317,7 @@ public List getAcceptableMediaTypes() { result.add(_value); } else { conversionApplied = true; - result.addAll(HttpHeaderReader.readAcceptMediaType(HeaderUtils.asString(value, configuration))); + result.addAll(HttpHeaderReader.readAcceptMediaType(HeaderUtils.asString(value, runtimeDelegateDecorator))); } } catch (java.text.ParseException e) { throw exception(HttpHeaders.ACCEPT, value, e); @@ -333,7 +356,7 @@ public List getAcceptableLanguages() { } else { conversionApplied = true; try { - result.addAll(HttpHeaderReader.readAcceptLanguage(HeaderUtils.asString(value, configuration)) + result.addAll(HttpHeaderReader.readAcceptLanguage(HeaderUtils.asString(value, runtimeDelegateDecorator)) .stream() .map(LanguageTag::getAsLocale) .collect(Collectors.toList())); @@ -366,7 +389,7 @@ public Map getRequestCookies() { } Map result = new HashMap(); - for (String cookie : HeaderUtils.asStringList(cookies, configuration)) { + for (String cookie : HeaderUtils.asStringList(cookies, runtimeDelegateDecorator)) { if (cookie != null) { result.putAll(HttpHeaderReader.readCookies(cookie)); } @@ -454,7 +477,7 @@ public Map getResponseCookies() { } Map result = new HashMap(); - for (String cookie : HeaderUtils.asStringList(cookies, configuration)) { + for (String cookie : HeaderUtils.asStringList(cookies, runtimeDelegateDecorator)) { if (cookie != null) { NewCookie newCookie = HttpHeaderReader.readNewCookie(cookie); String cookieName = newCookie.getName(); @@ -542,7 +565,7 @@ public Set getLinks() { } else { conversionApplied = true; try { - result.add(Link.valueOf(HeaderUtils.asString(value, configuration))); + result.add(Link.valueOf(HeaderUtils.asString(value, runtimeDelegateDecorator))); } catch (IllegalArgumentException e) { throw exception(HttpHeaders.LINK, value, e); } @@ -863,6 +886,7 @@ public void close() { void setConfiguration(Configuration configuration) { this.configuration = configuration; + this.runtimeDelegateDecorator = RuntimeDelegateDecorator.configured(configuration); } /** diff --git a/core-server/src/test/java/org/glassfish/jersey/server/RequestContextBuilder.java b/core-server/src/test/java/org/glassfish/jersey/server/RequestContextBuilder.java index 9687a61b99..1647f13a85 100644 --- a/core-server/src/test/java/org/glassfish/jersey/server/RequestContextBuilder.java +++ b/core-server/src/test/java/org/glassfish/jersey/server/RequestContextBuilder.java @@ -28,6 +28,7 @@ import java.util.logging.Logger; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Configuration; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.GenericType; @@ -161,7 +162,7 @@ public RequestContextBuilder type(final String contentType) { } public RequestContextBuilder type(final MediaType contentType) { - request.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE, HeaderUtils.asString(contentType, null)); + request.getHeaders().putSingle(HttpHeaders.CONTENT_TYPE, HeaderUtils.asString(contentType, (Configuration) null)); return this; } @@ -185,7 +186,7 @@ private void putHeader(final String name, final Object value) { request.getHeaders().remove(name); return; } - request.header(name, HeaderUtils.asString(value, null)); + request.header(name, HeaderUtils.asString(value, (Configuration) null)); } private void putHeaders(final String name, final Object... values) { @@ -193,7 +194,7 @@ private void putHeaders(final String name, final Object... values) { request.getHeaders().remove(name); return; } - request.getHeaders().addAll(name, HeaderUtils.asStringList(Arrays.asList(values), null)); + request.getHeaders().addAll(name, HeaderUtils.asStringList(Arrays.asList(values), (Configuration) null)); } private void putHeaders(final String name, final String... values) { diff --git a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/HeaderUtilsTest.java b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/HeaderUtilsTest.java index 3ac47823cd..74318b1566 100644 --- a/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/HeaderUtilsTest.java +++ b/tests/e2e-core-common/src/test/java/org/glassfish/jersey/tests/e2e/common/message/internal/HeaderUtilsTest.java @@ -25,6 +25,7 @@ import java.util.List; import javax.ws.rs.core.AbstractMultivaluedMap; +import javax.ws.rs.core.Configuration; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.NewCookie; import javax.ws.rs.ext.RuntimeDelegate; @@ -101,19 +102,19 @@ public void testCreateOutbound() throws Exception { @Test public void testAsString() throws Exception { - assertNull(HeaderUtils.asString(null, null)); + assertNull(HeaderUtils.asString(null, (Configuration) null)); final String value = "value"; - assertSame(value, HeaderUtils.asString(value, null)); + assertSame(value, HeaderUtils.asString(value, (Configuration) null)); final URI uri = new URI("test"); - assertEquals(uri.toASCIIString(), HeaderUtils.asString(uri, null)); + assertEquals(uri.toASCIIString(), HeaderUtils.asString(uri, (Configuration) null)); } @Test public void testAsStringList() throws Exception { - assertNotNull(HeaderUtils.asStringList(null, null)); - assertTrue(HeaderUtils.asStringList(null, null).isEmpty()); + assertNotNull(HeaderUtils.asStringList(null, (Configuration) null)); + assertTrue(HeaderUtils.asStringList(null, (Configuration) null).isEmpty()); final URI uri = new URI("test"); final List values = new LinkedList() {{ @@ -123,7 +124,7 @@ public void testAsStringList() throws Exception { }}; // test string values - final List stringList = HeaderUtils.asStringList(values, null); + final List stringList = HeaderUtils.asStringList(values, (Configuration) null); assertEquals(Arrays.asList("value", "[null]", uri.toASCIIString()), stringList); @@ -138,7 +139,7 @@ public void testAsStringList() throws Exception { @Test public void testAsStringHeaders() throws Exception { - assertNull(HeaderUtils.asStringHeaders(null, null)); + assertNull(HeaderUtils.asStringHeaders(null, (Configuration) null)); final AbstractMultivaluedMap headers = HeaderUtils.createOutbound(); @@ -150,7 +151,7 @@ public void testAsStringHeaders() throws Exception { headers.putSingle("k3", "value3"); - final MultivaluedMap stringHeaders = HeaderUtils.asStringHeaders(headers, null); + final MultivaluedMap stringHeaders = HeaderUtils.asStringHeaders(headers, (Configuration) null); // test string values assertEquals(Arrays.asList("value", "value2"), diff --git a/tests/e2e-entity/src/test/java/org/glassfish/jersey/tests/e2e/header/HeaderDelegateProviderTest.java b/tests/e2e-entity/src/test/java/org/glassfish/jersey/tests/e2e/header/HeaderDelegateProviderTest.java index 9d118a5a58..2218d913de 100644 --- a/tests/e2e-entity/src/test/java/org/glassfish/jersey/tests/e2e/header/HeaderDelegateProviderTest.java +++ b/tests/e2e-entity/src/test/java/org/glassfish/jersey/tests/e2e/header/HeaderDelegateProviderTest.java @@ -16,6 +16,7 @@ package org.glassfish.jersey.tests.e2e.header; + import org.glassfish.jersey.CommonProperties; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.internal.ServiceFinder; @@ -36,6 +37,7 @@ import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Configuration; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -155,7 +157,7 @@ public void testTheProviderIsFound() { public void testHeaderDelegateIsUsedWhenRuntimeDelegateDecoratorIsUsed() { MultivaluedHashMap headers = new MultivaluedHashMap(); headers.put(HEADER_NAME, Arrays.asList(new BeanForHeaderDelegateProviderTest())); - MultivaluedMap converted = HeaderUtils.asStringHeaders(headers, null); + MultivaluedMap converted = HeaderUtils.asStringHeaders(headers, (Configuration) null); testMap(converted, BeanForHeaderDelegateProviderTest.getValue()); Client client = ClientBuilder.newClient().property(CommonProperties.METAINF_SERVICES_LOOKUP_DISABLE, false); diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ResponseTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ResponseTest.java index f1f42bcb59..5730e4ba54 100644 --- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ResponseTest.java +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/api/ResponseTest.java @@ -24,6 +24,7 @@ import java.util.Locale; import java.util.Set; +import javax.ws.rs.core.Configuration; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -247,7 +248,7 @@ private String verifyResponse(Response resp, String content, int status, } MultivaluedMap mvp = HeaderUtils.asStringHeaders( - resp.getMetadata(), null); + resp.getMetadata(), (Configuration) null); for (String key : mvp.keySet()) { sb.append(indent + "Processing Key found in response: ").append(key).append(": ").append(mvp.get(key)).append("; ")