Reduce scope of Service Loader work to CompressionCodecs and JsonSerializers

Fixes: #458
This commit is contained in:
Brian Demers 2019-09-19 17:59:49 -04:00
parent ef32a1386d
commit 7037d64d24
59 changed files with 549 additions and 979 deletions

View File

@ -1,28 +0,0 @@
package io.jsonwebtoken;
/**
* Factory for {@link CompressionCodec} implementations. Backs the {@link io.jsonwebtoken.CompressionCodecs} constants
* class. Implementations of jjwt-api should provide their own implementation and make it available via a provider
* configuration file in META-INF/services.
*/
public interface CompressionCodecFactory {
/**
* Creates a new instance of a CompressionCodec implementing the <a href="https://tools.ietf.org/html/rfc7518">JWA</a>
* standard <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
*
* @return A new instance of a deflate CompressionCodec
*/
CompressionCodec deflateCodec();
/**
* Creates a new instance of a CompressionCodec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a>
* compression algorithm. * <h3>Compatibility Warning</h3> * <p><b>This is not a standard JWA compression
* algorithm</b>. Be sure to use this only when you are confident * that all parties accessing the token support
* the gzip algorithm.</p> * <p>If you're concerned about compatibility, the {@link #deflateCodec()} code is JWA
* standards-compliant.</p>
*
* @return A new instance of a gzip CompressionCodec
*/
CompressionCodec gzipCodec();
}

View File

