This is an automated email from the ASF dual-hosted git repository. rmannibucau pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/johnzon.git
The following commit(s) were added to refs/heads/master by this push: new a3b6f44 JOHNZON-281 ensure NoContentException can be thrown when an empty incoming stream arrives in JsonbJaxrsProvider and JsrProvider a3b6f44 is described below commit a3b6f4460196b69f47751471c53ca8085d542ee3 Author: Romain Manni-Bucau <rmannibu...@apache.org> AuthorDate: Sun Sep 29 16:57:27 2019 +0200 JOHNZON-281 ensure NoContentException can be thrown when an empty incoming stream arrives in JsonbJaxrsProvider and JsrProvider --- .../org/apache/johnzon/jaxrs/DelegateProvider.java | 20 ++++- .../org/apache/johnzon/jaxrs/JohnzonProvider.java | 4 + .../java/org/apache/johnzon/jaxrs/JsrProvider.java | 19 +++++ .../jaxrs/NoContentExceptionHandlerReader.java | 63 +++++++++++++++ .../johnzon/jaxrs/WildcardJohnzonProvider.java | 4 + .../apache/johnzon/jaxrs/WildcardJsrProvider.java | 4 + .../jaxrs/jsonb/jaxrs/JsonbJaxrsProvider.java | 65 ++++++++++++++-- .../jaxrs/jsonb/jaxrs/JsonbJaxrsProviderTest.java | 91 ++++++++++++++++++++++ .../apache/johnzon/jsonb/jaxrs/JsonbJaxRsTest.java | 2 +- src/site/markdown/index.md | 9 +++ 10 files changed, 274 insertions(+), 7 deletions(-) diff --git a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/DelegateProvider.java b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/DelegateProvider.java index aee0790..21a2b0e 100644 --- a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/DelegateProvider.java +++ b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/DelegateProvider.java @@ -18,6 +18,8 @@ */ package org.apache.johnzon.jaxrs; +import static java.util.Optional.ofNullable; + import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.MessageBodyReader; @@ -33,7 +35,8 @@ public abstract class DelegateProvider<T> implements MessageBodyWriter<T>, Messa private final MessageBodyWriter<T> writer; protected DelegateProvider(final MessageBodyReader<T> reader, final MessageBodyWriter<T> writer) { - this.reader = reader; + this.reader = shouldThrowNoContentExceptionOnEmptyStreams() && isJaxRs2() ? + new NoContentExceptionHandlerReader<>(reader) : reader; this.writer = writer; } @@ -70,4 +73,19 @@ public abstract class DelegateProvider<T> implements MessageBodyWriter<T>, Messa final OutputStream entityStream) throws IOException { writer.writeTo(t, rawType, genericType, annotations, mediaType, httpHeaders, entityStream); } + + protected boolean shouldThrowNoContentExceptionOnEmptyStreams() { + return false; + } + + private static boolean isJaxRs2() { + try { + ofNullable(Thread.currentThread().getContextClassLoader()) + .orElseGet(ClassLoader::getSystemClassLoader) + .loadClass("javax.ws.rs.core.Feature"); + return true; + } catch (final NoClassDefFoundError | ClassNotFoundException e) { + return false; + } + } } diff --git a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/JohnzonProvider.java b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/JohnzonProvider.java index 9847c4d..b99e42b 100644 --- a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/JohnzonProvider.java +++ b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/JohnzonProvider.java @@ -37,4 +37,8 @@ public class JohnzonProvider<T> extends DelegateProvider<T> { public JohnzonProvider() { this(new MapperBuilder().setDoCloseOnStreams(false).build(), null); } + + protected boolean shouldThrowNoContentExceptionOnEmptyStreams() { + return Boolean.getBoolean("johnzon.jaxrs.johnzon.throwNoContentExceptionOnEmptyStreams"); + } } diff --git a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/JsrProvider.java b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/JsrProvider.java index 06ec86d..872dfe9 100644 --- a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/JsrProvider.java +++ b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/JsrProvider.java @@ -18,9 +18,16 @@ */ package org.apache.johnzon.jaxrs; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + import javax.json.JsonStructure; import javax.ws.rs.Consumes; import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.Provider; @Provider @@ -30,4 +37,16 @@ public class JsrProvider extends DelegateProvider<JsonStructure> { public JsrProvider() { super(new JsrMessageBodyReader(), new JsrMessageBodyWriter()); } + + @Override + public JsonStructure readFrom(final Class<JsonStructure> rawType, final Type genericType, + final Annotation[] annotations, final MediaType mediaType, + final MultivaluedMap<String, String> httpHeaders, + final InputStream entityStream) throws IOException { + return super.readFrom(rawType, genericType, annotations, mediaType, httpHeaders, entityStream); + } + + protected boolean shouldThrowNoContentExceptionOnEmptyStreams() { + return Boolean.getBoolean("johnzon.jaxrs.jsr.throwNoContentExceptionOnEmptyStreams"); + } } diff --git a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/NoContentExceptionHandlerReader.java b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/NoContentExceptionHandlerReader.java new file mode 100644 index 0000000..acd496d --- /dev/null +++ b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/NoContentExceptionHandlerReader.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * + * 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 org.apache.johnzon.jaxrs; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NoContentException; +import javax.ws.rs.ext.MessageBodyReader; + +public class NoContentExceptionHandlerReader<T> implements MessageBodyReader<T> { + private final MessageBodyReader<T> delegate; + + public NoContentExceptionHandlerReader(final MessageBodyReader<T> delegate) { + this.delegate = delegate; + } + + public MessageBodyReader<T> getDelegate() { + return delegate; + } + + @Override + public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType) { + return delegate.isReadable(type, genericType, annotations, mediaType); + } + + @Override + public T readFrom(final Class<T> type, final Type genericType, final Annotation[] annotations, + final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders, + final InputStream entityStream) throws IOException, WebApplicationException { + try { + return delegate.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream); + } catch (final IllegalStateException ise) { + if (ise.getClass().getName() + .equals("org.apache.johnzon.core.JsonReaderImpl$NothingToRead")) { + // spec enables to return an empty java object but it does not mean anything in JSON context so just fail + throw new NoContentException(ise); + } + throw ise; + } + } +} diff --git a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/WildcardJohnzonProvider.java b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/WildcardJohnzonProvider.java index 275c0f1..585622d 100644 --- a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/WildcardJohnzonProvider.java +++ b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/WildcardJohnzonProvider.java @@ -45,4 +45,8 @@ public class WildcardJohnzonProvider<T> extends DelegateProvider<T> { public WildcardJohnzonProvider() { this(new MapperBuilder().setDoCloseOnStreams(false).build(), null); } + + protected boolean shouldThrowNoContentExceptionOnEmptyStreams() { + return Boolean.getBoolean("johnzon.jaxrs.johnzon.wildcard.throwNoContentExceptionOnEmptyStreams"); + } } diff --git a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/WildcardJsrProvider.java b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/WildcardJsrProvider.java index e424207..657a69e 100644 --- a/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/WildcardJsrProvider.java +++ b/johnzon-jaxrs/src/main/java/org/apache/johnzon/jaxrs/WildcardJsrProvider.java @@ -37,4 +37,8 @@ public class WildcardJsrProvider extends DelegateProvider<JsonStructure> { public WildcardJsrProvider() { super(new JsrMessageBodyReader(), new JsrMessageBodyWriter()); } + + protected boolean shouldThrowNoContentExceptionOnEmptyStreams() { + return Boolean.getBoolean("johnzon.jaxrs.jsr.wildcard.throwNoContentExceptionOnEmptyStreams"); + } } diff --git a/johnzon-jsonb/src/main/java/org/apache/johnzon/jaxrs/jsonb/jaxrs/JsonbJaxrsProvider.java b/johnzon-jsonb/src/main/java/org/apache/johnzon/jaxrs/jsonb/jaxrs/JsonbJaxrsProvider.java index 15e26fc..20d547a 100644 --- a/johnzon-jsonb/src/main/java/org/apache/johnzon/jaxrs/jsonb/jaxrs/JsonbJaxrsProvider.java +++ b/johnzon-jsonb/src/main/java/org/apache/johnzon/jaxrs/jsonb/jaxrs/JsonbJaxrsProvider.java @@ -18,6 +18,7 @@ */ package org.apache.johnzon.jaxrs.jsonb.jaxrs; +import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toMap; import javax.json.JsonStructure; @@ -29,6 +30,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NoContentException; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import javax.ws.rs.ext.MessageBodyReader; @@ -63,7 +65,9 @@ public class JsonbJaxrsProvider<T> implements MessageBodyWriter<T>, MessageBodyR protected final Collection<String> ignores; protected final JsonbConfig config = new JsonbConfig(); protected volatile Function<Class<?>, Jsonb> delegate = null; + protected volatile ReadImpl readImpl = null; private boolean customized; + private Boolean throwNoContentExceptionOnEmptyStreams; @Context private Providers providers; @@ -80,6 +84,11 @@ public class JsonbJaxrsProvider<T> implements MessageBodyWriter<T>, MessageBodyR return ignores != null && ignores.contains(type.getName()); } + public void setThrowNoContentExceptionOnEmptyStreams(final boolean throwNoContentExceptionOnEmptyStreams) { + this.throwNoContentExceptionOnEmptyStreams = throwNoContentExceptionOnEmptyStreams; + // customized = false since it is not a jsonb customization but a MBR one + } + // config - main containers support the configuration of providers this way public void setFailOnUnknownProperties(final boolean active) { config.setProperty("johnzon.fail-on-unknown-properties", active); @@ -187,8 +196,9 @@ public class JsonbJaxrsProvider<T> implements MessageBodyWriter<T>, MessageBodyR @Override public T readFrom(final Class<T> type, final Type genericType, final Annotation[] annotations, final MediaType mediaType, - final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws WebApplicationException { - return getJsonb(type).fromJson(entityStream, genericType); + final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws WebApplicationException, IOException { + final Jsonb jsonb = getJsonb(type); + return (T) readImpl.doRead(jsonb, genericType, entityStream); } @Override @@ -205,22 +215,63 @@ public class JsonbJaxrsProvider<T> implements MessageBodyWriter<T>, MessageBodyR if (delegate == null){ synchronized (this) { if (delegate == null) { + if (throwNoContentExceptionOnEmptyStreams == null) { + throwNoContentExceptionOnEmptyStreams = initThrowNoContentExceptionOnEmptyStreams(); + } final ContextResolver<Jsonb> contextResolver = providers.getContextResolver(Jsonb.class, MediaType.APPLICATION_JSON_TYPE); if (contextResolver != null) { if (customized) { - Logger.getLogger(JsonbJaxrsProvider.class.getName()) - .warning("Customizations done on the Jsonb instance will be ignored because a ContextResolver<Jsonb> was found"); + logger().warning("Customizations done on the Jsonb instance will be ignored because a ContextResolver<Jsonb> was found"); + } + if (throwNoContentExceptionOnEmptyStreams) { + logger().warning("Using a ContextResolver<Jsonb>, NoContentException will not be thrown for empty streams"); } delegate = new DynamicInstance(contextResolver); // faster than contextResolver::getContext } else { - delegate = new ProvidedInstance(createJsonb()); // don't recreate it + // don't recreate it for perfs + delegate = new ProvidedInstance(createJsonb()); } } + readImpl = throwNoContentExceptionOnEmptyStreams ? + this::doReadWithNoContentException : + this::doRead; } } return delegate.apply(type); } + private boolean initThrowNoContentExceptionOnEmptyStreams() { + try { + ofNullable(Thread.currentThread().getContextClassLoader()) + .orElseGet(ClassLoader::getSystemClassLoader) + .loadClass("javax.ws.rs.core.Feature"); + return true; + } catch (final NoClassDefFoundError | ClassNotFoundException e) { + return false; + } + } + + private Object doRead(final Jsonb jsonb, final Type t, final InputStream stream) { + return jsonb.fromJson(stream, t); + } + + private Object doReadWithNoContentException(final Jsonb jsonb, final Type t, final InputStream stream) throws NoContentException { + try { + return doRead(jsonb, t, stream); + } catch (final IllegalStateException ise) { + if (ise.getClass().getName() + .equals("org.apache.johnzon.core.JsonReaderImpl$NothingToRead")) { + // spec enables to return an empty java object but it does not mean anything in JSON context so just fail + throw new NoContentException(ise); + } + throw ise; + } + } + + private Logger logger() { + return Logger.getLogger(JsonbJaxrsProvider.class.getName()); + } + @Override public synchronized void close() throws Exception { if (AutoCloseable.class.isInstance(delegate)) { @@ -228,6 +279,10 @@ public class JsonbJaxrsProvider<T> implements MessageBodyWriter<T>, MessageBodyR } } + private interface ReadImpl { + Object doRead(Jsonb jsonb, Type type, InputStream entityStream) throws IOException; + } + private static final class DynamicInstance implements Function<Class<?>, Jsonb> { private final ContextResolver<Jsonb> contextResolver; diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jaxrs/jsonb/jaxrs/JsonbJaxrsProviderTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jaxrs/jsonb/jaxrs/JsonbJaxrsProviderTest.java new file mode 100644 index 0000000..ae14878 --- /dev/null +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jaxrs/jsonb/jaxrs/JsonbJaxrsProviderTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * + * 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 org.apache.johnzon.jaxrs.jsonb.jaxrs; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.NoContentException; +import javax.ws.rs.ext.ContextResolver; + +import org.apache.cxf.jaxrs.impl.ProvidersImpl; +import org.apache.johnzon.core.JsonReaderImpl; +import org.junit.Test; + +public class JsonbJaxrsProviderTest { + @Test(expected = NoContentException.class) + public void noContentExceptionAuto() throws IOException { // we run on jaxrs 2 in the build + readFoo(null, new ByteArrayInputStream(new byte[0])); + } + + @Test(expected = NoContentException.class) + public void noContentException() throws IOException { + readFoo(true, new ByteArrayInputStream(new byte[0])); + } + + @Test(expected = JsonReaderImpl.NothingToRead.class) + public void noContentExceptionDisabled() throws IOException { + readFoo(false, new ByteArrayInputStream(new byte[0])); + } + + @Test // just to ensure we didnt break soemthing on read impl + public void validTest() throws IOException { + final Foo foo = readFoo(null, new ByteArrayInputStream("{\"name\":\"ok\"}".getBytes(StandardCharsets.UTF_8))); + assertEquals("ok", foo.name); + } + + private Foo readFoo(final Boolean set, final InputStream stream) throws IOException { + return new JsonbJaxrsProvider<Foo>() {{ + if (set != null) { + setThrowNoContentExceptionOnEmptyStreams(set); + } + setProviders(this); + }}.readFrom(Foo.class, Foo.class, new Annotation[0], + MediaType.APPLICATION_JSON_TYPE, new MultivaluedHashMap<>(), + stream); + } + + private void setProviders(final JsonbJaxrsProvider<Foo> provider) { + try { + final Field providers = JsonbJaxrsProvider.class.getDeclaredField("providers"); + providers.setAccessible(true); + providers.set(provider, new ProvidersImpl(null) { + @Override + public <T> ContextResolver<T> getContextResolver(final Class<T> contextType, final MediaType mediaType) { + return null; + } + }); + } catch (final Exception e) { + fail(e.getMessage()); + } + } + + public static class Foo { + public String name; + } +} diff --git a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/jaxrs/JsonbJaxRsTest.java b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/jaxrs/JsonbJaxRsTest.java index 91fca7d..890f7ab 100644 --- a/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/jaxrs/JsonbJaxRsTest.java +++ b/johnzon-jsonb/src/test/java/org/apache/johnzon/jsonb/jaxrs/JsonbJaxRsTest.java @@ -168,7 +168,7 @@ public class JsonbJaxRsTest { return null; } }; - final List<Johnzon> johnzons = client().path("johnzon/all2").get(new GenericType<List<Johnzon>>(list)); + final List<Johnzon> johnzons = client().path("johnzon/all2").get(new GenericType<List<Johnzon>>(list) {}); assertEquals(2, johnzons.size()); int i = 1; for (final Johnzon f : johnzons) { diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 0170515..91f3f9f 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -251,6 +251,8 @@ split makes it easier to mix json and other MediaType in the same resource (like Tip: ConfigurableJohnzonProvider maps most of MapperBuilder configuration letting you configure it through any IoC including not programming language based formats. +IMPORTANT: when used with `johnzon-core`, `NoContentException` is not thrown in case of an empty incoming input stream by these providers except `JsrProvider` to limit the breaking changes. + ### TomEE Configuration TomEE uses by default Johnzon as JAX-RS provider for versions 7.x. If you want however to customize it you need to follow this procedure: @@ -315,6 +317,13 @@ JsonbConfig specific properties: TIP: more in JohnzonBuilder class. +A JAX-RS provider based on JSON-B is provided in the module as well. It is `org.apache.johnzon.jaxrs.jsonb.jaxrs.JsonbJaxrsProvider`. + +IMPORTANT: in JAX-RS 1.0 the provider can throw any exception he wants for an empty incoming stream on reader side. This had been broken in JAX-RS 2.x where it must throw a `javax.ws.rs.core.NoContentException`. +To ensure you can pick the implementation you can and limit the breaking changes, you can set ̀throwNoContentExceptionOnEmptyStreams` on the provider to switch between both behaviors. +Default will be picked from the current available API. Finally, this behavior only works with `johnzon-core`. + + #### Integration with `JsonValue` You can use some optimization to map a `JsonObject` to a POJO using Johnzon `JsonValueReader` and `JsonValueWriter`: