Merge pull request #496 from jwtk/service-loader

Replace hardcoded class names and reflection with ServiceLoader

Fixes: #458
This commit is contained in:
Brian Demers 2019-10-24 14:35:50 -07:00 committed by GitHub
commit c21c30a025
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 532 additions and 445 deletions

View File

@ -0,0 +1,76 @@
/*
* Copyright (C) 2019 jsonwebtoken.io
*
* 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.jsonwebtoken.lang;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
/**
* Helper class for loading services from the classpath, using a {@link ServiceLoader}. Decouples loading logic for
* better separation of concerns and testability.
*/
public final class Services {
private Services() {}
/**
* Loads and instantiates all service implementation of the given SPI class and returns them as a List.
*
* @param spi The class of the Service Provider Interface
* @param <T> The type of the SPI
* @return An unmodifiable list with an instance of all available implementations of the SPI. No guarantee is given
* on the order of implementations, if more than one.
*/
public static <T> List<T> loadAll(Class<T> spi) {
Assert.notNull(spi, "Parameter 'spi' must not be null.");
ServiceLoader<T> serviceLoader = ServiceLoader.load(spi);
List<T> implementations = new ArrayList<>();
for (T implementation : serviceLoader) {
implementations.add(implementation);
}
// fail if no implementations were found
if (implementations.isEmpty()) {
throw new UnavailableImplementationException(spi);
}
return Collections.unmodifiableList(implementations);
}
/**
* Loads the first available implementation the given SPI class from the classpath. Uses the {@link ServiceLoader}
* to find implementations. When multiple implementations are available it will return the first one that it
* encounters. There is no guarantee with regard to ordering.
*
* @param spi The class of the Service Provider Interface
* @param <T> The type of the SPI
* @return A new instance of the service.
* @throws UnavailableImplementationException When no implementation the SPI is available on the classpath.
*/
public static <T> T loadFirst(Class<T> spi) {
Assert.notNull(spi, "Parameter 'spi' must not be null.");
ServiceLoader<T> serviceLoader = ServiceLoader.load(spi);
if (serviceLoader.iterator().hasNext()) {
return serviceLoader.iterator().next();
} else {
throw new UnavailableImplementationException(spi);
}
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2019 jsonwebtoken.io
*
* 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.jsonwebtoken.lang;
/**
* Exception indicating that no implementation of an jjwt-api SPI was found on the classpath.
* @since 0.11.0
*/
public final class UnavailableImplementationException extends RuntimeException {
private static final String DEFAULT_NOT_FOUND_MESSAGE = "Unable to find an implementation for %s using java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, for example jjwt-impl.jar, or your own .jar for custom implementations.";
UnavailableImplementationException(final Class klass) {
super(String.format(DEFAULT_NOT_FOUND_MESSAGE, klass));
}
}

View File

@ -48,16 +48,17 @@ public class UnknownClassException extends RuntimeException {
public UnknownClassException(Throwable cause) {
super(cause);
}
*/
/**
* Constructs a new UnknownClassException.
*
* @param message the reason for the exception
* @param cause the underlying Throwable that caused this exception to be thrown.
*
*/
public UnknownClassException(String message, Throwable cause) {
// TODO: remove in v1.0, this constructor is only exposed to allow for backward compatible behavior
super(message, cause);
}
*/
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
* Copyright (C) 2019 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -13,12 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
package io.jsonwebtoken
/**
* @since 0.10.0
*/
public interface InstanceLocator<T> {
T getInstance();
class DefaultStubService implements StubService {
}

View File

@ -0,0 +1,19 @@
/*
* Copyright (C) 2019 jsonwebtoken.io
*
* 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.jsonwebtoken
interface StubService {
}

View File

@ -0,0 +1,79 @@
/*
* Copyright (C) 2019 jsonwebtoken.io
*
* 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.jsonwebtoken.lang
import io.jsonwebtoken.DefaultStubService
import io.jsonwebtoken.StubService
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertNotNull
@RunWith(PowerMockRunner.class)
@PrepareForTest([Services])
class ServicesTest {
@Test
void testSuccessfulLoading() {
def factory = Services.loadFirst(StubService)
assertNotNull factory
assertEquals(DefaultStubService, factory.class)
}
@Test(expected = UnavailableImplementationException)
void testLoadFirstUnavailable() {
NoServicesClassLoader.runWith {
Services.loadFirst(StubService.class)
}
}
@Test(expected = UnavailableImplementationException)
void testLoadAllUnavailable() {
NoServicesClassLoader.runWith {
Services.loadAll(StubService.class)
}
}
static class NoServicesClassLoader extends ClassLoader {
private NoServicesClassLoader(ClassLoader parent) {
super(parent)
}
@Override
Enumeration<URL> getResources(String name) throws IOException {
if (name.startsWith("META-INF/services/")) {
return java.util.Collections.emptyEnumeration()
} else {
return super.getResources(name)
}
}
static void runWith(Closure closure) {
ClassLoader originalClassloader = Thread.currentThread().getContextClassLoader()
try {
Thread.currentThread().setContextClassLoader(new NoServicesClassLoader(originalClassloader))
closure.run()
} finally {
if (originalClassloader != null) {
Thread.currentThread().setContextClassLoader(originalClassloader)
}
}
}
}
}

View File

@ -0,0 +1 @@
io.jsonwebtoken.DefaultStubService

View File

@ -0,0 +1 @@
io.jsonwebtoken.gson.io.GsonDeserializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.gson.io.GsonSerializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.jackson.io.JacksonDeserializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.jackson.io.JacksonSerializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.orgjson.io.OrgJsonDeserializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.orgjson.io.OrgJsonSerializer

View File

@ -24,14 +24,13 @@ import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.crypto.DefaultJwtSigner;
import io.jsonwebtoken.impl.crypto.JwtSigner;
import io.jsonwebtoken.impl.io.InstanceLocator;
import io.jsonwebtoken.impl.lang.LegacyServices;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoder;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.io.SerializationException;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.InvalidKeyException;
@ -295,10 +294,10 @@ public class DefaultJwtBuilder implements JwtBuilder {
public String compact() {
if (this.serializer == null) {
//try to find one based on the runtime environment:
InstanceLocator<Serializer<Map<String,?>>> locator =
Classes.newInstance("io.jsonwebtoken.impl.io.RuntimeClasspathSerializerLocator");
this.serializer = locator.getInstance();
// try to find one based on the services available
// TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0
// use the previous commented out line instead
this.serializer = LegacyServices.loadFirst(Serializer.class);
}
if (payload == null && Collections.isEmpty(claims)) {

View File

@ -39,13 +39,12 @@ import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver;
import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator;
import io.jsonwebtoken.impl.crypto.JwtSignatureValidator;
import io.jsonwebtoken.impl.io.InstanceLocator;
import io.jsonwebtoken.impl.lang.LegacyServices;
import io.jsonwebtoken.io.Decoder;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.DateFormats;
import io.jsonwebtoken.lang.Objects;
import io.jsonwebtoken.lang.Strings;
@ -107,13 +106,6 @@ public class DefaultJwtParser implements JwtParser {
this.base64UrlDecoder = base64UrlDecoder;
this.deserializer = deserializer;
this.compressionCodecResolver = compressionCodecResolver;
if (this.deserializer == null) {
//try to find one based on the runtime environment:
InstanceLocator<Deserializer<Map<String, ?>>> locator =
Classes.newInstance("io.jsonwebtoken.impl.io.RuntimeClasspathDeserializerLocator");
this.deserializer = locator.getInstance();
}
}
@Override
@ -255,12 +247,12 @@ public class DefaultJwtParser implements JwtParser {
@Override
public Jwt parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException {
// TODO move this to constructor before 1.0
// TODO, this logic is only need for a now deprecated code path
// remove this block in v1.0 (the equivalent is already in DefaultJwtParserBuilder)
if (this.deserializer == null) {
//try to find one based on the runtime environment:
InstanceLocator<Deserializer<Map<String, ?>>> locator =
Classes.newInstance("io.jsonwebtoken.impl.io.RuntimeClasspathDeserializerLocator");
this.deserializer = locator.getInstance();
// try to find one based on the services available
// TODO: This util class will throw a UnavailableImplementationException here to retain behavior of previous version, remove in v1.0
this.deserializer = LegacyServices.loadFirst(Deserializer.class);
}
Assert.hasText(jwt, "JWT String argument cannot be null or empty.");

View File

@ -26,6 +26,8 @@ import io.jsonwebtoken.io.Decoder;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Services;
import java.security.Key;
import java.util.Date;
import java.util.Map;
@ -170,6 +172,14 @@ public class DefaultJwtParserBuilder implements JwtParserBuilder {
@Override
public JwtParser build() {
// Only lookup the deserializer IF it is null. It is possible a Deserializer implementation was set
// that is NOT exposed as a service and no other implementations are available for lookup.
if (this.deserializer == null) {
// try to find one based on the services available:
this.deserializer = Services.loadFirst(Deserializer.class);
}
return new ImmutableJwtParser(
new DefaultJwtParser(signingKeyResolver,
key,

View File

@ -17,11 +17,17 @@ package io.jsonwebtoken.impl.compression;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionCodecResolver;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.CompressionException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Services;
import io.jsonwebtoken.lang.Strings;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Default implementation of {@link CompressionCodecResolver} that supports the following:
* <p>
@ -45,6 +51,22 @@ import io.jsonwebtoken.lang.Strings;
*/
public class DefaultCompressionCodecResolver implements CompressionCodecResolver {
private static final String MISSING_COMPRESSION_MESSAGE = "Unable to find an implementation for compression algorithm [%s] using java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, for example jjwt-impl.jar, or your own .jar for custom implementations.";
private final Map<String, CompressionCodec> codecs;
public DefaultCompressionCodecResolver() {
Map<String, CompressionCodec> codecMap = new HashMap<>();
for (CompressionCodec codec : Services.loadAll(CompressionCodec.class)) {
codecMap.put(codec.getAlgorithmName().toUpperCase(), codec);
}
codecMap.put(CompressionCodecs.DEFLATE.getAlgorithmName().toUpperCase(), CompressionCodecs.DEFLATE);
codecMap.put(CompressionCodecs.GZIP.getAlgorithmName().toUpperCase(), CompressionCodecs.GZIP);
codecs = Collections.unmodifiableMap(codecMap);
}
@Override
public CompressionCodec resolveCompressionCodec(Header header) {
String cmpAlg = getAlgorithmFromHeader(header);
@ -54,14 +76,7 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver
if (!hasCompressionAlgorithm) {
return null;
}
if (io.jsonwebtoken.CompressionCodecs.DEFLATE.getAlgorithmName().equalsIgnoreCase(cmpAlg)) {
return io.jsonwebtoken.CompressionCodecs.DEFLATE;
}
if (io.jsonwebtoken.CompressionCodecs.GZIP.getAlgorithmName().equalsIgnoreCase(cmpAlg)) {
return io.jsonwebtoken.CompressionCodecs.GZIP;
}
throw new CompressionException("Unsupported compression algorithm '" + cmpAlg + "'");
return byName(cmpAlg);
}
private String getAlgorithmFromHeader(Header header) {
@ -69,4 +84,15 @@ public class DefaultCompressionCodecResolver implements CompressionCodecResolver
return header.getCompressionAlgorithm();
}
private CompressionCodec byName(String name) {
Assert.hasText(name, "'name' must not be empty");
CompressionCodec codec = codecs.get(name.toUpperCase());
if (codec == null) {
throw new CompressionException(String.format(MISSING_COMPRESSION_MESSAGE, name));
}
return codec;
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
*
* 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.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import java.util.concurrent.atomic.AtomicReference;
/**
* @since 0.10.0
*/
public class RuntimeClasspathDeserializerLocator<T> implements InstanceLocator<Deserializer<T>> {
private static final AtomicReference<Deserializer> DESERIALIZER = new AtomicReference<>();
@SuppressWarnings("unchecked")
@Override
public Deserializer<T> getInstance() {
Deserializer<T> deserializer = DESERIALIZER.get();
if (deserializer == null) {
deserializer = locate();
Assert.state(deserializer != null, "locate() cannot return null.");
if (!compareAndSet(deserializer)) {
deserializer = DESERIALIZER.get();
}
}
Assert.state(deserializer != null, "deserializer cannot be null.");
return deserializer;
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected Deserializer<T> locate() {
if (isAvailable("io.jsonwebtoken.jackson.io.JacksonDeserializer")) {
return Classes.newInstance("io.jsonwebtoken.jackson.io.JacksonDeserializer");
} else if (isAvailable("io.jsonwebtoken.orgjson.io.OrgJsonDeserializer")) {
return Classes.newInstance("io.jsonwebtoken.orgjson.io.OrgJsonDeserializer");
} else if (isAvailable("io.jsonwebtoken.gson.io.GsonDeserializer")) {
return Classes.newInstance("io.jsonwebtoken.gson.io.GsonDeserializer");
} else {
throw new IllegalStateException("Unable to discover any JSON Deserializer implementations on the classpath.");
}
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean compareAndSet(Deserializer<T> d) {
return DESERIALIZER.compareAndSet(null, d);
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean isAvailable(String fqcn) {
return Classes.isAvailable(fqcn);
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
*
* 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.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import java.util.concurrent.atomic.AtomicReference;
/**
* @since 0.10.0
*/
public class RuntimeClasspathSerializerLocator implements InstanceLocator<Serializer> {
private static final AtomicReference<Serializer<Object>> SERIALIZER = new AtomicReference<>();
@SuppressWarnings("unchecked")
@Override
public Serializer<Object> getInstance() {
Serializer<Object> serializer = SERIALIZER.get();
if (serializer == null) {
serializer = locate();
Assert.state(serializer != null, "locate() cannot return null.");
if (!compareAndSet(serializer)) {
serializer = SERIALIZER.get();
}
}
Assert.state(serializer != null, "serializer cannot be null.");
return serializer;
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected Serializer<Object> locate() {
if (isAvailable("io.jsonwebtoken.jackson.io.JacksonSerializer")) {
return Classes.newInstance("io.jsonwebtoken.jackson.io.JacksonSerializer");
} else if (isAvailable("io.jsonwebtoken.orgjson.io.OrgJsonSerializer")) {
return Classes.newInstance("io.jsonwebtoken.orgjson.io.OrgJsonSerializer");
} else if (isAvailable("io.jsonwebtoken.gson.io.GsonSerializer")) {
return Classes.newInstance("io.jsonwebtoken.gson.io.GsonSerializer");
} else {
throw new IllegalStateException("Unable to discover any JSON Serializer implementations on the classpath.");
}
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean compareAndSet(Serializer<Object> s) {
return SERIALIZER.compareAndSet(null, s);
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean isAvailable(String fqcn) {
return Classes.isAvailable(fqcn);
}
}

View File

@ -0,0 +1,28 @@
package io.jsonwebtoken.impl.lang;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Services;
import io.jsonwebtoken.lang.UnavailableImplementationException;
import io.jsonwebtoken.lang.UnknownClassException;
/**
* A backward compatibility {@link Services} utility to help migrate away from {@link Classes#newInstance(String)}.
* TODO: remove before v1.0
* @deprecated use {@link Services} directly
*/
@Deprecated
public final class LegacyServices {
/**
* Wraps {@code Services.loadFirst} and throws a {@link UnknownClassException} instead of a
* {@link UnavailableImplementationException} to retain the previous behavior. This method should be used when
* to retain the previous behavior of methods that throw an unchecked UnknownClassException.
*/
public static <T> T loadFirst(Class<T> spi) {
try {
return Services.loadFirst(spi);
} catch (UnavailableImplementationException e) {
throw new UnknownClassException(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,2 @@
io.jsonwebtoken.impl.compression.DeflateCompressionCodec
io.jsonwebtoken.impl.compression.GzipCompressionCodec

View File

@ -19,8 +19,9 @@ import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.DefaultJwsHeader
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver
import io.jsonwebtoken.impl.compression.GzipCompressionCodec
import io.jsonwebtoken.impl.io.RuntimeClasspathSerializerLocator
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.lang.Services
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.WeakKeyException
@ -43,7 +44,7 @@ class DeprecatedJwtsTest {
}
protected static String toJson(o) {
def serializer = new RuntimeClasspathSerializerLocator().getInstance()
def serializer = Services.loadFirst(Serializer)
byte[] bytes = serializer.serialize(o)
return new String(bytes, Strings.UTF_8)
}

View File

@ -19,8 +19,9 @@ import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.DefaultJwsHeader
import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver
import io.jsonwebtoken.impl.compression.GzipCompressionCodec
import io.jsonwebtoken.impl.io.RuntimeClasspathSerializerLocator
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.lang.Services
import io.jsonwebtoken.lang.Strings
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.WeakKeyException
@ -43,7 +44,7 @@ class JwtsTest {
}
protected static String toJson(o) {
def serializer = new RuntimeClasspathSerializerLocator().getInstance()
def serializer = Services.loadFirst(Serializer)
byte[] bytes = serializer.serialize(o)
return new String(bytes, Strings.UTF_8)
}

View File

@ -0,0 +1,78 @@
/*
* Copyright (C) 2019 jsonwebtoken.io
*
* 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.jsonwebtoken.impl.compression
import io.jsonwebtoken.CompressionCodec
import io.jsonwebtoken.CompressionException
import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.io.FakeServiceDescriptorClassLoader
import io.jsonwebtoken.lang.Services
import org.junit.Assert
import org.junit.Test
import io.jsonwebtoken.CompressionCodecs
import static org.hamcrest.CoreMatchers.hasItem
import static org.hamcrest.CoreMatchers.instanceOf
import static org.hamcrest.CoreMatchers.is
import static org.hamcrest.CoreMatchers.nullValue
import static org.hamcrest.MatcherAssert.assertThat
class DefaultCompressionCodecResolverTest {
@Test
void resolveHeaderTest() {
assertThat new DefaultCompressionCodecResolver().resolveCompressionCodec(
new DefaultHeader()), nullValue()
assertThat new DefaultCompressionCodecResolver().resolveCompressionCodec(
new DefaultHeader().setCompressionAlgorithm("def")), is(CompressionCodecs.DEFLATE)
assertThat new DefaultCompressionCodecResolver().resolveCompressionCodec(
new DefaultHeader().setCompressionAlgorithm("gzip")), is(CompressionCodecs.GZIP)
}
@Test
void invalidCompressionNameTest() {
try {
new DefaultCompressionCodecResolver().resolveCompressionCodec(
new DefaultHeader().setCompressionAlgorithm("expected-missing"))
Assert.fail("Expected CompressionException to be thrown")
} catch (CompressionException e) {
assertThat e.message, is(String.format(DefaultCompressionCodecResolver.MISSING_COMPRESSION_MESSAGE, "expected-missing"))
}
}
@Test
void overrideDefaultCompressionImplTest() {
FakeServiceDescriptorClassLoader.runWithFake "io.jsonwebtoken.io.compression.CompressionCodec.test.override", {
// first make sure the service loader actually resolves the test class
assertThat Services.loadAll(CompressionCodec), hasItem(instanceOf(YagCompressionCodec))
// now we know the class is loadable, make sure we ALWAYS return the GZIP impl
assertThat new DefaultCompressionCodecResolver().byName("gzip"), instanceOf(GzipCompressionCodec)
}
}
@Test
void emptyCompressionAlgInHeaderTest() {
try {
new DefaultCompressionCodecResolver().byName("")
Assert.fail("Expected IllegalArgumentException to be thrown")
} catch (IllegalArgumentException e) {
// expected
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (C) 2019 jsonwebtoken.io
*
* 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.jsonwebtoken.impl.compression
import io.jsonwebtoken.CompressionCodec
import io.jsonwebtoken.CompressionException
/**
* Yet Another GZIP CompressionCodec. This codec has the same name as the Official GZIP impl. The DefaultCompressionCodecResolver will NOT resolve this class.
*/
class YagCompressionCodec implements CompressionCodec {
@Override
String getAlgorithmName() {
return new GzipCompressionCodec().getAlgorithmName();
}
@Override
byte[] compress(byte[] payload) throws CompressionException {
return new byte[0]
}
@Override
byte[] decompress(byte[] compressed) throws CompressionException {
return new byte[0]
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 2019 jsonwebtoken.io
*
* 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.jsonwebtoken.impl.io
class FakeServiceDescriptorClassLoader extends ClassLoader {
private String serviceDescriptor
FakeServiceDescriptorClassLoader(ClassLoader parent, String serviceDescriptor) {
super(parent)
this.serviceDescriptor = serviceDescriptor
}
@Override
Enumeration<URL> getResources(String name) throws IOException {
if (name.startsWith("META-INF/services/")) {
return super.getResources(serviceDescriptor)
} else {
return super.getResources(name)
}
}
static void runWithFake(String fakeDescriptor, Closure closure) {
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader()
try {
Thread.currentThread().setContextClassLoader(new FakeServiceDescriptorClassLoader(originalClassLoader, fakeDescriptor))
closure.run()
} finally {
if(originalClassLoader != null) {
Thread.currentThread().setContextClassLoader(originalClassLoader)
}
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2019 jsonwebtoken.io
*
* 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.jsonwebtoken.impl.io
class NoServiceDescriptorClassLoader extends ClassLoader {
NoServiceDescriptorClassLoader(ClassLoader parent) {
super(parent)
}
@Override
Enumeration<URL> getResources(String name) throws IOException {
if (name.startsWith("META-INF/services/")) {
return Collections.emptyEnumeration()
} else {
return super.getResources(name)
}
}
static void runWith(Closure closure) {
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader()
try {
Thread.currentThread().setContextClassLoader(new NoServiceDescriptorClassLoader(originalClassLoader))
closure.run()
} finally {
if(originalClassLoader != null) {
Thread.currentThread().setContextClassLoader(originalClassLoader)
}
}
}
}

View File

@ -1,134 +0,0 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
*
* 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.jsonwebtoken.impl.io
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.jackson.io.JacksonDeserializer
import io.jsonwebtoken.orgjson.io.OrgJsonDeserializer
import io.jsonwebtoken.gson.io.GsonDeserializer
import org.junit.After
import org.junit.Before
import org.junit.Test
import static org.easymock.EasyMock.createMock
import static org.junit.Assert.*
class RuntimeClasspathDeserializerLocatorTest {
@Before
void setUp() {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(null)
}
@After
void teardown() {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(null)
}
@Test
void testClassIsNotAvailable() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
return false
}
}
try {
locator.getInstance()
} catch (Exception ex) {
assertEquals 'Unable to discover any JSON Deserializer implementations on the classpath.', ex.message
}
}
@Test
void testCompareAndSetFalse() {
Deserializer deserializer = createMock(Deserializer)
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean compareAndSet(Deserializer d) {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(deserializer)
return false
}
}
def returned = locator.getInstance()
assertSame deserializer, returned
}
@Test(expected = IllegalStateException)
void testLocateReturnsNull() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected Deserializer locate() {
return null
}
}
locator.getInstance()
}
@Test(expected = IllegalStateException)
void testCompareAndSetFalseWithNullReturn() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean compareAndSet(Deserializer d) {
return false
}
}
locator.getInstance()
}
@Test
void testJackson() {
def deserializer = new RuntimeClasspathDeserializerLocator().getInstance()
assertTrue deserializer instanceof JacksonDeserializer
}
@Test
void testOrgJson() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (JacksonDeserializer.class.getName().equals(fqcn)) {
return false; //skip it to allow the OrgJson impl to be created
}
return super.isAvailable(fqcn)
}
}
def deserializer = locator.getInstance()
assertTrue deserializer instanceof OrgJsonDeserializer
}
@Test
void testGson() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (JacksonDeserializer.class.getName().equals(fqcn)) {
return false; //skip it to allow the Gson impl to be created
}
if (OrgJsonDeserializer.class.getName().equals(fqcn)) {
return false; //skip it to allow the Gson impl to be created
}
return super.isAvailable(fqcn)
}
}
def deserializer = locator.getInstance()
assertTrue deserializer instanceof GsonDeserializer
}
}

View File

@ -1,133 +0,0 @@
/*
* Copyright (C) 2014 jsonwebtoken.io
*
* 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.jsonwebtoken.impl.io
import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.jackson.io.JacksonSerializer
import io.jsonwebtoken.orgjson.io.OrgJsonSerializer
import io.jsonwebtoken.gson.io.GsonSerializer
import org.junit.After
import org.junit.Before
import org.junit.Test
import static org.easymock.EasyMock.createMock
import static org.junit.Assert.*
class RuntimeClasspathSerializerLocatorTest {
@Before
void setUp() {
RuntimeClasspathSerializerLocator.SERIALIZER.set(null)
}
@After
void teardown() {
RuntimeClasspathSerializerLocator.SERIALIZER.set(null)
}
@Test
void testClassIsNotAvailable() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
return false
}
}
try {
locator.getInstance()
} catch (Exception ex) {
assertEquals 'Unable to discover any JSON Serializer implementations on the classpath.', ex.message
}
}
@Test
void testCompareAndSetFalse() {
Serializer serializer = createMock(Serializer)
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean compareAndSet(Serializer s) {
RuntimeClasspathSerializerLocator.SERIALIZER.set(serializer)
return false
}
}
def returned = locator.getInstance()
assertSame serializer, returned
}
@Test(expected = IllegalStateException)
void testLocateReturnsNull() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected Serializer<Object> locate() {
return null
}
}
locator.getInstance()
}
@Test(expected = IllegalStateException)
void testCompareAndSetFalseWithNullReturn() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean compareAndSet(Serializer<Object> s) {
return false
}
}
locator.getInstance()
}
@Test
void testJackson() {
def serializer = new RuntimeClasspathSerializerLocator().getInstance()
assertTrue serializer instanceof JacksonSerializer
}
@Test
void testOrgJson() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (JacksonSerializer.class.getName().equals(fqcn)) {
return false //skip it to allow the OrgJson impl to be created
}
return super.isAvailable(fqcn)
}
}
def serializer = locator.getInstance()
assertTrue serializer instanceof OrgJsonSerializer
}
@Test
void testGson() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (JacksonSerializer.class.getName().equals(fqcn)) {
return false //skip it to allow the Gson impl to be created
}
if (OrgJsonSerializer.class.getName().equals(fqcn)) {
return false //skip it to allow the Gson impl to be created
}
return super.isAvailable(fqcn)
}
}
def serializer = locator.getInstance()
assertTrue serializer instanceof GsonSerializer
}
}

View File

@ -0,0 +1,13 @@
package io.jsonwebtoken.impl.lang
import io.jsonwebtoken.lang.UnknownClassException
import org.junit.Test
class LegacyServicesTest {
@Test(expected = UnknownClassException)
void serviceNotFoundTest() {
// try to load a class that will NOT have any services, i.e. this test class.
LegacyServices.loadFirst(LegacyServicesTest)
}
}

View File

@ -0,0 +1 @@
io.jsonwebtoken.gson.io.GsonDeserializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.orgjson.io.OrgJsonDeserializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.gson.io.GsonSerializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.orgjson.io.OrgJsonSerializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.impl.compression.YagCompressionCodec