JOHNZON-281 ensure NoContentException can be thrown when an empty incoming stream arrives in JsonbJaxrsProvider and JsrProvider
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 @@
     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 @@
                         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 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 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 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 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.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 @@
     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 @@
         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 @@
 
     @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 @@
         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 @@
         }
     }
 
+    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 @@
                 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 @@
 
 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 @@
 
 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`: