Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 6278: Programmatically control media providers with Nima WebServer #6412

Merged
merged 2 commits into from
Mar 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ static ContentEncodingContext create(Config config) {
ContentEncoder encoder(Headers headers);

/**
* Builder to set up this encoding support content.
* Builder to set up this encoding support context.
*
* @return a new builder
*/
Expand All @@ -126,11 +126,15 @@ class Builder implements io.helidon.common.Builder<Builder, ContentEncodingConte
private final HelidonServiceLoader.Builder<ContentEncodingProvider> encodingProviders
= HelidonServiceLoader.builder(ServiceLoader.load(ContentEncodingProvider.class));

// Builder instance must be created using factory method.
private Builder() {
}

/**
* Update this builder from configuration.
* <p>
* Configuration:<ul>
* <li><b>disable: true</b> - to disable content encoding support</li>
* <li><b>discover-services: false</b> - to disable content encoding support providers service loader discovery</li>
* </ul>
*
* @param config configuration to use
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,21 +16,37 @@

package io.helidon.nima.http.media;

import java.util.ServiceLoader;

import io.helidon.common.GenericType;
import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.config.Config;
import io.helidon.common.http.Headers;
import io.helidon.common.http.WritableHeaders;
import io.helidon.nima.http.media.spi.MediaSupportProvider;

/**
* Media context to obtain readers and writers of various supported content types.
*/
public interface MediaContext {

/**
* Create a new media context from {@link java.util.ServiceLoader}.
*
* @return media context
*/
static MediaContext create() {
return new MediaContextImpl();
return builder().build();
}

/**
* Create a new media context and apply provided configuration.
*
* @param config configuration to use
* @return media context
*/
static MediaContext create(Config config) {
return builder().config(config).build();
}

/**
Expand Down Expand Up @@ -79,4 +95,76 @@ <T> EntityReader<T> reader(GenericType<T> type,
*/
<T> EntityWriter<T> writer(GenericType<T> type,
WritableHeaders<?> requestHeaders);

/**
* Builder to set up this media support context.
*
* @return a new builder
*/
static Builder builder() {
return new Builder();
}

/**
* Fluent API builder for {@link MediaContext}.
*/
class Builder implements io.helidon.common.Builder<Builder, MediaContext> {

private final HelidonServiceLoader.Builder<MediaSupportProvider> mediaSupportProviders;

// Builder instance must be created using factory method.
private Builder() {
mediaSupportProviders = HelidonServiceLoader.builder(ServiceLoader.load(MediaSupportProvider.class));
}

/**
* Update this builder from configuration.
* <p>
* Configuration:<ul>
* <li><b>discover-services: false</b> - to disable media support providers service loader discovery</li>
* </ul>
*
* @param config configuration to use
* @return updated builder instance
*/
public Builder config(Config config) {
config.get("discover-services").asBoolean().ifPresent(this::discoverServices);
return this;
}

/**
* Whether Java Service Loader should be used to load {@link MediaSupportProvider}.
*
* @return updated builder
*/
public Builder discoverServices(boolean discoverServices) {
this.mediaSupportProviders.useSystemServiceLoader(discoverServices);
return this;
}

/**
* Configure media support provider.
* This instance has priority over provider(s) discovered by service loader.
*
* @param mediaSupportProvider explicit media support provider
* @return updated builder
*/
public Builder addMediaSupportProvider(MediaSupportProvider mediaSupportProvider) {
mediaSupportProviders.addService(mediaSupportProvider);
return this;
}

@Override
public MediaContext build() {
return new MediaContextImpl(
mediaSupportProviders
.addService(new StringSupportProvider())
.addService(new FormParamsSupportProvider())
.addService(new PathSupportProvider())
.build()
.asList()
);
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2022, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,12 +21,10 @@
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import io.helidon.common.GenericType;
import io.helidon.common.HelidonServiceLoader;
import io.helidon.common.http.Headers;
import io.helidon.common.http.WritableHeaders;
import io.helidon.nima.http.media.spi.MediaSupportProvider;
Expand All @@ -42,16 +40,11 @@ class MediaContextImpl implements MediaContext {
private static final ConcurrentHashMap<GenericType<?>, AtomicBoolean> LOGGED_READERS = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<GenericType<?>, AtomicBoolean> LOGGED_WRITERS = new ConcurrentHashMap<>();

private final List<MediaSupportProvider> providers =
HelidonServiceLoader.builder(ServiceLoader.load(MediaSupportProvider.class))
.addService(new StringSupportProvider())
.addService(new FormParamsSupportProvider())
.addService(new PathSupportProvider())
.build()
.asList();
private final List<MediaSupportProvider> providers;

MediaContextImpl() {
providers.forEach(it -> it.init(this));
MediaContextImpl(List<MediaSupportProvider> providers) {
this.providers = providers;
this.providers.forEach(it -> it.init(this));
}

@Override
Expand Down
11 changes: 11 additions & 0 deletions nima/webserver/webserver/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@
<groupId>io.helidon.nima.http.encoding</groupId>
<scope>test</scope>
</dependency>
<!-- Put JSON media support on test classpath to see whether it's loaded or not. -->
<dependency>
<artifactId>helidon-nima-http-media-jsonp</artifactId>
<groupId>io.helidon.nima.http.media</groupId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
barchetta marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ class Builder implements io.helidon.common.Builder<Builder, WebServer>, Router.R
= HelidonServiceLoader.builder(ServiceLoader.load(ServerConnectionProvider.class));

private Config providersConfig = Config.empty();
private MediaContext mediaContext = MediaContext.create();
// MediaContext should be updated with config processing or during final build if not set.
private MediaContext mediaContext;
private ContentEncodingContext contentEncodingContext = ContentEncodingContext.create();

private boolean shutdownHook = true;
Expand All @@ -192,6 +193,9 @@ public WebServer build() {
.id("web-" + WEBSERVER_COUNTER.getAndIncrement())
.build();
}
if (mediaContext == null) {
mediaContext(MediaContext.create());
}

return new LoomServer(this, directHandlers.build());
}
Expand Down Expand Up @@ -254,6 +258,11 @@ public Builder config(Config config) {
config.get("content-encoding")
.as(ContentEncodingContext::create)
.ifPresent(this::contentEncodingContext);
// Configure media support
config.get("media-support")
.as(MediaContext::create)
// MediaContext always needs to be refreshed after config change
.ifPresent(this::mediaContext);
// Store providers config node for later usage.
providersConfig = config.get("connection-providers");
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,28 @@

package io.helidon.nima.webserver;

import java.lang.reflect.InvocationTargetException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.NoSuchElementException;

import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.json.JsonStructure;

import org.junit.jupiter.api.Test;

import io.helidon.common.GenericType;
import io.helidon.common.http.WritableHeaders;
import io.helidon.config.Config;
import io.helidon.nima.http.encoding.ContentEncodingContext;
import io.helidon.nima.http.media.EntityWriter;
import io.helidon.nima.http.media.MediaContext;
import io.helidon.nima.http.media.jsonp.JsonpMediaSupportProvider;
import io.helidon.nima.webserver.spi.ServerConnectionSelector;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.junit.jupiter.api.Assertions.fail;
Expand Down Expand Up @@ -73,6 +82,120 @@ void testContentEncodingConfig() {
failsWith(() -> contentEncodingContext.encoder("x-gzip"), NoSuchElementException.class);
}

// Check that WebServer MediaContext builder produces expected provider using MediaContext configuration from Config file:
// - java service loader enabled in server node of application.yaml
// - JSON should be present
// Writing JsonObject should work and produce valid JSON data.
@Test
void testMediaSupportFileConfigJson() throws IOException {
Config config = Config.create();
Config server = config.get("server2");
WebServer.Builder wsBuilder = WebServer.builder().config(server);
MediaContext mediaContext = wsBuilder.mediaContext();
assertThat(mediaContext, is(notNullValue()));
WritableHeaders<?> writableHeaders = WritableHeaders.create();
EntityWriter<JsonObject> writer = mediaContext.writer(GenericType.create(JsonObject.class), writableHeaders);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
writer.write(GenericType.create(JsonObject.class),
Json.createObjectBuilder()
.add("name", "John Smith")
.build(),
outputStream,
writableHeaders);
outputStream.close();
// Verify written data
JsonObject verify = Json.createObjectBuilder()
.add("name", "John Smith")
.build();
JsonStructure js = Json.createReader(new ByteArrayInputStream(outputStream.toByteArray())).read();
assertThat(js.asJsonObject(), is(verify));
}

// Check that WebServer MediaContext builder produces expected provider using MediaContext configuration from Config file:
// - java service loader disabled in server node of application.yaml
// - JSON should not be present
// Writing JsonObject should work and produce valid JSON data.
@Test
void testMediaSupportFileConfigNoJson() throws IOException {
Config config = Config.create();
WebServer.Builder wsBuilder = WebServer.builder().config(config.get("server"));
MediaContext mediaContext = wsBuilder.mediaContext();
assertThat(mediaContext, is(notNullValue()));
WritableHeaders<?> writableHeaders = WritableHeaders.create();
EntityWriter<JsonObject> writer = mediaContext.writer(GenericType.create(JsonObject.class), writableHeaders);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
failsWith(() -> writer.write(
GenericType.create(JsonObject.class),
Json.createObjectBuilder()
.add("name", "John Smith")
.build(),
outputStream,
writableHeaders),
IllegalArgumentException.class);
outputStream.close();
}

// Check that WebServer MediaContext builder produces expected provider using manually built MediaContext:
// - java service loader disabled
// - JSON added manually
// Writing JsonObject should work and produce valid JSON data.
@Test
void testMediaSupportManualConfigJson() throws IOException {
Config config = Config.create();
WebServer.Builder wsBuilder = WebServer.builder()
.config(config.get("server"))
.mediaContext(MediaContext.builder()
.discoverServices(false)
.addMediaSupportProvider(new JsonpMediaSupportProvider())
.build());
MediaContext mediaContext = wsBuilder.mediaContext();
assertThat(mediaContext, is(notNullValue()));
WritableHeaders<?> writableHeaders = WritableHeaders.create();
EntityWriter<JsonObject> writer = mediaContext.writer(GenericType.create(JsonObject.class), writableHeaders);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
writer.write(GenericType.create(JsonObject.class),
Json.createObjectBuilder()
.add("name", "John Smith")
.build(),
outputStream,
writableHeaders);
outputStream.close();
// Verify written data
JsonObject verify = Json.createObjectBuilder()
.add("name", "John Smith")
.build();
JsonStructure js = Json.createReader(new ByteArrayInputStream(outputStream.toByteArray())).read();
assertThat(js.asJsonObject(), is(verify));
}

// Check that WebServer MediaContext builder produces expected provider using manually built MediaContext:
// - java service loader disabled
// - JSON not added manually
// Writing JsonObject should fail on missing JSOn media support.
@Test
void testMediaSupportManualConfigNoJson() throws IOException {
Config config = Config.create();
WebServer.Builder wsBuilder = WebServer.builder()
.config(config.get("server"))
.mediaContext(MediaContext.builder()
.discoverServices(false)
.build());
MediaContext mediaContext = wsBuilder.mediaContext();
assertThat(mediaContext, is(notNullValue()));
WritableHeaders<?> writableHeaders = WritableHeaders.create();
EntityWriter<JsonObject> writer = mediaContext.writer(GenericType.create(JsonObject.class), writableHeaders);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024);
failsWith(() -> writer.write(
GenericType.create(JsonObject.class),
Json.createObjectBuilder()
.add("name", "John Smith")
.build(),
outputStream,
writableHeaders),
IllegalArgumentException.class);
outputStream.close();
}

// Verify that provided task throws an exception
private static void failsWith(Runnable task, Class<? extends Exception> exception) {
try {
Expand Down
Loading