Skip to content

Commit

Permalink
feat: Dynamic JsonMappingProvider registration.
Browse files Browse the repository at this point in the history
  • Loading branch information
nstdio committed Apr 9, 2022
1 parent aec3906 commit 4975edd
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 58 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ just drop one of them as dependency and voilà
client.send(request, BodyHandlers.ofJson(User.class));
```

And if special configuration required, SPI can be used
And if special configuration required
```java
package com.example;

Expand All @@ -152,9 +152,10 @@ class JacksonMappingProvider implements JsonMappingProvider {
}
}
```
then register this provider like
```shell
$ cat src/main/resources/META-INF/services/io.github.nstdio.http.ext.spi.JsonMappingProvider
then standard SPI registration can be used or custom provider can be registered manually:

com.example.JacksonMappingProvider
```java
JacksonMappingProvider jackson = ...

JsonMappingProvider.addProvider(jackson);
```

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

package io.github.nstdio.http.ext.spi;

import java.util.Optional;
import java.util.ServiceLoader;
import java.util.ServiceLoader.Provider;
import static java.util.Objects.requireNonNull;

/**
* The SPI to for {@linkplain JsonMapping}.
Expand All @@ -32,10 +30,7 @@ public interface JsonMappingProvider {
* @throws JsonMappingProviderNotFoundException When cannot find any provider.
*/
static JsonMappingProvider provider() {
return loader()
.findFirst()
.or(JsonMappingProvider::defaultProvider)
.orElseThrow(() -> new JsonMappingProviderNotFoundException("Cannot find any JsonMappingProvider."));
return JsonMappingProviders.provider();
}

/**
Expand All @@ -48,25 +43,25 @@ static JsonMappingProvider provider() {
* @throws JsonMappingProviderNotFoundException When requested provider is not found.
*/
static JsonMappingProvider provider(String name) {
return loader()
.stream()
.filter(provider -> provider.type().getName().equals(name))
.findFirst()
.map(Provider::get)
.or(() -> defaultProvider().filter(provider -> provider.getClass().getName().equals(name)))
.orElseThrow(() -> new JsonMappingProviderNotFoundException("JsonMappingProvider not found: " + name));
return JsonMappingProviders.provider(name);
}

private static Optional<JsonMappingProvider> defaultProvider() {
if (CompositeJsonMappingProvider.hasAnyImplementation()) {
return Optional.of(DefaultProviderHolder.DEFAULT_PROVIDER);
} else {
return Optional.empty();
}
/**
* Adds {@code provider}.
*
* @param provider The provider to add.
*/
static void addProvider(JsonMappingProvider provider) {
JsonMappingProviders.addProvider(requireNonNull(provider, "provider cannot be null"));
}

private static ServiceLoader<JsonMappingProvider> loader() {
return ServiceLoader.load(JsonMappingProvider.class);
/**
* Removes provider with name {@code name}.
*
* @param name The provider name to remove.
*/
static void removeProvider(String name) {
JsonMappingProviders.removeProvider(requireNonNull(name, "name cannot be null"));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2022 Edgar Asatryan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.nstdio.http.ext.spi;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.ServiceLoader.Provider;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Stream;

class JsonMappingProviders {
private static final List<Provider<JsonMappingProvider>> userProvided = new CopyOnWriteArrayList<>();

static void addProvider(JsonMappingProvider provider) {
userProvided.add(0, new InitializedProvider(provider));
}

static void removeProvider(String name) {
userProvided.removeIf(p -> p.type().getName().equals(name));
}

static void clear() {
userProvided.clear();
}

static JsonMappingProvider provider() {
return providerStream()
.findFirst()
.map(Provider::get)
.or(JsonMappingProviders::defaultProvider)
.orElseThrow(() -> new JsonMappingProviderNotFoundException("Cannot find any JsonMappingProvider."));
}

static JsonMappingProvider provider(String name) {
return providerStream()
.filter(provider -> provider.type().getName().equals(name))
.findFirst()
.map(Provider::get)
.or(() -> defaultProvider().filter(provider -> provider.getClass().getName().equals(name)))
.orElseThrow(() -> new JsonMappingProviderNotFoundException("JsonMappingProvider not found: " + name));
}

private static Stream<Provider<JsonMappingProvider>> providerStream() {
return Stream.concat(userProvided.stream(), loader().stream());
}

private static Optional<JsonMappingProvider> defaultProvider() {
if (CompositeJsonMappingProvider.hasAnyImplementation()) {
return Optional.of(DefaultProviderHolder.DEFAULT_PROVIDER);
} else {
return Optional.empty();
}
}

private static ServiceLoader<JsonMappingProvider> loader() {
return ServiceLoader.load(JsonMappingProvider.class);
}

private static class InitializedProvider implements Provider<JsonMappingProvider> {
private final JsonMappingProvider mappingProvider;

private InitializedProvider(JsonMappingProvider mappingProvider) {
this.mappingProvider = Objects.requireNonNull(mappingProvider);
}

@Override
public Class<? extends JsonMappingProvider> type() {
return mappingProvider.getClass();
}

@Override
public JsonMappingProvider get() {
return mappingProvider;
}
}

private static final class DefaultProviderHolder {
private static final CompositeJsonMappingProvider DEFAULT_PROVIDER = new CompositeJsonMappingProvider();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,58 @@
*/
package io.github.nstdio.http.ext.spi

import org.assertj.core.api.Assertions.assertThatExceptionOfType
import io.kotest.assertions.throwables.shouldThrowExactly
import io.kotest.matchers.throwable.shouldHaveMessage
import io.kotest.matchers.types.shouldBeSameInstanceAs
import io.kotest.matchers.types.shouldNotBeSameInstanceAs
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock

internal class JsonMappingProviderTest {
@Test
fun shouldThrowWhenProviderNotFound() {
//given
val providerName = "abc"

//when + then
assertThatExceptionOfType(RuntimeException::class.java)
.isThrownBy { JsonMappingProvider.provider(providerName) }
.withMessageEndingWith(providerName)
@AfterEach
fun `tear down`() {
JsonMappingProviders.clear()
}

@Test
fun shouldThrowWhenProviderNotFound() {
//given
val providerName = "abc"

//when + then
shouldThrowExactly<JsonMappingProviderNotFoundException> { JsonMappingProvider.provider(providerName) }
.shouldHaveMessage("JsonMappingProvider not found: $providerName")
}

@Test
fun `Should add provider and get it`() {
//given
val mockProvider = mock(JsonMappingProvider::class.java)
val providerName = mockProvider.javaClass.name

//when
JsonMappingProvider.addProvider(mockProvider)

//then
JsonMappingProvider.provider().shouldBeSameInstanceAs(mockProvider)
JsonMappingProvider.provider(providerName).shouldBeSameInstanceAs(mockProvider)
}

@Test
fun `Should remove provider`() {
//given
val mockProvider = mock(JsonMappingProvider::class.java)
val providerName = mockProvider.javaClass.name

//when
JsonMappingProvider.addProvider(mockProvider)
JsonMappingProvider.removeProvider(providerName)

//then
JsonMappingProvider.provider().shouldNotBeSameInstanceAs(mockProvider)
shouldThrowExactly<JsonMappingProviderNotFoundException> {
JsonMappingProvider.provider(providerName).shouldNotBeSameInstanceAs(mockProvider)
}
}
}

0 comments on commit 4975edd

Please sign in to comment.