@ -15,7 +15,7 @@
*/
package io.jsonwebtoken;
import io.jsonwebtoken.lang.Services;
import io.jsonwebtoken.lang.Classes;
/**
* Provides default implementations of the {@link CompressionCodec} interface.
@ -26,8 +26,6 @@ import io.jsonwebtoken.lang.Services;
*/
public final class CompressionCodecs {
private static final CompressionCodecFactory FACTORY = Services.loadFirst(CompressionCodecFactory.class);
private CompressionCodecs() {
} //prevent external instantiation
@ -35,7 +33,8 @@ public final class CompressionCodecs {
* Codec implementing the <a href="https://tools.ietf.org/html/rfc7518">JWA</a> standard
* <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
*/
public static final CompressionCodec DEFLATE = FACTORY.deflateCodec();
public static final CompressionCodec DEFLATE =
Classes.newInstance("io.jsonwebtoken.impl.compression.DeflateCompressionCodec");
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> compression algorithm.
@ -44,6 +43,7 @@ public final class CompressionCodecs {
* that all parties accessing the token support the gzip algorithm.</p>
* <p>If you're concerned about compatibility, the {@link #DEFLATE DEFLATE} code is JWA standards-compliant.</p>
*/
public static final CompressionCodec GZIP = FACTORY.gzipCodec();
public static final CompressionCodec GZIP =
Classes.newInstance("io.jsonwebtoken.impl.compression.GzipCompressionCodec");
}

View File

@ -1,80 +0,0 @@
package io.jsonwebtoken;
import java.util.Map;
/**
* Factory for creating instances of JWT interfaces. Backs the {@link io.jsonwebtoken.Jwts} factory class.
* Implementations of jjwt-api should provide their own implementation and make it available via a provider
* configuration file in META-INF/services.
*
* @since 0.1
*/
public interface JwtFactory {
/**
* Creates a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs. As this
* is a less common use of JWTs, consider using the {@link #jwsHeader()} factory method instead if you will later
* digitally sign the JWT.
*
* @return a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs.
*/
Header header();
/**
* Creates a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs, populated
* with the specified name/value pairs. As this is a less common use of JWTs, consider using the
* {@link #jwsHeader(java.util.Map)} factory method instead if you will later digitally sign the JWT.
*
* @return a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs.
*/
Header header(Map<String, Object> header);
/**
* Returns a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's).
*
* @return a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's).
* @see JwtBuilder#setHeader(Header)
*/
JwsHeader jwsHeader();
/**
* Returns a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's), populated with the
* specified name/value pairs.
*
* @return a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's), populated with the
* specified name/value pairs.
* @see JwtBuilder#setHeader(Header)
*/
JwsHeader jwsHeader(Map<String, Object> header);
/**
* Returns a new {@link Claims} instance to be used as a JWT body.
*
* @return a new {@link Claims} instance to be used as a JWT body.
*/
Claims claims();
/**
* Returns a new {@link Claims} instance populated with the specified name/value pairs.
*
* @param claims the name/value pairs to populate the new Claims instance.
* @return a new {@link Claims} instance populated with the specified name/value pairs.
*/
Claims claims(Map<String, Object> claims);
/**
* Returns a new {@link JwtParser} instance that can be configured and then used to parse JWT strings.
*
* @return a new {@link JwtParser} instance that can be configured and then used to parse JWT strings.
*/
JwtParser parser();
/**
* Returns a new {@link JwtBuilder} instance that can be configured and then used to create JWT compact serialized
* strings.
*
* @return a new {@link JwtBuilder} instance that can be configured and then used to create JWT compact serialized
* strings.
*/
JwtBuilder builder();
}

View File

@ -15,7 +15,7 @@
*/
package io.jsonwebtoken;
import io.jsonwebtoken.lang.Services;
import io.jsonwebtoken.lang.Classes;
import java.util.Map;
@ -23,11 +23,11 @@ import java.util.Map;
* Factory class useful for creating instances of JWT interfaces. Using this factory class can be a good
* alternative to tightly coupling your code to implementation classes.
*
* @since 0.11
* @since 0.1
*/
public final class Jwts {
private static final JwtFactory FACTORY = Services.loadFirst(JwtFactory.class);
private static final Class[] MAP_ARG = new Class[]{Map.class};
private Jwts() {
}
@ -40,7 +40,7 @@ public final class Jwts {
* @return a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs.
*/
public static Header header() {
return FACTORY.header();
return Classes.newInstance("io.jsonwebtoken.impl.DefaultHeader");
}
/**
@ -51,7 +51,7 @@ public final class Jwts {
* @return a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs.
*/
public static Header header(Map<String, Object> header) {
return FACTORY.header(header);
return Classes.newInstance("io.jsonwebtoken.impl.DefaultHeader", MAP_ARG, header);
}
/**
@ -61,7 +61,7 @@ public final class Jwts {
* @see JwtBuilder#setHeader(Header)
*/
public static JwsHeader jwsHeader() {
return FACTORY.jwsHeader();
return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwsHeader");
}
/**
@ -73,7 +73,7 @@ public final class Jwts {
* @see JwtBuilder#setHeader(Header)
*/
public static JwsHeader jwsHeader(Map<String, Object> header) {
return FACTORY.jwsHeader(header);
return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwsHeader", MAP_ARG, header);
}
/**
@ -82,7 +82,7 @@ public final class Jwts {
* @return a new {@link Claims} instance to be used as a JWT body.
*/
public static Claims claims() {
return FACTORY.claims();
return Classes.newInstance("io.jsonwebtoken.impl.DefaultClaims");
}
/**
@ -92,7 +92,7 @@ public final class Jwts {
* @return a new {@link Claims} instance populated with the specified name/value pairs.
*/
public static Claims claims(Map<String, Object> claims) {
return FACTORY.claims(claims);
return Classes.newInstance("io.jsonwebtoken.impl.DefaultClaims", MAP_ARG, claims);
}
/**
@ -118,7 +118,7 @@ public final class Jwts {
*/
@Deprecated
public static JwtParser parser() {
return FACTORY.parser();
return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwtParser");
}
/**
@ -138,6 +138,6 @@ public final class Jwts {
* strings.
*/
public static JwtBuilder builder() {
return FACTORY.builder();
return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwtBuilder");
}
}

View File

@ -1,13 +0,0 @@
package io.jsonwebtoken.lang;
import io.jsonwebtoken.JwtException;
/**
* Exception indicating that no implementation of an jjwt-api SPI was found on the classpath.
*/
public class ImplementationNotFoundException extends JwtException {
ImplementationNotFoundException(final String message) {
super(message);
}
}

View File

@ -1,3 +1,18 @@
/*
* 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;
@ -11,6 +26,8 @@ import java.util.ServiceLoader;
*/
public final class Services {
private Services() {}
/**
* Loads and instantiates all service implementation of the given SPI class and returns them as a List.
*
@ -19,14 +36,21 @@ public final class Services {
* @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> loadAllAvailableImplementations(Class<T> spi) {
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);
}
@ -38,14 +62,15 @@ public final class Services {
* @param spi The class of the Service Provider Interface
* @param <T> The type of the SPI
* @return A new instance of the service.
* @throws ImplementationNotFoundException When no implementation the SPI is available on the classpath.
* @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 ImplementationNotFoundException("No implementation of " + spi.getName() + " found on the classpath. Make sure to include an implementation of jjwt-api.");
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,19 +0,0 @@
package io.jsonwebtoken.security;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
public interface KeyGenerator {
boolean supports(SignatureAlgorithm alg);
/**
* Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures
* according to the specified {@code SignatureAlgorithm}.
*
* @param alg the desired signature algorithm
* @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according
* to the specified {@code SignatureAlgorithm}.
*/
SecretKey generateKey(SignatureAlgorithm alg);
}

View File

@ -1,19 +0,0 @@
package io.jsonwebtoken.security;
import io.jsonwebtoken.SignatureAlgorithm;
import java.security.KeyPair;
public interface KeyPairGenerator {
boolean supports(SignatureAlgorithm alg);
/**
* Generates a new secure-random key pair of sufficient strength for the specified {@link SignatureAlgorithm} using
* JJWT's default SecureRandom instance.
*
* @param alg the algorithm indicating strength
* @return a new secure-randomly generated key pair of sufficient strength for the specified {@link
* SignatureAlgorithm}.
*/
KeyPair generateKeyPair(SignatureAlgorithm alg);
}

View File

@ -17,7 +17,7 @@ package io.jsonwebtoken.security;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Services;
import io.jsonwebtoken.lang.Classes;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@ -32,8 +32,12 @@ import java.util.List;
* @since 0.10.0
*/
public final class Keys {
private static final List<KeyGenerator> KEY_GENERATORS = Services.loadAllAvailableImplementations(KeyGenerator.class);
private static final List<KeyPairGenerator> KEY_PAIR_GENERATORS = Services.loadAllAvailableImplementations(KeyPairGenerator.class);
private static final String MAC = "io.jsonwebtoken.impl.crypto.MacProvider";
private static final String RSA = "io.jsonwebtoken.impl.crypto.RsaProvider";
private static final String EC = "io.jsonwebtoken.impl.crypto.EllipticCurveProvider";
private static final Class[] SIG_ARG_TYPES = new Class[]{SignatureAlgorithm.class};
//purposefully ordered higher to lower:
private static final List<SignatureAlgorithm> PREFERRED_HMAC_ALGS = Collections.unmodifiableList(Arrays.asList(
@ -125,14 +129,15 @@ public final class Keys {
*/
public static SecretKey secretKeyFor(SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
for (KeyGenerator keyGenerator : KEY_GENERATORS) {
if (keyGenerator.supports(alg)) {
return keyGenerator.generateKey(alg);
}
switch (alg) {
case HS256:
case HS384:
case HS512:
return Classes.invokeStatic(MAC, "generateKey", SIG_ARG_TYPES, alg);
default:
String msg = "The " + alg.name() + " algorithm does not support shared secret keys.";
throw new IllegalArgumentException(msg);
}
String msg = "The " + alg.name() + " algorithm does not support shared secret keys.";
throw new IllegalArgumentException(msg);
}
/**
@ -206,14 +211,21 @@ public final class Keys {
*/
public static KeyPair keyPairFor(SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
for (KeyPairGenerator keyPairGenerator : KEY_PAIR_GENERATORS) {
if (keyPairGenerator.supports(alg)) {
return keyPairGenerator.generateKeyPair(alg);
}
switch (alg) {
case RS256:
case PS256:
case RS384:
case PS384:
case RS512:
case PS512:
return Classes.invokeStatic(RSA, "generateKeyPair", SIG_ARG_TYPES, alg);
case ES256:
case ES384:
case ES512:
return Classes.invokeStatic(EC, "generateKeyPair", SIG_ARG_TYPES, alg);
default:
String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
throw new IllegalArgumentException(msg);
}
String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
throw new IllegalArgumentException(msg);
}
}

View File

@ -15,41 +15,41 @@
*/
package io.jsonwebtoken
import io.jsonwebtoken.lang.Services
import io.jsonwebtoken.lang.Classes
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.easymock.EasyMock.createMock
import static org.easymock.EasyMock.eq
import static org.easymock.EasyMock.expect
import static org.junit.Assert.assertSame
import static org.powermock.api.easymock.PowerMock.*
import static org.powermock.api.easymock.PowerMock.mockStatic
import static org.powermock.api.easymock.PowerMock.replay
import static org.powermock.api.easymock.PowerMock.verify
@RunWith(PowerMockRunner.class)
@PrepareForTest([Services, CompressionCodecs])
@PrepareForTest([Classes, CompressionCodecs])
class CompressionCodecsTest {
@Test
void testStatics() {
mockStatic(Services)
def factory = createMock(CompressionCodecFactory)
expect(Services.loadFirst(CompressionCodecFactory)).andReturn(factory)
mockStatic(Classes)
def deflate = createMock(CompressionCodec)
def gzip = createMock(CompressionCodec)
expect(factory.deflateCodec()).andReturn(deflate)
expect(factory.gzipCodec()).andReturn(gzip)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.compression.DeflateCompressionCodec"))).andReturn(deflate)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.compression.GzipCompressionCodec"))).andReturn(gzip)
replay Services, factory, deflate, gzip
replay Classes, deflate, gzip
assertSame deflate, CompressionCodecs.DEFLATE
assertSame gzip, CompressionCodecs.GZIP
verify Services, factory, deflate, gzip
verify Classes, deflate, gzip
//test coverage for private constructor:
new CompressionCodecs()

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

@ -15,44 +15,26 @@
*/
package io.jsonwebtoken
import io.jsonwebtoken.lang.Services
import org.junit.Before
import org.junit.BeforeClass
import io.jsonwebtoken.lang.Classes
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.easymock.EasyMock.createMock
import static org.easymock.EasyMock.eq
import static org.easymock.EasyMock.expect
import static org.easymock.EasyMock.mock
import static org.easymock.EasyMock.reset
import static org.easymock.EasyMock.same
import static org.junit.Assert.assertSame
import static org.powermock.api.easymock.PowerMock.createMock
import static org.powermock.api.easymock.PowerMock.mockStatic
import static org.powermock.api.easymock.PowerMock.replay
import static org.powermock.api.easymock.PowerMock.reset
import static org.powermock.api.easymock.PowerMock.verify
@RunWith(PowerMockRunner.class)
@PrepareForTest([Services])
@PrepareForTest([Classes, Jwts])
class JwtsTest {
static JwtFactory factory = mock(JwtFactory)
@BeforeClass
static void prepareFactory() {
mockStatic(Services)
expect(Services.loadFirst(JwtFactory)).andReturn(factory).anyTimes()
replay Services
}
@Before
void resetFactoryMock() {
reset(factory)
}
@Test
void testPrivateCtor() { //for code coverage only
new Jwts()
@ -61,118 +43,146 @@ class JwtsTest {
@Test
void testHeader() {
mockStatic(Classes)
def instance = createMock(Header)
expect(factory.header()).andReturn(instance)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultHeader"))).andReturn(instance)
replay factory, instance
replay Classes, instance
assertSame instance, Jwts.header()
verify factory, instance
verify Classes, instance
}
@Test
void testHeaderFromMap() {
mockStatic(Classes)
def map = [:]
def instance = createMock(Header)
expect(factory.header(same(map) as Map<String, Object>)).andReturn(instance)
expect(Classes.newInstance(
eq("io.jsonwebtoken.impl.DefaultHeader"),
same(Jwts.MAP_ARG),
same(map))
).andReturn(instance)
replay factory, instance
replay Classes, instance
assertSame instance, Jwts.header(map)
verify factory, instance
verify Classes, instance
}
@Test
void testJwsHeader() {
mockStatic(Classes)
def instance = createMock(JwsHeader)
expect(factory.jwsHeader()).andReturn(instance)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwsHeader"))).andReturn(instance)
replay factory, instance
replay Classes, instance
assertSame instance, Jwts.jwsHeader()
verify factory, instance
verify Classes, instance
}
@Test
void testJwsHeaderFromMap() {
mockStatic(Classes)
def map = [:]
def instance = createMock(JwsHeader)
expect(factory.jwsHeader(same(map) as Map<String, Object>)).andReturn(instance)
expect(Classes.newInstance(
eq("io.jsonwebtoken.impl.DefaultJwsHeader"),
same(Jwts.MAP_ARG),
same(map))
).andReturn(instance)
replay factory, instance
replay Classes, instance
assertSame instance, Jwts.jwsHeader(map)
verify factory, instance
verify Classes, instance
}
@Test
void testClaims() {
mockStatic(Classes)
def instance = createMock(Claims)
expect(factory.claims()).andReturn(instance)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultClaims"))).andReturn(instance)
replay factory, instance
replay Classes, instance
assertSame instance, Jwts.claims()
verify factory, instance
verify Classes, instance
}
@Test
void testClaimsFromMap() {
mockStatic(Classes)
def map = [:]
def instance = createMock(Claims)
expect(factory.claims(same(map) as Map<String, Object>)).andReturn(instance)
expect(Classes.newInstance(
eq("io.jsonwebtoken.impl.DefaultClaims"),
same(Jwts.MAP_ARG),
same(map))
).andReturn(instance)
replay factory, instance
replay Classes, instance
assertSame instance, Jwts.claims(map)
verify factory, instance
verify Classes, instance
}
@Test
void testParser() {
mockStatic(Classes)
def instance = createMock(JwtParser)
expect(factory.parser()).andReturn(instance)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwtParser"))).andReturn(instance)
replay factory, instance
replay Classes, instance
assertSame instance, Jwts.parser()
verify factory, instance
verify Classes, instance
}
@Test
void testBuilder() {
mockStatic(Classes)
def instance = createMock(JwtBuilder)
expect(factory.builder()).andReturn(instance)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwtBuilder"))).andReturn(instance)
replay factory, instance
replay Classes, instance
assertSame instance, Jwts.builder()
verify factory, instance
verify Classes, instance
}
}

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

@ -1,43 +0,0 @@
package io.jsonwebtoken
class TestJwtFactory implements JwtFactory {
@Override
Header header() {
return null
}
@Override
Header header(final Map<String, Object> header) {
return null
}
@Override
JwsHeader jwsHeader() {
return null
}
@Override
JwsHeader jwsHeader(final Map<String, Object> header) {
return null
}
@Override
Claims claims() {
return null
}
@Override
Claims claims(final Map<String, Object> claim) {
return null
}
@Override
JwtParser parser() {
return null
}
@Override
JwtBuilder builder() {
return null
}
}

View File

@ -1,12 +1,28 @@
/*
* 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.JwtFactory
import io.jsonwebtoken.TestJwtFactory
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)
@ -15,20 +31,23 @@ class ServicesTest {
@Test
void testSuccessfulLoading() {
def factory = Services.loadFirst(JwtFactory.class)
def factory = Services.loadFirst(StubService)
assertNotNull factory
org.junit.Assert.assertEquals(TestJwtFactory, factory.class)
assertEquals(DefaultStubService, factory.class)
}
@Test(expected = ImplementationNotFoundException)
void testFailedLoading() {
ClassLoader cl = Thread.currentThread().getContextClassLoader()
@Test(expected = UnavailableImplementationException)
void testLoadFirstUnavailable() {
NoServicesClassLoader.runWith {
Services.loadFirst(StubService.class)
}
}
Thread.currentThread().setContextClassLoader(new NoServicesClassLoader(cl))
Services.loadFirst(JwtFactory.class)
@Test(expected = UnavailableImplementationException)
void testLoadAllUnavailable() {
NoServicesClassLoader.runWith {
Services.loadAll(StubService.class)
}
}
static class NoServicesClassLoader extends ClassLoader {
@ -44,5 +63,17 @@ class ServicesTest {
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

@ -16,9 +16,7 @@
package io.jsonwebtoken.security
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.lang.Services
import org.junit.Before
import org.junit.BeforeClass
import io.jsonwebtoken.lang.Classes
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
@ -27,8 +25,8 @@ import org.powermock.modules.junit4.PowerMockRunner
import javax.crypto.SecretKey
import java.security.KeyPair
import static org.easymock.EasyMock.eq
import static org.easymock.EasyMock.expect
import static org.easymock.EasyMock.mock
import static org.easymock.EasyMock.same
import static org.junit.Assert.*
import static org.powermock.api.easymock.PowerMock.*
@ -39,28 +37,9 @@ import static org.powermock.api.easymock.PowerMock.*
* The actual implementation assertions are done in KeysImplTest in the impl module.
*/
@RunWith(PowerMockRunner)
@PrepareForTest([Keys, Services])
@PrepareForTest([Classes, Keys])
class KeysTest {
static KeyGenerator keyGenerator = mock(KeyGenerator)
static KeyPairGenerator keyPairGenerator = mock(KeyPairGenerator)
@BeforeClass
static void prepareServices() {
mockStatic(Services)
expect(Services.loadAllAvailableImplementations(KeyGenerator)).andReturn([keyGenerator]).anyTimes()
expect(Services.loadAllAvailableImplementations(KeyPairGenerator)).andReturn([keyPairGenerator]).anyTimes()
replay Services
}
@Before
void reset() {
reset keyGenerator
reset keyPairGenerator
}
@Test
void testPrivateCtor() { //for code coverage only
new Keys()
@ -101,30 +80,27 @@ class KeysTest {
if (name.startsWith('H')) {
def key = createMock(SecretKey)
expect(keyGenerator.supports(same(alg))).andReturn(true)
expect(keyGenerator.generateKey(same(alg))).andReturn(key)
mockStatic(Classes)
replay keyGenerator, key
def key = createMock(SecretKey)
expect(Classes.invokeStatic(eq(Keys.MAC), eq("generateKey"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(key)
replay Classes, key
assertSame key, Keys.secretKeyFor(alg)
verify keyGenerator, key
verify Classes, key
reset keyGenerator, key
reset Classes, key
} else {
expect(keyGenerator.supports(same(alg))).andReturn(false)
replay(keyGenerator)
try {
Keys.secretKeyFor(alg)
fail()
} catch (IllegalArgumentException expected) {
assertEquals "The $name algorithm does not support shared secret keys." as String, expected.message
reset keyGenerator
}
}
}
@ -138,29 +114,27 @@ class KeysTest {
String name = alg.name()
if (name.equals('NONE') || name.startsWith('H')) {
expect(keyPairGenerator.supports(alg)).andReturn(false)
replay keyPairGenerator
try {
Keys.keyPairFor(alg)
fail()
} catch (IllegalArgumentException expected) {
assertEquals "The $name algorithm does not support Key Pairs." as String, expected.message
reset keyPairGenerator
}
} else {
def pair = createMock(KeyPair)
expect(keyPairGenerator.supports(same(alg))).andReturn(true)
expect(keyPairGenerator.generateKeyPair(same(alg))).andReturn(pair)
String fqcn = name.startsWith('E') ? Keys.EC : Keys.RSA
replay keyPairGenerator, pair
mockStatic Classes
def pair = createMock(KeyPair)
expect(Classes.invokeStatic(eq(fqcn), eq("generateKeyPair"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(pair)
replay Classes, pair
assertSame pair, Keys.keyPairFor(alg)
verify keyPairGenerator, pair
verify Classes, pair
reset keyPairGenerator, pair
reset Classes, pair
}
}
}

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionCodecFactory;
import io.jsonwebtoken.impl.compression.DeflateCompressionCodec;
import io.jsonwebtoken.impl.compression.GzipCompressionCodec;
public class DefaultCompressionCodecFactory implements CompressionCodecFactory {
@Override
public CompressionCodec deflateCodec() {
return new DeflateCompressionCodec();
}
@Override
public CompressionCodec gzipCodec() {
return new GzipCompressionCodec();
}
}

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

@ -1,58 +0,0 @@
package io.jsonwebtoken.impl;
import java.util.Map;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtFactory;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.impl.DefaultClaims;
import io.jsonwebtoken.impl.DefaultHeader;
import io.jsonwebtoken.impl.DefaultJwsHeader;
import io.jsonwebtoken.impl.DefaultJwtBuilder;
import io.jsonwebtoken.impl.DefaultJwtParser;
public class DefaultJwtFactory implements JwtFactory {
@Override
public Header header() {
return new DefaultHeader();
}
@Override
public Header header(final Map<String, Object> map) {
return new DefaultHeader(map);
}
@Override
public JwsHeader jwsHeader() {
return new DefaultJwsHeader();
}
@Override
public JwsHeader jwsHeader(final Map<String, Object> header) {
return new DefaultJwsHeader(header);
}
@Override
public Claims claims() {
return new DefaultClaims();
}
@Override
public Claims claims(final Map<String, Object> claims) {
return new DefaultClaims(claims);
}
@Override
public JwtBuilder builder() {
return new DefaultJwtBuilder();
}
@Override
public JwtParser parser() {
return new DefaultJwtParser();
}
}

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,18 +0,0 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.KeyPairGenerator;
import java.security.KeyPair;
public class EllipticCurveKeyPairGenerator implements KeyPairGenerator {
@Override
public boolean supports(SignatureAlgorithm alg) {
return alg.isEllipticCurve();
}
@Override
public KeyPair generateKeyPair(SignatureAlgorithm alg) {
return EllipticCurveProvider.generateKeyPair(alg);
}
}

View File

@ -1,19 +0,0 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.KeyGenerator;
import javax.crypto.SecretKey;
public final class MacKeyGenerator implements KeyGenerator {
@Override
public boolean supports(SignatureAlgorithm alg) {
return alg.isHmac();
}
@Override
public SecretKey generateKey(SignatureAlgorithm alg) {
return MacProvider.generateKey(alg);
}
}

View File

@ -1,18 +0,0 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.KeyPairGenerator;
import java.security.KeyPair;
public class RsaKeyPairGenerator implements KeyPairGenerator {
@Override
public boolean supports(SignatureAlgorithm alg) {
return alg.isRsa();
}
@Override
public KeyPair generateKeyPair(SignatureAlgorithm alg) {
return RsaProvider.generateKeyPair(alg);
}
}

View File

@ -132,6 +132,7 @@ public abstract class RsaProvider extends SignatureProvider {
* @see #generateKeyPair(String, int, SecureRandom)
* @since 0.10.0
*/
@SuppressWarnings("unused") //used by io.jsonwebtoken.security.Keys
public static KeyPair generateKeyPair(SignatureAlgorithm alg) {
Assert.isTrue(alg.isRsa(), "Only RSA algorithms are supported by this method.");
int keySizeInBits = 4096;

View File

@ -1,62 +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 java.util.Iterator;
import java.util.ServiceLoader;
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() {
ServiceLoader<Deserializer> serviceLoader = ServiceLoader.load(Deserializer.class);
Iterator<Deserializer> iterator = serviceLoader.iterator();
if(iterator.hasNext()) {
return (Deserializer<T>)iterator.next();
} 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);
}
}

View File

@ -1,62 +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 java.util.Iterator;
import java.util.ServiceLoader;
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({"unchecked", "WeakerAccess"}) //to allow testing override
protected Serializer<Object> locate() {
ServiceLoader<Serializer> serviceLoader = ServiceLoader.load(Serializer.class);
Iterator<Serializer> iterator = serviceLoader.iterator();
if(iterator.hasNext()) {
return (Serializer<Object>)iterator.next();
} 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);
}
}

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

@ -1 +0,0 @@
io.jsonwebtoken.impl.DefaultCompressionCodecFactory

View File

@ -1 +0,0 @@
io.jsonwebtoken.impl.DefaultJwtFactory

View File

@ -1 +0,0 @@
io.jsonwebtoken.impl.crypto.MacKeyGenerator

View File

@ -1,2 +0,0 @@
io.jsonwebtoken.impl.crypto.RsaKeyPairGenerator
io.jsonwebtoken.impl.crypto.EllipticCurveKeyPairGenerator

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

@ -1,26 +0,0 @@
package io.jsonwebtoken.impl
import io.jsonwebtoken.impl.DefaultCompressionCodecFactory
import io.jsonwebtoken.impl.compression.DeflateCompressionCodec
import io.jsonwebtoken.impl.compression.GzipCompressionCodec
import org.junit.Test
import static org.junit.Assert.assertEquals
class DefaultCompressionCodecFactoryTest {
@Test
void testCreateDeflateCodec() {
def deflate = new DefaultCompressionCodecFactory().deflateCodec()
assertEquals(DeflateCompressionCodec, deflate.class)
}
@Test
void testCreateGzipCodec() {
def gzipCodec = new DefaultCompressionCodecFactory().gzipCodec()
assertEquals(GzipCompressionCodec, gzipCodec.class)
}
}

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

@ -1,3 +1,18 @@
/*
* 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 {
@ -16,4 +31,16 @@ class FakeServiceDescriptorClassLoader extends ClassLoader {
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

@ -1,3 +1,18 @@
/*
* 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 {
@ -13,4 +28,16 @@ class NoServiceDescriptorClassLoader extends ClassLoader {
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,144 +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 {
private static final String TEST_SERVICE_DESCRIPTOR = "io.jsonwebtoken.io.Deserializer.test.orgjson"
private ClassLoader originalClassLoader
@Before
void setUp() {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(null)
}
@After
void teardown() {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(null)
restoreOriginalClassLoader()
}
private void restoreOriginalClassLoader() {
if(originalClassLoader != null) {
Thread.currentThread().setContextClassLoader(originalClassLoader)
originalClassLoader = null
}
}
@Test
void testClassIsNotAvailable() {
prepareNoServiceDescriptorClassLoader()
try {
new RuntimeClasspathDeserializerLocator().getInstance()
fail 'Located Deserializer class, whereas none was expected.'
} 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() {
prepareFakeServiceClassLoader()
def deserializer = new RuntimeClasspathDeserializerLocator().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
}
private void prepareNoServiceDescriptorClassLoader() {
originalClassLoader = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new NoServiceDescriptorClassLoader(originalClassLoader))
}
private void prepareFakeServiceClassLoader() {
originalClassLoader = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new FakeServiceDescriptorClassLoader(originalClassLoader, TEST_SERVICE_DESCRIPTOR))
}
}

View File

@ -1,144 +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 {
private static final String TEST_SERVICE_DESCRIPTOR = "io.jsonwebtoken.io.Serializer.test.orgjson"
private ClassLoader originalClassLoader
@Before
void setUp() {
RuntimeClasspathSerializerLocator.SERIALIZER.set(null)
}
@After
void teardown() {
RuntimeClasspathSerializerLocator.SERIALIZER.set(null)
restoreOriginalClassLoader()
}
private void restoreOriginalClassLoader() {
if(originalClassLoader != null) {
Thread.currentThread().setContextClassLoader(originalClassLoader)
originalClassLoader = null
}
}
@Test
void testClassIsNotAvailable() {
prepareNoServiceDescriptorClassLoader()
try {
new RuntimeClasspathSerializerLocator().getInstance()
fail 'Located Deserializer class, whereas none was expected.'
} 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() {
prepareFakeServiceClassLoader()
def serializer = new RuntimeClasspathSerializerLocator().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
}
private void prepareNoServiceDescriptorClassLoader() {
originalClassLoader = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new NoServiceDescriptorClassLoader(originalClassLoader))
}
private void prepareFakeServiceClassLoader() {
originalClassLoader = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new FakeServiceDescriptorClassLoader(originalClassLoader, TEST_SERVICE_DESCRIPTOR))
}
}

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

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

View File

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

View File

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

View File

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