From 75f1182d171be5c4f6a1cc6b9df0dfe7cba75f49 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Thu, 18 Sep 2014 19:14:22 -0700 Subject: [PATCH] Initial commit! --- .gitignore | 5 + README.md | 59 +- pom.xml | 246 ++++ src/main/java/io/jsonwebtoken/Claims.java | 59 + src/main/java/io/jsonwebtoken/Header.java | 39 + src/main/java/io/jsonwebtoken/JWTs.java | 44 + src/main/java/io/jsonwebtoken/JwtBuilder.java | 48 + .../java/io/jsonwebtoken/JwtException.java | 27 + src/main/java/io/jsonwebtoken/JwtParser.java | 33 + .../jsonwebtoken/MalformedJwtException.java | 27 + .../io/jsonwebtoken/SignatureAlgorithm.java | 89 ++ .../io/jsonwebtoken/SignatureException.java | 27 + .../java/io/jsonwebtoken/SignedToken.java | 21 + src/main/java/io/jsonwebtoken/Token.java | 27 + .../jsonwebtoken/impl/AbstractTextCodec.java | 39 + .../io/jsonwebtoken/impl/Base64Codec.java | 32 + .../io/jsonwebtoken/impl/Base64UrlCodec.java | 97 ++ .../io/jsonwebtoken/impl/DefaultClaims.java | 109 ++ .../io/jsonwebtoken/impl/DefaultHeader.java | 63 + .../jsonwebtoken/impl/DefaultJwtBuilder.java | 192 +++ .../jsonwebtoken/impl/DefaultJwtParser.java | 218 ++++ .../io/jsonwebtoken/impl/DefaultToken.java | 58 + .../java/io/jsonwebtoken/impl/JwtMap.java | 151 +++ .../java/io/jsonwebtoken/impl/TextCodec.java | 30 + .../impl/crypto/AbstractSigner.java | 34 + .../crypto/DefaultJwtSignatureValidator.java | 49 + .../impl/crypto/DefaultJwtSigner.java | 49 + .../DefaultSignatureValidatorFactory.java | 56 + .../impl/crypto/DefaultSignerFactory.java | 56 + .../impl/crypto/JwtSignatureValidator.java | 21 + .../jsonwebtoken/impl/crypto/JwtSigner.java | 21 + .../jsonwebtoken/impl/crypto/MacProvider.java | 29 + .../jsonwebtoken/impl/crypto/MacSigner.java | 60 + .../impl/crypto/MacValidator.java | 36 + .../jsonwebtoken/impl/crypto/RsaProvider.java | 92 ++ .../impl/crypto/RsaSignatureValidator.java | 56 + .../jsonwebtoken/impl/crypto/RsaSigner.java | 54 + .../impl/crypto/SignatureProvider.java | 34 + .../impl/crypto/SignatureValidator.java | 22 + .../crypto/SignatureValidatorFactory.java | 25 + .../io/jsonwebtoken/impl/crypto/Signer.java | 23 + .../impl/crypto/SignerFactory.java | 25 + .../java/io/jsonwebtoken/lang/Assert.java | 375 ++++++ .../java/io/jsonwebtoken/lang/Classes.java | 246 ++++ .../io/jsonwebtoken/lang/Collections.java | 363 ++++++ .../lang/InstantiationException.java | 26 + .../java/io/jsonwebtoken/lang/Objects.java | 908 +++++++++++++ .../jsonwebtoken/lang/RuntimeEnvironment.java | 61 + .../java/io/jsonwebtoken/lang/Strings.java | 1126 +++++++++++++++++ .../lang/UnknownClassException.java | 61 + .../groovy/io/jsonwebtoken/JWTsTest.groovy | 244 ++++ .../SignatureAlgorithmTest.groovy | 44 + .../crypto/DefaultSignerFactoryTest.groovy | 46 + .../impl/crypto/MacSignerTest.groovy | 74 ++ 54 files changed, 6055 insertions(+), 1 deletion(-) create mode 100644 pom.xml create mode 100644 src/main/java/io/jsonwebtoken/Claims.java create mode 100644 src/main/java/io/jsonwebtoken/Header.java create mode 100644 src/main/java/io/jsonwebtoken/JWTs.java create mode 100644 src/main/java/io/jsonwebtoken/JwtBuilder.java create mode 100644 src/main/java/io/jsonwebtoken/JwtException.java create mode 100644 src/main/java/io/jsonwebtoken/JwtParser.java create mode 100644 src/main/java/io/jsonwebtoken/MalformedJwtException.java create mode 100644 src/main/java/io/jsonwebtoken/SignatureAlgorithm.java create mode 100644 src/main/java/io/jsonwebtoken/SignatureException.java create mode 100644 src/main/java/io/jsonwebtoken/SignedToken.java create mode 100644 src/main/java/io/jsonwebtoken/Token.java create mode 100644 src/main/java/io/jsonwebtoken/impl/AbstractTextCodec.java create mode 100644 src/main/java/io/jsonwebtoken/impl/Base64Codec.java create mode 100644 src/main/java/io/jsonwebtoken/impl/Base64UrlCodec.java create mode 100644 src/main/java/io/jsonwebtoken/impl/DefaultClaims.java create mode 100644 src/main/java/io/jsonwebtoken/impl/DefaultHeader.java create mode 100644 src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java create mode 100644 src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java create mode 100644 src/main/java/io/jsonwebtoken/impl/DefaultToken.java create mode 100644 src/main/java/io/jsonwebtoken/impl/JwtMap.java create mode 100644 src/main/java/io/jsonwebtoken/impl/TextCodec.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/AbstractSigner.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/Signer.java create mode 100644 src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java create mode 100644 src/main/java/io/jsonwebtoken/lang/Assert.java create mode 100644 src/main/java/io/jsonwebtoken/lang/Classes.java create mode 100644 src/main/java/io/jsonwebtoken/lang/Collections.java create mode 100644 src/main/java/io/jsonwebtoken/lang/InstantiationException.java create mode 100644 src/main/java/io/jsonwebtoken/lang/Objects.java create mode 100644 src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java create mode 100644 src/main/java/io/jsonwebtoken/lang/Strings.java create mode 100644 src/main/java/io/jsonwebtoken/lang/UnknownClassException.java create mode 100644 src/test/groovy/io/jsonwebtoken/JWTsTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy diff --git a/.gitignore b/.gitignore index 32858aad..1a9eb967 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +.idea +*.iml +*.iws + diff --git a/README.md b/README.md index 4177ede8..5f417202 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,61 @@ jjwt ==== -JSON Web Token library for Java +# JSON Web Token for Java + +This library is intended to be the easiest to use and understand library for creating JSON Web Tokens (JWTs) on the JVM, period. Most complexity is hidden behind convenient and readable Builder chaining calls. Here's an example: + + //Let's create a random signing key for testing: + Random random = new SecureRandom(); + byte[] key = new byte[64]; + random.nextBytes(key); + + Claims claims = JWTs.claims().setIssuer("Me").setSubject("Joe") + .setExpiration(new Date(System.currentTimeMillis() + 3600)); + + String jwt = JWTs.builder().setClaims(claims).signWith(SigningAlgorithm.HS256, key).compact(); + +How easy was that!? + +Now let's verify the JWT (you should always discard JWTs that don't match an expected signature): + + Token token = JWTs.parser().setSigningKey(key).parse(jwt); + + assert token.getClaims().getSubject().equals("Joe"); + +You have to love one-line code snippets in Java! + +But what if signature validation failed? You can catch `SignatureException` and react accordingly: + + try { + + JWTs.parser().setSigningKey(key).parse(jwt); + + //OK, we can trust this JWT + + } catch (SignatureException e) { + + //don't trust the JWT! + } + +## Supported Features + +* Creating and parsing plaintext JWTs + +* Creating and parsing digitally signed JWTs (aka JWSs) with the following algorithms: + * HS256: HMAC using SHA-384 + * HS384: HMAC using SHA-384 + * HS512: HMAC using SHA-512 + * RS256: RSASSA-PKCS-v1_5 using SHA-256 + * RS384: RSASSA-PKCS-v1_5 using SHA-384 + * RS512: RSASSA-PKCS-v1_5 using SHA-512 + * PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256 + * PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384 + * PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512 + +## Currently Unsupported Features + +* Elliptic Curve signature algorithms ES256, ES384 and ES512 are not yet implemented. +* JWE (Encryption for JWT) is not yet implemented. + +Both of these feature sets will be implemented in a future release when possible. Community contributions are welcome! diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..82fe5c71 --- /dev/null +++ b/pom.xml @@ -0,0 +1,246 @@ + + + + 4.0.0 + + + org.sonatype.oss + oss-parent + 7 + + + io.jsonwebtoken + jjwt + 0.1-SNAPSHOT + JSON Web Token support for the JVM + jar + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + repo + + + + + scm:git:https://github.com/jwtk/jjwt.git + scm:git:git@github.com:jwtk/jjwt.git + git@github.com:jwtk/jjwt.git + HEAD + + + GitHub Issues + https://github.com/jwtk/jjwt/issues + + + TravisCI + https://travis-ci.org/jwtk/jjwt + + + + 2.4 + 3.1 + + 1.6 + UTF-8 + ${user.name}-${maven.build.timestamp} + + 1.7.6 + 2.4.2 + + + 1.51 + + + 2.3.0-beta-2 + 1.0.7 + 3.1 + 6.8 + 2.12.4 + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + runtime + true + + + + + ch.qos.logback + logback-classic + ${logback.version} + test + + + org.testng + testng + ${testng.version} + test + + + org.codehaus.groovy + groovy-all + ${groovy.version} + test + + + org.easymock + easymock + ${easymock.version} + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.compiler.version} + + ${jdk.version} + ${jdk.version} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven.jar.version} + + + + true + true + + + + + + + org.codehaus.gmaven + gmaven-plugin + 1.5 + + 2.0 + + + + + + generateStubs + compile + generateTestStubs + testCompile + + + + + + org.codehaus.gmaven.runtime + gmaven-runtime-2.0 + 1.5 + + + org.codehaus.groovy + groovy-all + + + + + org.codehaus.groovy + groovy-all + ${groovy.version} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.17 + + + **/*IT.java + **/*IT.groovy + **/*ITCase.java + **/*ITCase.groovy + + + **/*ManualIT.java + **/*ManualIT.groovy + + + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.5 + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.9 + + + + forked-path + false + ${arguments} -Psonatype-oss-release + true + + + + + + \ No newline at end of file diff --git a/src/main/java/io/jsonwebtoken/Claims.java b/src/main/java/io/jsonwebtoken/Claims.java new file mode 100644 index 00000000..00342277 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/Claims.java @@ -0,0 +1,59 @@ +/* + * 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; + +import java.util.Date; +import java.util.Map; + +public interface Claims extends Map { + + public static final String ISSUER = "iss"; + public static final String SUBJECT = "sub"; + public static final String AUDIENCE = "aud"; + public static final String EXPIRATION = "exp"; + public static final String NOT_BEFORE = "nbf"; + public static final String ISSUED_AT = "iat"; + public static final String ID = "jti"; + + String getIssuer(); + + Claims setIssuer(String iss); + + String getSubject(); + + Claims setSubject(String sub); + + String getAudience(); + + Claims setAudience(String aud); + + Date getExpiration(); + + Claims setExpiration(Date exp); + + Date getNotBefore(); + + Claims setNotBefore(Date nbf); + + Date getIssuedAt(); + + Claims setIssuedAt(Date iat); + + String getId(); + + Claims setId(String jti); + +} diff --git a/src/main/java/io/jsonwebtoken/Header.java b/src/main/java/io/jsonwebtoken/Header.java new file mode 100644 index 00000000..68ef4f62 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/Header.java @@ -0,0 +1,39 @@ +/* + * 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; + +import java.util.Map; + +public interface Header extends Map { + + public static final String JWT_TYPE = "JWT"; + public static final String TYPE = "typ"; + public static final String ALGORITHM = "alg"; + public static final String CONTENT_TYPE = "cty"; + + public String getType(); + + public Header setType(String typ); + + public String getAlgorithm(); + + public Header setAlgorithm(String alg); + + public String getContentType(); + + public void setContentType(String cty); + +} diff --git a/src/main/java/io/jsonwebtoken/JWTs.java b/src/main/java/io/jsonwebtoken/JWTs.java new file mode 100644 index 00000000..aed46cf9 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/JWTs.java @@ -0,0 +1,44 @@ +/* + * 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; + +import io.jsonwebtoken.impl.DefaultClaims; +import io.jsonwebtoken.impl.DefaultJwtBuilder; +import io.jsonwebtoken.impl.DefaultJwtParser; + +import java.util.Map; + +public class JWTs { + + public static Claims claims() { + return new DefaultClaims(); + } + + public static Claims claims(Map claims) { + if (claims == null) { + return claims(); + } + return new DefaultClaims(claims); + } + + public static JwtParser parser() { + return new DefaultJwtParser(); + } + + public static JwtBuilder builder() { + return new DefaultJwtBuilder(); + } +} diff --git a/src/main/java/io/jsonwebtoken/JwtBuilder.java b/src/main/java/io/jsonwebtoken/JwtBuilder.java new file mode 100644 index 00000000..3563b8b4 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/JwtBuilder.java @@ -0,0 +1,48 @@ +/* + * 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; + +import java.security.Key; +import java.util.Map; + +public interface JwtBuilder { + + //replaces any existing header with the specified header. + JwtBuilder setHeader(Header header); + + //replaces current header with specified header + JwtBuilder setHeader(Map header); + + //appends to any existing header the specified parameters. + JwtBuilder setHeaderParams(Map params); + + //sets the specified header parameter, overwriting any previous value under the same name. + JwtBuilder setHeaderParam(String name, Object value); + + JwtBuilder setPayload(String payload); + + JwtBuilder setClaims(Claims claims); + + JwtBuilder setClaims(Map claims); + + JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey); + + JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey); + + JwtBuilder signWith(SignatureAlgorithm alg, Key key); + + String compact(); +} diff --git a/src/main/java/io/jsonwebtoken/JwtException.java b/src/main/java/io/jsonwebtoken/JwtException.java new file mode 100644 index 00000000..65f1bc45 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/JwtException.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class JwtException extends RuntimeException { + + public JwtException(String message) { + super(message); + } + + public JwtException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/JwtParser.java b/src/main/java/io/jsonwebtoken/JwtParser.java new file mode 100644 index 00000000..5efef4de --- /dev/null +++ b/src/main/java/io/jsonwebtoken/JwtParser.java @@ -0,0 +1,33 @@ +/* + * 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; + +import java.security.Key; + +public interface JwtParser { + + public static final char SEPARATOR_CHAR = '.'; + + JwtParser setSigningKey(byte[] key); + + JwtParser setSigningKey(String base64EncodedKeyBytes); + + JwtParser setSigningKey(Key key); + + boolean isSigned(String jwt); + + Token parse(String jwt) throws MalformedJwtException, SignatureException; +} diff --git a/src/main/java/io/jsonwebtoken/MalformedJwtException.java b/src/main/java/io/jsonwebtoken/MalformedJwtException.java new file mode 100644 index 00000000..2f42afc8 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/MalformedJwtException.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class MalformedJwtException extends JwtException { + + public MalformedJwtException(String message) { + super(message); + } + + public MalformedJwtException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java b/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java new file mode 100644 index 00000000..07d02194 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java @@ -0,0 +1,89 @@ +/* + * 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; + +import io.jsonwebtoken.lang.RuntimeEnvironment; + +public enum SignatureAlgorithm { + + NONE("none", "No digital signature or MAC performed", null, false), + HS256("HS256", "HMAC using SHA-256", "HmacSHA256", true), + HS384("HS384", "HMAC using SHA-384", "HmacSHA384", true), + HS512("HS512", "HMAC using SHA-512", "HmacSHA512", true), + RS256("RS256", "RSASSA-PKCS-v1_5 using SHA-256", "SHA256withRSA", true), + RS384("RS384", "RSASSA-PKCS-v1_5 using SHA-384", "SHA384withRSA", true), + RS512("RS512", "RSASSA-PKCS-v1_5 using SHA-512", "SHA512withRSA", true), + ES256("ES256", "ECDSA using P-256 and SHA-256", "secp256r1", false), //bouncy castle, not in the jdk + ES384("ES384", "ECDSA using P-384 and SHA-384", "secp384r1", false), //bouncy castle, not in the jdk + ES512("ES512", "ECDSA using P-512 and SHA-512", "secp521r1", false), //bouncy castle, not in the jdk + PS256("PS256", "RSASSA-PSS using SHA-256 and MGF1 with SHA-256", "SHA256withRSAandMGF1", false), //bouncy castle, not in the jdk + PS384("PS384", "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "SHA384withRSAandMGF1", false), //bouncy castle, not in the jdk + PS512("PS512", "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "SHA512withRSAandMGF1", false); //bouncy castle, not in the jdk + + static { + RuntimeEnvironment.enableBouncyCastleIfPossible(); + } + + private final String value; + private final String description; + private final String jcaName; + private final boolean jdkStandard; + + private SignatureAlgorithm(String value, String description, String jcaName, boolean jdkStandard) { + this.value = value; + this.description = description; + this.jcaName = jcaName; + this.jdkStandard = jdkStandard; + } + + public String getValue() { + return value; + } + + public String getDescription() { + return description; + } + + public String getJcaName() { + return jcaName; + } + + public boolean isJdkStandard() { + return jdkStandard; + } + + public boolean isHmac() { + return name().startsWith("HS"); + } + + public boolean isRsa() { + return getDescription().startsWith("RSASSA"); + } + + public boolean isEllipticCurve() { + return name().startsWith("ES"); + } + + public static SignatureAlgorithm forName(String value) { + for (SignatureAlgorithm alg : values()) { + if (alg.getValue().equalsIgnoreCase(value)) { + return alg; + } + } + + throw new SignatureException("Unsupported signature algorithm '" + value + "'"); + } +} diff --git a/src/main/java/io/jsonwebtoken/SignatureException.java b/src/main/java/io/jsonwebtoken/SignatureException.java new file mode 100644 index 00000000..ad58c4a4 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/SignatureException.java @@ -0,0 +1,27 @@ +/* + * 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; + +public class SignatureException extends JwtException { + + public SignatureException(String message) { + super(message); + } + + public SignatureException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/io/jsonwebtoken/SignedToken.java b/src/main/java/io/jsonwebtoken/SignedToken.java new file mode 100644 index 00000000..1a87624c --- /dev/null +++ b/src/main/java/io/jsonwebtoken/SignedToken.java @@ -0,0 +1,21 @@ +/* + * 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; + +public interface SignedToken extends Token { + + String getDigest(); +} diff --git a/src/main/java/io/jsonwebtoken/Token.java b/src/main/java/io/jsonwebtoken/Token.java new file mode 100644 index 00000000..64d1a571 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/Token.java @@ -0,0 +1,27 @@ +/* + * 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; + +public interface Token { + + Header getHeader(); + + B getBody(); + + boolean isSigned(); + + String getSignature(); +} diff --git a/src/main/java/io/jsonwebtoken/impl/AbstractTextCodec.java b/src/main/java/io/jsonwebtoken/impl/AbstractTextCodec.java new file mode 100644 index 00000000..3ba757a6 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/AbstractTextCodec.java @@ -0,0 +1,39 @@ +/* + * 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; + +import io.jsonwebtoken.lang.Assert; + +import java.nio.charset.Charset; + +public abstract class AbstractTextCodec implements TextCodec { + + protected static final Charset UTF8 = Charset.forName("UTF-8"); + protected static final Charset US_ASCII = Charset.forName("US-ASCII"); + + @Override + public String encode(String data) { + Assert.hasText(data, "String argument to encode cannot be null or empty."); + byte[] bytes = data.getBytes(UTF8); + return encode(bytes); + } + + @Override + public String decodeToString(String encoded) { + byte[] bytes = decode(encoded); + return new String(bytes, UTF8); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/Base64Codec.java b/src/main/java/io/jsonwebtoken/impl/Base64Codec.java new file mode 100644 index 00000000..87672ba9 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/Base64Codec.java @@ -0,0 +1,32 @@ +/* + * 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; + +import javax.xml.bind.DatatypeConverter; + +public class Base64Codec extends AbstractTextCodec { + + @Override + public String encode(byte[] data) { + return DatatypeConverter.printBase64Binary(data); + } + + @Override + public byte[] decode(String encoded) { + return DatatypeConverter.parseBase64Binary(encoded); + } + +} diff --git a/src/main/java/io/jsonwebtoken/impl/Base64UrlCodec.java b/src/main/java/io/jsonwebtoken/impl/Base64UrlCodec.java new file mode 100644 index 00000000..73420452 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/Base64UrlCodec.java @@ -0,0 +1,97 @@ +/* + * 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; + +public class Base64UrlCodec extends AbstractTextCodec { + + @Override + public String encode(byte[] data) { + String base64Text = TextCodec.BASE64.encode(data); + byte[] bytes = base64Text.getBytes(US_ASCII); + + //base64url encoding doesn't use padding chars: + bytes = removePadding(bytes); + + //replace URL-unfriendly Base64 chars to url-friendly ones: + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == '+') { + bytes[i] = '-'; + } else if (bytes[i] == '/') { + bytes[i] = '_'; + } + } + + return new String(bytes, US_ASCII); + } + + protected byte[] removePadding(byte[] bytes) { + + byte[] result = bytes; + + int paddingCount = 0; + for (int i = bytes.length - 1; i > 0; i--) { + if (bytes[i] == '=') { + paddingCount++; + } else { + break; + } + } + if (paddingCount > 0) { + result = new byte[bytes.length - paddingCount]; + System.arraycopy(bytes, 0, result, 0, bytes.length - paddingCount); + } + + return result; + } + + @Override + public byte[] decode(String encoded) { + char[] chars = encoded.toCharArray(); //always ASCII - one char == 1 byte + + //Base64 requires padding to be in place before decoding, so add it if necessary: + chars = ensurePadding(chars); + + //Replace url-friendly chars back to normal Base64 chars: + for (int i = 0; i < chars.length; i++) { + if (chars[i] == '-') { + chars[i] = '+'; + } else if (chars[i] == '_') { + chars[i] = '/'; + } + } + + String base64Text = new String(chars); + + return TextCodec.BASE64.decode(base64Text); + } + + protected char[] ensurePadding(char[] chars) { + + char[] result = chars; //assume argument in case no padding is necessary + + int paddingCount = chars.length % 4; + if (paddingCount > 0) { + result = new char[chars.length + paddingCount]; + System.arraycopy(chars, 0, result, 0, chars.length); + for (int i = 0; i < paddingCount; i++) { + result[chars.length + i] = '='; + } + } + + return result; + } + +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java new file mode 100644 index 00000000..5e6284eb --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/DefaultClaims.java @@ -0,0 +1,109 @@ +/* + * 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; + +import io.jsonwebtoken.Claims; + +import java.util.Date; +import java.util.Map; + +public class DefaultClaims extends JwtMap implements Claims { + + public DefaultClaims() { + super(); + } + + public DefaultClaims(Map map) { + super(map); + } + + @Override + public String getIssuer() { + return getString(ISSUER); + } + + @Override + public Claims setIssuer(String iss) { + setValue(ISSUER, iss); + return this; + } + + @Override + public String getSubject() { + return getString(SUBJECT); + } + + @Override + public Claims setSubject(String sub) { + setValue(SUBJECT, sub); + return this; + } + + @Override + public String getAudience() { + return getString(AUDIENCE); + } + + @Override + public Claims setAudience(String aud) { + setValue(AUDIENCE, aud); + return this; + } + + @Override + public Date getExpiration() { + return getDate(Claims.EXPIRATION); + } + + @Override + public Claims setExpiration(Date exp) { + setDate(Claims.EXPIRATION, exp); + return this; + } + + @Override + public Date getNotBefore() { + return getDate(Claims.NOT_BEFORE); + } + + @Override + public Claims setNotBefore(Date nbf) { + setDate(Claims.NOT_BEFORE, nbf); + return this; + } + + @Override + public Date getIssuedAt() { + return getDate(Claims.ISSUED_AT); + } + + @Override + public Claims setIssuedAt(Date iat) { + setDate(Claims.ISSUED_AT, iat); + return this; + } + + @Override + public String getId() { + return getString(ID); + } + + @Override + public Claims setId(String jti) { + setValue(Claims.ID, jti); + return this; + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java b/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java new file mode 100644 index 00000000..84b6a6d2 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/DefaultHeader.java @@ -0,0 +1,63 @@ +/* + * 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; + +import io.jsonwebtoken.Header; + +import java.util.Map; + +public class DefaultHeader extends JwtMap implements Header { + + public DefaultHeader() { + super(); + } + + public DefaultHeader(Map map) { + super(map); + } + + @Override + public String getType() { + return getString(TYPE); + } + + @Override + public Header setType(String typ) { + setValue(TYPE, typ); + return this; + } + + @Override + public String getAlgorithm() { + return getString(ALGORITHM); + } + + @Override + public Header setAlgorithm(String alg) { + setValue(ALGORITHM, alg); + return this; + } + + @Override + public String getContentType() { + return getString(CONTENT_TYPE); + } + + @Override + public void setContentType(String cty) { + setValue(CONTENT_TYPE, cty); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java new file mode 100644 index 00000000..5ad54333 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -0,0 +1,192 @@ +/* + * 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; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JWTs; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.crypto.DefaultJwtSigner; +import io.jsonwebtoken.impl.crypto.JwtSigner; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Collections; +import io.jsonwebtoken.lang.Objects; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.util.Map; + +public class DefaultJwtBuilder implements JwtBuilder { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + private Header header; + private Claims claims; + private String payload; + + private SignatureAlgorithm algorithm; + private Key key; + private byte[] keyBytes; + + @Override + public JwtBuilder setHeader(Header header) { + this.header = header; + return this; + } + + @Override + public JwtBuilder setHeader(Map header) { + this.header = new DefaultHeader(header); + return this; + } + + @Override + public JwtBuilder setHeaderParams(Map params) { + if (!Collections.isEmpty(params)) { + + Header header = ensureHeader(); + + for (Map.Entry entry : params.entrySet()) { + header.put(entry.getKey(), entry.getValue()); + } + } + return this; + } + + protected Header ensureHeader() { + if (this.header == null) { + this.header = new DefaultHeader(); + } + return this.header; + } + + @Override + public JwtBuilder setHeaderParam(String name, Object value) { + ensureHeader().put(name, value); + return this; + } + + @Override + public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey) { + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); + Assert.notEmpty(secretKey, "secret key byte array cannot be null or empty."); + Assert.isTrue(!alg.isRsa(), "Key bytes cannot be specified for RSA signatures. Please specify an RSA PrivateKey instance."); + this.algorithm = alg; + this.keyBytes = secretKey; + return this; + } + + @Override + public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) { + Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty."); + byte[] bytes = TextCodec.BASE64.decode(base64EncodedSecretKey); + return signWith(alg, bytes); + } + + @Override + public JwtBuilder signWith(SignatureAlgorithm alg, Key key) { + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); + Assert.notNull(key, "Key argument cannot be null."); + this.algorithm = alg; + this.key = key; + return this; + } + + @Override + public JwtBuilder setPayload(String payload) { + this.payload = payload; + return this; + } + + @Override + public JwtBuilder setClaims(Claims claims) { + this.claims = claims; + return this; + } + + @Override + public JwtBuilder setClaims(Map claims) { + this.claims = JWTs.claims(claims); + return this; + } + + @Override + public String compact() { + if (payload == null && claims == null) { + throw new IllegalStateException("Either 'payload' or 'claims' must be specified."); + } + + if (payload != null && claims != null) { + throw new IllegalStateException("Both 'payload' and 'claims' cannot both be specified. Choose either one."); + } + + if (key != null && keyBytes != null) { + throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either one."); + } + + Header header = ensureHeader(); + + Key key = this.key; + if (key == null && !Objects.isEmpty(keyBytes)) { + key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); + } + + if (key != null) { + header.setAlgorithm(algorithm.getValue()); + } else { + //no signature - plaintext JWT: + header.setAlgorithm(SignatureAlgorithm.NONE.getValue()); + } + + String base64UrlEncodedHeader = base64UrlEncode(header, "Unable to serialize header to json."); + + String base64UrlEncodedBody = this.payload != null ? + TextCodec.BASE64URL.encode(this.payload) : + base64UrlEncode(claims, "Unable to serialize claims object to json."); + + String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody; + + if (key != null) { //jwt must be signed: + + JwtSigner signer = new DefaultJwtSigner(algorithm, key); + + String base64UrlSignature = signer.sign(jwt); + + jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature; + } else { + // no signature (plaintext), but must terminate w/ a period, see + // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1 + jwt += JwtParser.SEPARATOR_CHAR; + } + + return jwt; + } + + public static String base64UrlEncode(Object o, String errMsg) { + String s; + try { + s = OBJECT_MAPPER.writeValueAsString(o); + } catch (JsonProcessingException e) { + throw new IllegalStateException(errMsg, e); + } + + return TextCodec.BASE64URL.encode(s); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java new file mode 100644 index 00000000..6869cd34 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -0,0 +1,218 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.Token; +import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; +import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.Objects; +import io.jsonwebtoken.lang.Strings; + +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.security.Key; +import java.util.Map; + +public class DefaultJwtParser implements JwtParser { + + private ObjectMapper objectMapper = new ObjectMapper(); + + private byte[] keyBytes; + + private Key key; + + @Override + public JwtParser setSigningKey(byte[] key) { + Assert.notEmpty(key, "signing key cannot be null or empty."); + this.keyBytes = key; + return this; + } + + @Override + public JwtParser setSigningKey(String base64EncodedKeyBytes) { + Assert.hasText(base64EncodedKeyBytes, "signing key cannot be null or empty."); + this.keyBytes = TextCodec.BASE64.decode(base64EncodedKeyBytes); + return this; + } + + @Override + public JwtParser setSigningKey(Key key) { + Assert.notNull(key, "signing key cannot be null."); + this.key = key; + return this; + } + + @Override + public boolean isSigned(String jwt) { + + if (jwt == null) { + return false; + } + + int delimiterCount = 0; + + for (int i = 0; i < jwt.length(); i++) { + char c = jwt.charAt(i); + + if (delimiterCount == 2) { + return !Character.isWhitespace(c) && c != SEPARATOR_CHAR; + } + + if (c == SEPARATOR_CHAR) { + delimiterCount++; + } + } + + return false; + } + + @Override + public Token parse(String jwt) throws MalformedJwtException, SignatureException { + + Assert.hasText(jwt, "JWT String argument cannot be null or empty."); + + String base64UrlEncodedHeader = null; + String base64UrlEncodedPayload = null; + String base64UrlEncodedDigest = null; + + int delimiterCount = 0; + + StringBuilder sb = new StringBuilder(128); + + for (char c : jwt.toCharArray()) { + + if (c == SEPARATOR_CHAR) { + + String token = Strings.clean(sb.toString()); + + if (delimiterCount == 0) { + base64UrlEncodedHeader = token; + } else if (delimiterCount == 1) { + base64UrlEncodedPayload = token; + } + + delimiterCount++; + sb = new StringBuilder(128); + } else { + sb.append(c); + } + } + + if (delimiterCount != 2) { + String msg = "JWT strings must contain exactly 2 period characters. Found: " + delimiterCount; + throw new MalformedJwtException(msg); + } + if (sb.length() > 0) { + base64UrlEncodedDigest = sb.toString(); + } + + if (base64UrlEncodedPayload == null) { + throw new MalformedJwtException("JWT string '" + jwt + "' is missing a body/payload."); + } + + // =============== Header ================= + Header header = null; + + if (base64UrlEncodedHeader != null) { + String origValue = TextCodec.BASE64URL.decodeToString(base64UrlEncodedHeader); + Map m = readValue(origValue); + header = new DefaultHeader(m); + } + + // =============== Body ================= + String payload = TextCodec.BASE64URL.decodeToString(base64UrlEncodedPayload); + + Claims claims = null; + + if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { //likely to be json, parse it: + Map claimsMap = readValue(payload); + claims = new DefaultClaims(claimsMap); + } + + // =============== Signature ================= + if (base64UrlEncodedDigest != null) { //it is signed - validate the signature + + SignatureAlgorithm algorithm = null; + + if (header != null) { + String alg = header.getAlgorithm(); + if (Strings.hasText(alg)) { + algorithm = SignatureAlgorithm.forName(alg); + } + } + + if (algorithm == null || algorithm == SignatureAlgorithm.NONE) { + //it is plaintext, but it has a signature. This is invalid: + String msg = "JWT string has a digest/signature, but the header does not reference a valid signature " + + "algorithm."; + throw new MalformedJwtException(msg); + } + + if (key != null && keyBytes != null) { + throw new IllegalStateException("A key object and key bytes cannot both be specified. Choose either."); + } + + //digitally signed, let's assert the signature: + Key key = this.key; + + if (key == null) { //fall back to keyBytes + + if (!Objects.isEmpty(this.keyBytes)) { + + Assert.isTrue(!algorithm.isRsa(), "Key bytes cannot be specified for RSA signatures. Please specify a PublicKey or PrivateKey instance."); + + key = new SecretKeySpec(keyBytes, algorithm.getJcaName()); + } + } + + Assert.notNull(key, "A signing key must be specified if the specified JWT is digitally signed."); + + //re-create the jwt part without the signature. This is what needs to be signed for verification: + String jwtWithoutSignature = base64UrlEncodedHeader + SEPARATOR_CHAR + base64UrlEncodedPayload; + + JwtSignatureValidator validator = new DefaultJwtSignatureValidator(algorithm, key); + + if (!validator.isValid(jwtWithoutSignature, base64UrlEncodedDigest)) { + String msg = "JWT signature does not match locally computed signature. JWT validity cannot be " + + "asserted and should not be trusted."; + throw new SignatureException(msg); + } + } + + if (claims != null) { + return new DefaultToken(header, claims, base64UrlEncodedDigest); + } else { + return new DefaultToken(header, payload, base64UrlEncodedDigest); + } + } + + @SuppressWarnings("unchecked") + protected Map readValue(String val) { + try { + return objectMapper.readValue(val, Map.class); + } catch (IOException e) { + throw new MalformedJwtException("Unable to read JSON value: " + val, e); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultToken.java b/src/main/java/io/jsonwebtoken/impl/DefaultToken.java new file mode 100644 index 00000000..d967e663 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/DefaultToken.java @@ -0,0 +1,58 @@ +/* + * 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; + +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Token; +import io.jsonwebtoken.lang.Assert; + +public class DefaultToken implements Token { + + private final Header header; + private final B body; + private final String signature; + + public DefaultToken(Header header, B body, String signature) { + this.header = header; + this.body = body; + this.signature = signature; + } + + public boolean hasHeader() { + return this.header != null; + } + + public boolean isSigned() { + return this.signature != null; + } + + @Override + public String getSignature() { + Assert.notNull(signature, "Not a signed token. Call 'isSigned()' before calling this method."); + return this.signature; + } + + @Override + public Header getHeader() { + Assert.notNull(header, "Header is not present. Call 'hasHeader()' before calling this method."); + return header; + } + + @Override + public B getBody() { + return body; + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/src/main/java/io/jsonwebtoken/impl/JwtMap.java new file mode 100644 index 00000000..d4929147 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/JwtMap.java @@ -0,0 +1,151 @@ +/* + * 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; + +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +public class JwtMap implements Map { + + private final Map map; + + public JwtMap() { + this(new LinkedHashMap()); + } + + public JwtMap(Map map) { + if (map == null) { + throw new IllegalArgumentException("Map argument cannot be null."); + } + this.map = map; + } + + protected String getString(String name) { + Object v = get(name); + return v != null ? String.valueOf(v) : null; + } + + protected static Date toDate(Object v, String name) { + if (v == null) { + return null; + } else if (v instanceof Date) { + return (Date) v; + } else if (v instanceof Number) { + int seconds = ((Number) v).intValue(); + return new Date(seconds * 1000); + } else if (v instanceof String) { + int seconds = Integer.parseInt((String) v); + return new Date(seconds * 1000); + } else { + throw new IllegalStateException("Cannot convert '" + name + "' value [" + v + "] to Date instance."); + } + } + + protected void setValue(String name, Object v) { + if (v == null) { + map.remove(name); + } else { + map.put(name, v); + } + } + + protected Date getDate(String name) { + Object v = map.get(name); + return toDate(v, name); + } + + protected void setDate(String name, Date d) { + if (d == null) { + map.remove(name); + } else { + long seconds = d.getTime() / 1000; + map.put(name, seconds); + } + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object o) { + return map.containsKey(o); + } + + @Override + public boolean containsValue(Object o) { + return map.containsValue(o); + } + + @Override + public Object get(Object o) { + return map.get(o); + } + + @Override + public Object put(String s, Object o) { + if (o == null) { + return map.remove(s); + } else { + return map.put(s, o); + } + } + + @Override + public Object remove(Object o) { + return map.remove(o); + } + + @SuppressWarnings("NullableProblems") + @Override + public void putAll(Map m) { + if (m == null) { + return; + } + for (String s : m.keySet()) { + map.put(s, m.get(s)); + } + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + public Collection values() { + return map.values(); + } + + @Override + public Set> entrySet() { + return map.entrySet(); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/TextCodec.java b/src/main/java/io/jsonwebtoken/impl/TextCodec.java new file mode 100644 index 00000000..caf955a8 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/TextCodec.java @@ -0,0 +1,30 @@ +/* + * 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; + +public interface TextCodec { + + public static final TextCodec BASE64 = new Base64Codec(); + public static final TextCodec BASE64URL = new Base64UrlCodec(); + + String encode(String data); + + String encode(byte[] data); + + byte[] decode(String encoded); + + String decodeToString(String encoded); +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/AbstractSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/AbstractSigner.java new file mode 100644 index 00000000..849ab255 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/AbstractSigner.java @@ -0,0 +1,34 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; + +public abstract class AbstractSigner implements Signer { + + protected final SignatureAlgorithm alg; + protected final Key key; + + protected AbstractSigner(SignatureAlgorithm alg, Key key) { + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); + Assert.notNull(key, "Key cannot be null."); + this.alg = alg; + this.key = key; + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java new file mode 100644 index 00000000..245c0d55 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidator.java @@ -0,0 +1,49 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.TextCodec; +import io.jsonwebtoken.lang.Assert; + +import java.nio.charset.Charset; +import java.security.Key; + +public class DefaultJwtSignatureValidator implements JwtSignatureValidator { + + private static final Charset US_ASCII = Charset.forName("US-ASCII"); + + private final SignatureValidator signatureValidator; + + public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) { + this(DefaultSignatureValidatorFactory.INSTANCE, alg, key); + } + + public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) { + Assert.notNull(factory, "SignerFactory argument cannot be null."); + this.signatureValidator = factory.createSignatureValidator(alg, key); + } + + @Override + public boolean isValid(String jwtWithoutSignature, String base64UrlEncodedSignature) { + + byte[] data = jwtWithoutSignature.getBytes(US_ASCII); + + byte[] signature = TextCodec.BASE64URL.decode(base64UrlEncodedSignature); + + return this.signatureValidator.isValid(data, signature); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java new file mode 100644 index 00000000..e55d5933 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultJwtSigner.java @@ -0,0 +1,49 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.impl.TextCodec; +import io.jsonwebtoken.lang.Assert; + +import java.nio.charset.Charset; +import java.security.Key; + +public class DefaultJwtSigner implements JwtSigner { + + private static final Charset US_ASCII = Charset.forName("US-ASCII"); + + private final Signer signer; + + public DefaultJwtSigner(SignatureAlgorithm alg, Key key) { + this(DefaultSignerFactory.INSTANCE, alg, key); + } + + public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key) { + Assert.notNull(factory, "SignerFactory argument cannot be null."); + this.signer = factory.createSigner(alg, key); + } + + @Override + public String sign(String jwtWithoutSignature) { + + byte[] bytesToSign = jwtWithoutSignature.getBytes(US_ASCII); + + byte[] signature = signer.sign(bytesToSign); + + return TextCodec.BASE64URL.encode(signature); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java new file mode 100644 index 00000000..ab9405d8 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java @@ -0,0 +1,56 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; + +public class DefaultSignatureValidatorFactory implements SignatureValidatorFactory { + + public static final SignatureValidatorFactory INSTANCE = new DefaultSignatureValidatorFactory(); + + @Override + public SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key) { + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); + Assert.notNull(key, "Signing Key cannot be null."); + + switch (alg) { + case NONE: + throw new IllegalArgumentException("The 'NONE' algorithm cannot be used for signing."); + case HS256: + case HS384: + case HS512: + return new MacValidator(alg, key); + case RS256: + case RS384: + case RS512: + case PS256: + case PS384: + case PS512: + return new RsaSignatureValidator(alg, key); + case ES256: + case ES384: + case ES512: + throw new UnsupportedOperationException("Elliptic Curve digests are not yet supported."); + default: + String msg = "Unrecognized algorithm '" + alg.name() + "'. This is a bug. Please submit a ticket " + + "via the project issue tracker."; + throw new IllegalStateException(msg); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java new file mode 100644 index 00000000..07f50450 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java @@ -0,0 +1,56 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; + +public class DefaultSignerFactory implements SignerFactory { + + public static final SignerFactory INSTANCE = new DefaultSignerFactory(); + + @Override + public Signer createSigner(SignatureAlgorithm alg, Key key) { + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); + Assert.notNull(key, "Signing Key cannot be null."); + + switch (alg) { + case NONE: + throw new IllegalArgumentException("The 'NONE' algorithm cannot be used for signing."); + case HS256: + case HS384: + case HS512: + return new MacSigner(alg, key); + case RS256: + case RS384: + case RS512: + case PS256: + case PS384: + case PS512: + return new RsaSigner(alg, key); + case ES256: + case ES384: + case ES512: + throw new UnsupportedOperationException("Elliptic Curve digests are not yet supported."); + default: + String msg = "Unrecognized algorithm '" + alg.name() + "'. This is a bug. Please submit a ticket " + + "via the project issue tracker."; + throw new IllegalStateException(msg); + } + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java new file mode 100644 index 00000000..a7175070 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/JwtSignatureValidator.java @@ -0,0 +1,21 @@ +/* + * 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.crypto; + +public interface JwtSignatureValidator { + + boolean isValid(String jwtWithoutSignature, String base64UrlEncodedSignature); +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java new file mode 100644 index 00000000..450b707f --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/JwtSigner.java @@ -0,0 +1,21 @@ +/* + * 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.crypto; + +public interface JwtSigner { + + String sign(String jwtWithoutSignature); +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java new file mode 100644 index 00000000..32bf7edf --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -0,0 +1,29 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; + +abstract class MacProvider extends SignatureProvider { + + protected MacProvider(SignatureAlgorithm alg, Key key) { + super(alg, key); + Assert.isTrue(alg.isHmac(), "SignatureAlgorithm must be a HMAC SHA algorithm."); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java new file mode 100644 index 00000000..f5383045 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java @@ -0,0 +1,60 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +public class MacSigner extends MacProvider implements Signer { + + public MacSigner(SignatureAlgorithm alg, byte[] key) { + this(alg, new SecretKeySpec(key, alg.getJcaName())); + } + + public MacSigner(SignatureAlgorithm alg, Key key) { + super(alg, key); + } + + @Override + public byte[] sign(byte[] data) { + Mac mac = getMacInstance(); + return mac.doFinal(data); + } + + protected Mac getMacInstance() throws SignatureException { + try { + return doGetMacInstance(); + } catch (NoSuchAlgorithmException e) { + String msg = "Unable to obtain JCA MAC algorithm '" + alg.getJcaName() + "': " + e.getMessage(); + throw new SignatureException(msg, e); + } catch (InvalidKeyException e) { + String msg = "The specified signing key is not a valid " + alg.name() + " key: " + e.getMessage(); + throw new SignatureException(msg, e); + } + } + + protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { + Mac mac = Mac.getInstance(alg.getJcaName()); + mac.init(key); + return mac; + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java new file mode 100644 index 00000000..95227ae9 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacValidator.java @@ -0,0 +1,36 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; + +import java.security.Key; +import java.util.Arrays; + +public class MacValidator implements SignatureValidator { + + private final MacSigner signer; + + public MacValidator(SignatureAlgorithm alg, Key key) { + this.signer = new MacSigner(alg, key); + } + + @Override + public boolean isValid(byte[] data, byte[] signature) { + byte[] computed = this.signer.sign(data); + return Arrays.equals(computed, signature); + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java new file mode 100644 index 00000000..51db7bf4 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java @@ -0,0 +1,92 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.lang.Assert; + +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PSSParameterSpec; + +abstract class RsaProvider extends SignatureProvider { + + protected RsaProvider(SignatureAlgorithm alg, Key key) { + super(alg, key); + Assert.isTrue(alg.isRsa(), "SignatureAlgorithm must be an RSASSA or RSASSA-PSS algorithm."); + } + + protected Signature createSignatureInstance() { + + Signature sig = newSignatureInstance(); + + if (alg.name().startsWith("PS")) { + + MGF1ParameterSpec paramSpec; + int saltLength; + + switch (alg) { + case PS256: + paramSpec = MGF1ParameterSpec.SHA256; + saltLength = 32; + break; + case PS384: + paramSpec = MGF1ParameterSpec.SHA384; + saltLength = 48; + break; + case PS512: + paramSpec = MGF1ParameterSpec.SHA512; + saltLength = 64; + break; + default: + throw new IllegalArgumentException("Unsupported RSASSA-PSS algorithm: " + alg); + } + + PSSParameterSpec pssParamSpec = + new PSSParameterSpec(paramSpec.getDigestAlgorithm(), "MGF1", paramSpec, saltLength, 1); + + setParameter(sig, pssParamSpec); + } + + return sig; + } + + protected Signature newSignatureInstance() { + try { + return Signature.getInstance(alg.getJcaName()); + } catch (NoSuchAlgorithmException e) { + String msg = "Unavailable RSA Signature algorithm."; + if (!alg.isJdkStandard()) { + msg += " This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath."; + } + throw new SignatureException(msg, e); + } + } + + protected void setParameter(Signature sig, PSSParameterSpec spec) { + try { + sig.setParameter(spec); + } catch (InvalidAlgorithmParameterException e) { + String msg = "Unsupported RSASSA-PSS parameter '" + spec + "': " + e.getMessage(); + throw new SignatureException(msg, e); + } + + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java new file mode 100644 index 00000000..825c5d31 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java @@ -0,0 +1,56 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.util.Arrays; + +public class RsaSignatureValidator extends RsaProvider implements SignatureValidator { + + public RsaSignatureValidator(SignatureAlgorithm alg, Key key) { + super(alg, key); + Assert.isTrue(key instanceof PrivateKey || key instanceof PublicKey, + "RSA Signature validation requires either a PublicKey or PrivateKey instance."); + } + + @Override + public boolean isValid(byte[] data, byte[] signature) { + + if (key instanceof PublicKey) { + Signature sig = createSignatureInstance(); + PublicKey publicKey = (PublicKey) key; + try { + sig.initVerify(publicKey); + sig.update(data); + return sig.verify(signature); + } catch (Exception e) { + String msg = "Unable to verify RSA signature using configured PublicKey. " + e.getMessage(); + throw new SignatureException(msg, e); + } + } else { + byte[] computed = new RsaSigner(alg, key).sign(data); + return Arrays.equals(computed, signature); + } + } + +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java new file mode 100644 index 00000000..73d5a8fa --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java @@ -0,0 +1,54 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.lang.Assert; + +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.PrivateKey; +import java.security.Signature; + +public class RsaSigner extends RsaProvider implements Signer { + + public RsaSigner(SignatureAlgorithm alg, Key key) { + super(alg, key); + Assert.isInstanceOf(PrivateKey.class, key, "RSA signatures be computed using a PrivateKey."); + } + + @Override + public byte[] sign(byte[] data) { + try { + return doSign(data); + } catch (InvalidKeyException e) { + throw new SignatureException("Invalid RSA PrivateKey. " + e.getMessage(), e); + } catch (java.security.SignatureException e) { + throw new SignatureException("Unable to calculate signature using RSA PrivateKey. " + e.getMessage(), e); + } + } + + protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { + Assert.isInstanceOf(PrivateKey.class, key, "RSA signatures be computed using a PrivateKey."); + PrivateKey privateKey = (PrivateKey)key; + Signature sig = createSignatureInstance(); + sig.initSign(privateKey); + sig.update(data); + return sig.sign(); + } + +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java new file mode 100644 index 00000000..3abb3e06 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java @@ -0,0 +1,34 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; + +import java.security.Key; + +abstract class SignatureProvider { + + protected final SignatureAlgorithm alg; + protected final Key key; + + protected SignatureProvider(SignatureAlgorithm alg, Key key) { + Assert.notNull(alg, "SignatureAlgorithm cannot be null."); + Assert.notNull(key, "Key cannot be null."); + this.alg = alg; + this.key = key; + } +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java new file mode 100644 index 00000000..edeccb7e --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidator.java @@ -0,0 +1,22 @@ +/* + * 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.crypto; + +public interface SignatureValidator { + + boolean isValid(byte[] data, byte[] signature); + +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java new file mode 100644 index 00000000..1e84b620 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureValidatorFactory.java @@ -0,0 +1,25 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; + +import java.security.Key; + +public interface SignatureValidatorFactory { + + SignatureValidator createSignatureValidator(SignatureAlgorithm alg, Key key); +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java b/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java new file mode 100644 index 00000000..096cc135 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java @@ -0,0 +1,23 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureException; + +public interface Signer { + + byte[] sign(byte[] data) throws SignatureException; +} diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java new file mode 100644 index 00000000..e0e46067 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignerFactory.java @@ -0,0 +1,25 @@ +/* + * 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.crypto; + +import io.jsonwebtoken.SignatureAlgorithm; + +import java.security.Key; + +public interface SignerFactory { + + Signer createSigner(SignatureAlgorithm alg, Key key); +} diff --git a/src/main/java/io/jsonwebtoken/lang/Assert.java b/src/main/java/io/jsonwebtoken/lang/Assert.java new file mode 100644 index 00000000..3ac082f3 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/Assert.java @@ -0,0 +1,375 @@ +/* + * 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.lang; + +import java.util.Collection; +import java.util.Map; + +public abstract class Assert { + + /** + * Assert a boolean expression, throwing IllegalArgumentException + * if the test result is false. + *
Assert.isTrue(i > 0, "The value must be greater than zero");
+ * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression, String message) { + if (!expression) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert a boolean expression, throwing IllegalArgumentException + * if the test result is false. + *
Assert.isTrue(i > 0);
+ * @param expression a boolean expression + * @throws IllegalArgumentException if expression is false + */ + public static void isTrue(boolean expression) { + isTrue(expression, "[Assertion failed] - this expression must be true"); + } + + /** + * Assert that an object is null . + *
Assert.isNull(value, "The value must be null");
+ * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is not null + */ + public static void isNull(Object object, String message) { + if (object != null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is null . + *
Assert.isNull(value);
+ * @param object the object to check + * @throws IllegalArgumentException if the object is not null + */ + public static void isNull(Object object) { + isNull(object, "[Assertion failed] - the object argument must be null"); + } + + /** + * Assert that an object is not null . + *
Assert.notNull(clazz, "The class must not be null");
+ * @param object the object to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object is null + */ + public static void notNull(Object object, String message) { + if (object == null) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an object is not null . + *
Assert.notNull(clazz);
+ * @param object the object to check + * @throws IllegalArgumentException if the object is null + */ + public static void notNull(Object object) { + notNull(object, "[Assertion failed] - this argument is required; it must not be null"); + } + + /** + * Assert that the given String is not empty; that is, + * it must not be null and not the empty String. + *
Assert.hasLength(name, "Name must not be empty");
+ * @param text the String to check + * @param message the exception message to use if the assertion fails + * @see Strings#hasLength + */ + public static void hasLength(String text, String message) { + if (!Strings.hasLength(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String is not empty; that is, + * it must not be null and not the empty String. + *
Assert.hasLength(name);
+ * @param text the String to check + * @see Strings#hasLength + */ + public static void hasLength(String text) { + hasLength(text, + "[Assertion failed] - this String argument must have length; it must not be null or empty"); + } + + /** + * Assert that the given String has valid text content; that is, it must not + * be null and must contain at least one non-whitespace character. + *
Assert.hasText(name, "'name' must not be empty");
+ * @param text the String to check + * @param message the exception message to use if the assertion fails + * @see Strings#hasText + */ + public static void hasText(String text, String message) { + if (!Strings.hasText(text)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given String has valid text content; that is, it must not + * be null and must contain at least one non-whitespace character. + *
Assert.hasText(name, "'name' must not be empty");
+ * @param text the String to check + * @see Strings#hasText + */ + public static void hasText(String text) { + hasText(text, + "[Assertion failed] - this String argument must have text; it must not be null, empty, or blank"); + } + + /** + * Assert that the given text does not contain the given substring. + *
Assert.doesNotContain(name, "rod", "Name must not contain 'rod'");
+ * @param textToSearch the text to search + * @param substring the substring to find within the text + * @param message the exception message to use if the assertion fails + */ + public static void doesNotContain(String textToSearch, String substring, String message) { + if (Strings.hasLength(textToSearch) && Strings.hasLength(substring) && + textToSearch.indexOf(substring) != -1) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that the given text does not contain the given substring. + *
Assert.doesNotContain(name, "rod");
+ * @param textToSearch the text to search + * @param substring the substring to find within the text + */ + public static void doesNotContain(String textToSearch, String substring) { + doesNotContain(textToSearch, substring, + "[Assertion failed] - this String argument must not contain the substring [" + substring + "]"); + } + + + /** + * Assert that an array has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(array, "The array must have elements");
+ * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array is null or has no elements + */ + public static void notEmpty(Object[] array, String message) { + if (Objects.isEmpty(array)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that an array has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(array);
+ * @param array the array to check + * @throws IllegalArgumentException if the object array is null or has no elements + */ + public static void notEmpty(Object[] array) { + notEmpty(array, "[Assertion failed] - this array must not be empty: it must contain at least 1 element"); + } + + public static void notEmpty(byte[] array, String msg) { + if (Objects.isEmpty(array)) { + throw new IllegalArgumentException(msg); + } + } + + /** + * Assert that an array has no null elements. + * Note: Does not complain if the array is empty! + *
Assert.noNullElements(array, "The array must have non-null elements");
+ * @param array the array to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the object array contains a null element + */ + public static void noNullElements(Object[] array, String message) { + if (array != null) { + for (int i = 0; i < array.length; i++) { + if (array[i] == null) { + throw new IllegalArgumentException(message); + } + } + } + } + + /** + * Assert that an array has no null elements. + * Note: Does not complain if the array is empty! + *
Assert.noNullElements(array);
+ * @param array the array to check + * @throws IllegalArgumentException if the object array contains a null element + */ + public static void noNullElements(Object[] array) { + noNullElements(array, "[Assertion failed] - this array must not contain any null elements"); + } + + /** + * Assert that a collection has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(collection, "Collection must have elements");
+ * @param collection the collection to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the collection is null or has no elements + */ + public static void notEmpty(Collection collection, String message) { + if (Collections.isEmpty(collection)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a collection has elements; that is, it must not be + * null and must have at least one element. + *
Assert.notEmpty(collection, "Collection must have elements");
+ * @param collection the collection to check + * @throws IllegalArgumentException if the collection is null or has no elements + */ + public static void notEmpty(Collection collection) { + notEmpty(collection, + "[Assertion failed] - this collection must not be empty: it must contain at least 1 element"); + } + + /** + * Assert that a Map has entries; that is, it must not be null + * and must have at least one entry. + *
Assert.notEmpty(map, "Map must have entries");
+ * @param map the map to check + * @param message the exception message to use if the assertion fails + * @throws IllegalArgumentException if the map is null or has no entries + */ + public static void notEmpty(Map map, String message) { + if (Collections.isEmpty(map)) { + throw new IllegalArgumentException(message); + } + } + + /** + * Assert that a Map has entries; that is, it must not be null + * and must have at least one entry. + *
Assert.notEmpty(map);
+ * @param map the map to check + * @throws IllegalArgumentException if the map is null or has no entries + */ + public static void notEmpty(Map map) { + notEmpty(map, "[Assertion failed] - this map must not be empty; it must contain at least one entry"); + } + + + /** + * Assert that the provided object is an instance of the provided class. + *
Assert.instanceOf(Foo.class, foo);
+ * @param clazz the required class + * @param obj the object to check + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance + */ + public static void isInstanceOf(Class clazz, Object obj) { + isInstanceOf(clazz, obj, ""); + } + + /** + * Assert that the provided object is an instance of the provided class. + *
Assert.instanceOf(Foo.class, foo);
+ * @param type the type to check against + * @param obj the object to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @throws IllegalArgumentException if the object is not an instance of clazz + * @see Class#isInstance + */ + public static void isInstanceOf(Class type, Object obj, String message) { + notNull(type, "Type to check against must not be null"); + if (!type.isInstance(obj)) { + throw new IllegalArgumentException(message + + "Object of class [" + (obj != null ? obj.getClass().getName() : "null") + + "] must be an instance of " + type); + } + } + + /** + * Assert that superType.isAssignableFrom(subType) is true. + *
Assert.isAssignable(Number.class, myClass);
+ * @param superType the super type to check + * @param subType the sub type to check + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class superType, Class subType) { + isAssignable(superType, subType, ""); + } + + /** + * Assert that superType.isAssignableFrom(subType) is true. + *
Assert.isAssignable(Number.class, myClass);
+ * @param superType the super type to check against + * @param subType the sub type to check + * @param message a message which will be prepended to the message produced by + * the function itself, and which may be used to provide context. It should + * normally end in a ": " or ". " so that the function generate message looks + * ok when prepended to it. + * @throws IllegalArgumentException if the classes are not assignable + */ + public static void isAssignable(Class superType, Class subType, String message) { + notNull(superType, "Type to check against must not be null"); + if (subType == null || !superType.isAssignableFrom(subType)) { + throw new IllegalArgumentException(message + subType + " is not assignable to " + superType); + } + } + + + /** + * Assert a boolean expression, throwing IllegalStateException + * if the test result is false. Call isTrue if you wish to + * throw IllegalArgumentException on an assertion failure. + *
Assert.state(id == null, "The id property must not already be initialized");
+ * @param expression a boolean expression + * @param message the exception message to use if the assertion fails + * @throws IllegalStateException if expression is false + */ + public static void state(boolean expression, String message) { + if (!expression) { + throw new IllegalStateException(message); + } + } + + /** + * Assert a boolean expression, throwing {@link IllegalStateException} + * if the test result is false. + *

Call {@link #isTrue(boolean)} if you wish to + * throw {@link IllegalArgumentException} on an assertion failure. + *

Assert.state(id == null);
+ * @param expression a boolean expression + * @throws IllegalStateException if the supplied expression is false + */ + public static void state(boolean expression) { + state(expression, "[Assertion failed] - this state invariant must be true"); + } + +} diff --git a/src/main/java/io/jsonwebtoken/lang/Classes.java b/src/main/java/io/jsonwebtoken/lang/Classes.java new file mode 100644 index 00000000..b8a62e1b --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/Classes.java @@ -0,0 +1,246 @@ +/* + * 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.lang; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.lang.reflect.Constructor; + +/** + * @since 0.1 + */ +public class Classes { + + /** + * Private internal log instance. + */ + private static final Logger log = LoggerFactory.getLogger(Classes.class); + + /** + * @since 0.1 + */ + private static final ClassLoaderAccessor THREAD_CL_ACCESSOR = new ExceptionIgnoringAccessor() { + @Override + protected ClassLoader doGetClassLoader() throws Throwable { + return Thread.currentThread().getContextClassLoader(); + } + }; + + /** + * @since 0.1 + */ + private static final ClassLoaderAccessor CLASS_CL_ACCESSOR = new ExceptionIgnoringAccessor() { + @Override + protected ClassLoader doGetClassLoader() throws Throwable { + return Classes.class.getClassLoader(); + } + }; + + /** + * @since 0.1 + */ + private static final ClassLoaderAccessor SYSTEM_CL_ACCESSOR = new ExceptionIgnoringAccessor() { + @Override + protected ClassLoader doGetClassLoader() throws Throwable { + return ClassLoader.getSystemClassLoader(); + } + }; + + /** + * Attempts to load the specified class name from the current thread's + * {@link Thread#getContextClassLoader() context class loader}, then the + * current ClassLoader (Classes.class.getClassLoader()), then the system/application + * ClassLoader (ClassLoader.getSystemClassLoader(), in that order. If any of them cannot locate + * the specified class, an UnknownClassException is thrown (our RuntimeException equivalent of + * the JRE's ClassNotFoundException. + * + * @param fqcn the fully qualified class name to load + * @return the located class + * @throws UnknownClassException if the class cannot be found. + */ + public static Class forName(String fqcn) throws UnknownClassException { + + Class clazz = THREAD_CL_ACCESSOR.loadClass(fqcn); + + if (clazz == null) { + if (log.isTraceEnabled()) { + log.trace("Unable to load class named [" + fqcn + + "] from the thread context ClassLoader. Trying the current ClassLoader..."); + } + clazz = CLASS_CL_ACCESSOR.loadClass(fqcn); + } + + if (clazz == null) { + if (log.isTraceEnabled()) { + log.trace("Unable to load class named [" + fqcn + "] from the current ClassLoader. " + + "Trying the system/application ClassLoader..."); + } + clazz = SYSTEM_CL_ACCESSOR.loadClass(fqcn); + } + + if (clazz == null) { + String msg = "Unable to load class named [" + fqcn + "] from the thread context, current, or " + + "system/application ClassLoaders. All heuristics have been exhausted. Class could not be found."; + + if (fqcn != null && fqcn.startsWith("com.stormpath.sdk.impl")) { + msg += " Have you remembered to include the stormpath-sdk-impl .jar in your runtime classpath?"; + } + + throw new UnknownClassException(msg); + } + + return clazz; + } + + /** + * Returns the specified resource by checking the current thread's + * {@link Thread#getContextClassLoader() context class loader}, then the + * current ClassLoader (Classes.class.getClassLoader()), then the system/application + * ClassLoader (ClassLoader.getSystemClassLoader(), in that order, using + * {@link ClassLoader#getResourceAsStream(String) getResourceAsStream(name)}. + * + * @param name the name of the resource to acquire from the classloader(s). + * @return the InputStream of the resource found, or null if the resource cannot be found from any + * of the three mentioned ClassLoaders. + * @since 0.8 + */ + public static InputStream getResourceAsStream(String name) { + + InputStream is = THREAD_CL_ACCESSOR.getResourceStream(name); + + if (is == null) { + is = CLASS_CL_ACCESSOR.getResourceStream(name); + } + + if (is == null) { + is = SYSTEM_CL_ACCESSOR.getResourceStream(name); + } + + return is; + } + + public static boolean isAvailable(String fullyQualifiedClassName) { + try { + forName(fullyQualifiedClassName); + return true; + } catch (UnknownClassException e) { + return false; + } + } + + @SuppressWarnings("unchecked") + public static Object newInstance(String fqcn) { + return newInstance(forName(fqcn)); + } + + @SuppressWarnings("unchecked") + public static Object newInstance(String fqcn, Object... args) { + return newInstance(forName(fqcn), args); + } + + public static T newInstance(Class clazz) { + if (clazz == null) { + String msg = "Class method parameter cannot be null."; + throw new IllegalArgumentException(msg); + } + try { + return clazz.newInstance(); + } catch (Exception e) { + throw new InstantiationException("Unable to instantiate class [" + clazz.getName() + "]", e); + } + } + + public static T newInstance(Class clazz, Object... args) { + Class[] argTypes = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + argTypes[i] = args[i].getClass(); + } + Constructor ctor = getConstructor(clazz, argTypes); + return instantiate(ctor, args); + } + + public static Constructor getConstructor(Class clazz, Class... argTypes) { + try { + return clazz.getConstructor(argTypes); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + + } + + public static T instantiate(Constructor ctor, Object... args) { + try { + return ctor.newInstance(args); + } catch (Exception e) { + String msg = "Unable to instantiate instance with constructor [" + ctor + "]"; + throw new InstantiationException(msg, e); + } + } + + /** + * @since 1.0 + */ + private static interface ClassLoaderAccessor { + Class loadClass(String fqcn); + InputStream getResourceStream(String name); + } + + /** + * @since 1.0 + */ + private static abstract class ExceptionIgnoringAccessor implements ClassLoaderAccessor { + + public Class loadClass(String fqcn) { + Class clazz = null; + ClassLoader cl = getClassLoader(); + if (cl != null) { + try { + clazz = cl.loadClass(fqcn); + } catch (ClassNotFoundException e) { + if (log.isTraceEnabled()) { + log.trace("Unable to load clazz named [" + fqcn + "] from class loader [" + cl + "]"); + } + } + } + return clazz; + } + + public InputStream getResourceStream(String name) { + InputStream is = null; + ClassLoader cl = getClassLoader(); + if (cl != null) { + is = cl.getResourceAsStream(name); + } + return is; + } + + protected final ClassLoader getClassLoader() { + try { + return doGetClassLoader(); + } catch (Throwable t) { + if (log.isDebugEnabled()) { + log.debug("Unable to acquire ClassLoader.", t); + } + } + return null; + } + + protected abstract ClassLoader doGetClassLoader() throws Throwable; + } +} + diff --git a/src/main/java/io/jsonwebtoken/lang/Collections.java b/src/main/java/io/jsonwebtoken/lang/Collections.java new file mode 100644 index 00000000..b0313cf7 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/Collections.java @@ -0,0 +1,363 @@ +/* + * 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.lang; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +public abstract class Collections { + + /** + * Return true if the supplied Collection is null + * or empty. Otherwise, return false. + * @param collection the Collection to check + * @return whether the given Collection is empty + */ + public static boolean isEmpty(Collection collection) { + return (collection == null || collection.isEmpty()); + } + + /** + * Returns the collection's size or {@code 0} if the collection is {@code null}. + * + * @param collection the collection to check. + * @return the collection's size or {@code 0} if the collection is {@code null}. + * @since 0.9.2 + */ + public static int size(Collection collection) { + return collection == null ? 0 : collection.size(); + } + + /** + * Returns the map's size or {@code 0} if the map is {@code null}. + * + * @param map the map to check + * @return the map's size or {@code 0} if the map is {@code null}. + * @since 0.9.2 + */ + public static int size(Map map) { + return map == null ? 0 : map.size(); + } + + /** + * Return true if the supplied Map is null + * or empty. Otherwise, return false. + * @param map the Map to check + * @return whether the given Map is empty + */ + public static boolean isEmpty(Map map) { + return (map == null || map.isEmpty()); + } + + /** + * Convert the supplied array into a List. A primitive array gets + * converted into a List of the appropriate wrapper type. + *

A null source value will be converted to an + * empty List. + * @param source the (potentially primitive) array + * @return the converted List result + * @see Objects#toObjectArray(Object) + */ + public static List arrayToList(Object source) { + return Arrays.asList(Objects.toObjectArray(source)); + } + + /** + * Merge the given array into the given Collection. + * @param array the array to merge (may be null) + * @param collection the target Collection to merge the array into + */ + @SuppressWarnings("unchecked") + public static void mergeArrayIntoCollection(Object array, Collection collection) { + if (collection == null) { + throw new IllegalArgumentException("Collection must not be null"); + } + Object[] arr = Objects.toObjectArray(array); + for (Object elem : arr) { + collection.add(elem); + } + } + + /** + * Merge the given Properties instance into the given Map, + * copying all properties (key-value pairs) over. + *

Uses Properties.propertyNames() to even catch + * default properties linked into the original Properties instance. + * @param props the Properties instance to merge (may be null) + * @param map the target Map to merge the properties into + */ + @SuppressWarnings("unchecked") + public static void mergePropertiesIntoMap(Properties props, Map map) { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + if (props != null) { + for (Enumeration en = props.propertyNames(); en.hasMoreElements();) { + String key = (String) en.nextElement(); + Object value = props.getProperty(key); + if (value == null) { + // Potentially a non-String value... + value = props.get(key); + } + map.put(key, value); + } + } + } + + + /** + * Check whether the given Iterator contains the given element. + * @param iterator the Iterator to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean contains(Iterator iterator, Object element) { + if (iterator != null) { + while (iterator.hasNext()) { + Object candidate = iterator.next(); + if (Objects.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Enumeration contains the given element. + * @param enumeration the Enumeration to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean contains(Enumeration enumeration, Object element) { + if (enumeration != null) { + while (enumeration.hasMoreElements()) { + Object candidate = enumeration.nextElement(); + if (Objects.nullSafeEquals(candidate, element)) { + return true; + } + } + } + return false; + } + + /** + * Check whether the given Collection contains the given element instance. + *

Enforces the given instance to be present, rather than returning + * true for an equal element as well. + * @param collection the Collection to check + * @param element the element to look for + * @return true if found, false else + */ + public static boolean containsInstance(Collection collection, Object element) { + if (collection != null) { + for (Object candidate : collection) { + if (candidate == element) { + return true; + } + } + } + return false; + } + + /** + * Return true if any element in 'candidates' is + * contained in 'source'; otherwise returns false. + * @param source the source Collection + * @param candidates the candidates to search for + * @return whether any of the candidates has been found + */ + public static boolean containsAny(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return false; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return true; + } + } + return false; + } + + /** + * Return the first element in 'candidates' that is contained in + * 'source'. If no element in 'candidates' is present in + * 'source' returns null. Iteration order is + * {@link Collection} implementation specific. + * @param source the source Collection + * @param candidates the candidates to search for + * @return the first present object, or null if not found + */ + public static Object findFirstMatch(Collection source, Collection candidates) { + if (isEmpty(source) || isEmpty(candidates)) { + return null; + } + for (Object candidate : candidates) { + if (source.contains(candidate)) { + return candidate; + } + } + return null; + } + + /** + * Find a single value of the given type in the given Collection. + * @param collection the Collection to search + * @param type the type to look for + * @return a value of the given type found if there is a clear match, + * or null if none or more than one such value found + */ + @SuppressWarnings("unchecked") + public static T findValueOfType(Collection collection, Class type) { + if (isEmpty(collection)) { + return null; + } + T value = null; + for (Object element : collection) { + if (type == null || type.isInstance(element)) { + if (value != null) { + // More than one value found... no clear single value. + return null; + } + value = (T) element; + } + } + return value; + } + + /** + * Find a single value of one of the given types in the given Collection: + * searching the Collection for a value of the first type, then + * searching for a value of the second type, etc. + * @param collection the collection to search + * @param types the types to look for, in prioritized order + * @return a value of one of the given types found if there is a clear match, + * or null if none or more than one such value found + */ + public static Object findValueOfType(Collection collection, Class[] types) { + if (isEmpty(collection) || Objects.isEmpty(types)) { + return null; + } + for (Class type : types) { + Object value = findValueOfType(collection, type); + if (value != null) { + return value; + } + } + return null; + } + + /** + * Determine whether the given Collection only contains a single unique object. + * @param collection the Collection to check + * @return true if the collection contains a single reference or + * multiple references to the same instance, false else + */ + public static boolean hasUniqueObject(Collection collection) { + if (isEmpty(collection)) { + return false; + } + boolean hasCandidate = false; + Object candidate = null; + for (Object elem : collection) { + if (!hasCandidate) { + hasCandidate = true; + candidate = elem; + } + else if (candidate != elem) { + return false; + } + } + return true; + } + + /** + * Find the common element type of the given Collection, if any. + * @param collection the Collection to check + * @return the common element type, or null if no clear + * common type has been found (or the collection was empty) + */ + public static Class findCommonElementType(Collection collection) { + if (isEmpty(collection)) { + return null; + } + Class candidate = null; + for (Object val : collection) { + if (val != null) { + if (candidate == null) { + candidate = val.getClass(); + } + else if (candidate != val.getClass()) { + return null; + } + } + } + return candidate; + } + + /** + * Marshal the elements from the given enumeration into an array of the given type. + * Enumeration elements must be assignable to the type of the given array. The array + * returned will be a different instance than the array given. + */ + public static A[] toArray(Enumeration enumeration, A[] array) { + ArrayList elements = new ArrayList(); + while (enumeration.hasMoreElements()) { + elements.add(enumeration.nextElement()); + } + return elements.toArray(array); + } + + /** + * Adapt an enumeration to an iterator. + * @param enumeration the enumeration + * @return the iterator + */ + public static Iterator toIterator(Enumeration enumeration) { + return new EnumerationIterator(enumeration); + } + + /** + * Iterator wrapping an Enumeration. + */ + private static class EnumerationIterator implements Iterator { + + private Enumeration enumeration; + + public EnumerationIterator(Enumeration enumeration) { + this.enumeration = enumeration; + } + + public boolean hasNext() { + return this.enumeration.hasMoreElements(); + } + + public E next() { + return this.enumeration.nextElement(); + } + + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Not supported"); + } + } +} + diff --git a/src/main/java/io/jsonwebtoken/lang/InstantiationException.java b/src/main/java/io/jsonwebtoken/lang/InstantiationException.java new file mode 100644 index 00000000..0ee8418f --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/InstantiationException.java @@ -0,0 +1,26 @@ +/* + * 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.lang; + +/** + * @since 0.1 + */ +public class InstantiationException extends RuntimeException { + + public InstantiationException(String s, Throwable t) { + super(s, t); + } +} diff --git a/src/main/java/io/jsonwebtoken/lang/Objects.java b/src/main/java/io/jsonwebtoken/lang/Objects.java new file mode 100644 index 00000000..dbdc84ce --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/Objects.java @@ -0,0 +1,908 @@ +/* + * 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.lang; + +import java.lang.reflect.Array; +import java.util.Arrays; + +public abstract class Objects { + + private static final int INITIAL_HASH = 7; + private static final int MULTIPLIER = 31; + + private static final String EMPTY_STRING = ""; + private static final String NULL_STRING = "null"; + private static final String ARRAY_START = "{"; + private static final String ARRAY_END = "}"; + private static final String EMPTY_ARRAY = ARRAY_START + ARRAY_END; + private static final String ARRAY_ELEMENT_SEPARATOR = ", "; + + /** + * Return whether the given throwable is a checked exception: + * that is, neither a RuntimeException nor an Error. + * + * @param ex the throwable to check + * @return whether the throwable is a checked exception + * @see java.lang.Exception + * @see java.lang.RuntimeException + * @see java.lang.Error + */ + public static boolean isCheckedException(Throwable ex) { + return !(ex instanceof RuntimeException || ex instanceof Error); + } + + /** + * Check whether the given exception is compatible with the exceptions + * declared in a throws clause. + * + * @param ex the exception to checked + * @param declaredExceptions the exceptions declared in the throws clause + * @return whether the given exception is compatible + */ + public static boolean isCompatibleWithThrowsClause(Throwable ex, Class[] declaredExceptions) { + if (!isCheckedException(ex)) { + return true; + } + if (declaredExceptions != null) { + int i = 0; + while (i < declaredExceptions.length) { + if (declaredExceptions[i].isAssignableFrom(ex.getClass())) { + return true; + } + i++; + } + } + return false; + } + + /** + * Determine whether the given object is an array: + * either an Object array or a primitive array. + * + * @param obj the object to check + */ + public static boolean isArray(Object obj) { + return (obj != null && obj.getClass().isArray()); + } + + /** + * Determine whether the given array is empty: + * i.e. null or of zero length. + * + * @param array the array to check + */ + public static boolean isEmpty(Object[] array) { + return (array == null || array.length == 0); + } + + /** + * Returns {@code true} if the specified byte array is null or of zero length, {@code false} otherwise. + * + * @param array the byte array to check + * @return {@code true} if the specified byte array is null or of zero length, {@code false} otherwise. + */ + public static boolean isEmpty(byte[] array) { + return array == null || array.length == 0; + } + + /** + * Check whether the given array contains the given element. + * + * @param array the array to check (may be null, + * in which case the return value will always be false) + * @param element the element to check for + * @return whether the element has been found in the given array + */ + public static boolean containsElement(Object[] array, Object element) { + if (array == null) { + return false; + } + for (Object arrayEle : array) { + if (nullSafeEquals(arrayEle, element)) { + return true; + } + } + return false; + } + + /** + * Check whether the given array of enum constants contains a constant with the given name, + * ignoring case when determining a match. + * + * @param enumValues the enum values to check, typically the product of a call to MyEnum.values() + * @param constant the constant name to find (must not be null or empty string) + * @return whether the constant has been found in the given array + */ + public static boolean containsConstant(Enum[] enumValues, String constant) { + return containsConstant(enumValues, constant, false); + } + + /** + * Check whether the given array of enum constants contains a constant with the given name. + * + * @param enumValues the enum values to check, typically the product of a call to MyEnum.values() + * @param constant the constant name to find (must not be null or empty string) + * @param caseSensitive whether case is significant in determining a match + * @return whether the constant has been found in the given array + */ + public static boolean containsConstant(Enum[] enumValues, String constant, boolean caseSensitive) { + for (Enum candidate : enumValues) { + if (caseSensitive ? + candidate.toString().equals(constant) : + candidate.toString().equalsIgnoreCase(constant)) { + return true; + } + } + return false; + } + + /** + * Case insensitive alternative to {@link Enum#valueOf(Class, String)}. + * + * @param the concrete Enum type + * @param enumValues the array of all Enum constants in question, usually per Enum.values() + * @param constant the constant to get the enum value of + * @throws IllegalArgumentException if the given constant is not found in the given array + * of enum values. Use {@link #containsConstant(Enum[], String)} as a guard to + * avoid this exception. + */ + public static > E caseInsensitiveValueOf(E[] enumValues, String constant) { + for (E candidate : enumValues) { + if (candidate.toString().equalsIgnoreCase(constant)) { + return candidate; + } + } + throw new IllegalArgumentException( + String.format("constant [%s] does not exist in enum type %s", + constant, enumValues.getClass().getComponentType().getName())); + } + + /** + * Append the given object to the given array, returning a new array + * consisting of the input array contents plus the given object. + * + * @param array the array to append to (can be null) + * @param obj the object to append + * @return the new array (of the same component type; never null) + */ + public static A[] addObjectToArray(A[] array, O obj) { + Class compType = Object.class; + if (array != null) { + compType = array.getClass().getComponentType(); + } else if (obj != null) { + compType = obj.getClass(); + } + int newArrLength = (array != null ? array.length + 1 : 1); + @SuppressWarnings("unchecked") + A[] newArr = (A[]) Array.newInstance(compType, newArrLength); + if (array != null) { + System.arraycopy(array, 0, newArr, 0, array.length); + } + newArr[newArr.length - 1] = obj; + return newArr; + } + + /** + * Convert the given array (which may be a primitive array) to an + * object array (if necessary of primitive wrapper objects). + *

A null source value will be converted to an + * empty Object array. + * + * @param source the (potentially primitive) array + * @return the corresponding object array (never null) + * @throws IllegalArgumentException if the parameter is not an array + */ + public static Object[] toObjectArray(Object source) { + if (source instanceof Object[]) { + return (Object[]) source; + } + if (source == null) { + return new Object[0]; + } + if (!source.getClass().isArray()) { + throw new IllegalArgumentException("Source is not an array: " + source); + } + int length = Array.getLength(source); + if (length == 0) { + return new Object[0]; + } + Class wrapperType = Array.get(source, 0).getClass(); + Object[] newArray = (Object[]) Array.newInstance(wrapperType, length); + for (int i = 0; i < length; i++) { + newArray[i] = Array.get(source, i); + } + return newArray; + } + + + //--------------------------------------------------------------------- + // Convenience methods for content-based equality/hash-code handling + //--------------------------------------------------------------------- + + /** + * Determine if the given objects are equal, returning true + * if both are null or false if only one is + * null. + *

Compares arrays with Arrays.equals, performing an equality + * check based on the array elements rather than the array reference. + * + * @param o1 first Object to compare + * @param o2 second Object to compare + * @return whether the given objects are equal + * @see java.util.Arrays#equals + */ + public static boolean nullSafeEquals(Object o1, Object o2) { + if (o1 == o2) { + return true; + } + if (o1 == null || o2 == null) { + return false; + } + if (o1.equals(o2)) { + return true; + } + if (o1.getClass().isArray() && o2.getClass().isArray()) { + if (o1 instanceof Object[] && o2 instanceof Object[]) { + return Arrays.equals((Object[]) o1, (Object[]) o2); + } + if (o1 instanceof boolean[] && o2 instanceof boolean[]) { + return Arrays.equals((boolean[]) o1, (boolean[]) o2); + } + if (o1 instanceof byte[] && o2 instanceof byte[]) { + return Arrays.equals((byte[]) o1, (byte[]) o2); + } + if (o1 instanceof char[] && o2 instanceof char[]) { + return Arrays.equals((char[]) o1, (char[]) o2); + } + if (o1 instanceof double[] && o2 instanceof double[]) { + return Arrays.equals((double[]) o1, (double[]) o2); + } + if (o1 instanceof float[] && o2 instanceof float[]) { + return Arrays.equals((float[]) o1, (float[]) o2); + } + if (o1 instanceof int[] && o2 instanceof int[]) { + return Arrays.equals((int[]) o1, (int[]) o2); + } + if (o1 instanceof long[] && o2 instanceof long[]) { + return Arrays.equals((long[]) o1, (long[]) o2); + } + if (o1 instanceof short[] && o2 instanceof short[]) { + return Arrays.equals((short[]) o1, (short[]) o2); + } + } + return false; + } + + /** + * Return as hash code for the given object; typically the value of + * {@link Object#hashCode()}. If the object is an array, + * this method will delegate to any of the nullSafeHashCode + * methods for arrays in this class. If the object is null, + * this method returns 0. + * + * @see #nullSafeHashCode(Object[]) + * @see #nullSafeHashCode(boolean[]) + * @see #nullSafeHashCode(byte[]) + * @see #nullSafeHashCode(char[]) + * @see #nullSafeHashCode(double[]) + * @see #nullSafeHashCode(float[]) + * @see #nullSafeHashCode(int[]) + * @see #nullSafeHashCode(long[]) + * @see #nullSafeHashCode(short[]) + */ + public static int nullSafeHashCode(Object obj) { + if (obj == null) { + return 0; + } + if (obj.getClass().isArray()) { + if (obj instanceof Object[]) { + return nullSafeHashCode((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeHashCode((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeHashCode((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeHashCode((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeHashCode((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeHashCode((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeHashCode((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeHashCode((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeHashCode((short[]) obj); + } + } + return obj.hashCode(); + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(Object[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + nullSafeHashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(boolean[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(byte[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(char[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(double[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(float[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(int[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(long[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + hashCode(array[i]); + } + return hash; + } + + /** + * Return a hash code based on the contents of the specified array. + * If array is null, this method returns 0. + */ + public static int nullSafeHashCode(short[] array) { + if (array == null) { + return 0; + } + int hash = INITIAL_HASH; + int arraySize = array.length; + for (int i = 0; i < arraySize; i++) { + hash = MULTIPLIER * hash + array[i]; + } + return hash; + } + + /** + * Return the same value as {@link Boolean#hashCode()}. + * + * @see Boolean#hashCode() + */ + public static int hashCode(boolean bool) { + return bool ? 1231 : 1237; + } + + /** + * Return the same value as {@link Double#hashCode()}. + * + * @see Double#hashCode() + */ + public static int hashCode(double dbl) { + long bits = Double.doubleToLongBits(dbl); + return hashCode(bits); + } + + /** + * Return the same value as {@link Float#hashCode()}. + * + * @see Float#hashCode() + */ + public static int hashCode(float flt) { + return Float.floatToIntBits(flt); + } + + /** + * Return the same value as {@link Long#hashCode()}. + * + * @see Long#hashCode() + */ + public static int hashCode(long lng) { + return (int) (lng ^ (lng >>> 32)); + } + + + //--------------------------------------------------------------------- + // Convenience methods for toString output + //--------------------------------------------------------------------- + + /** + * Return a String representation of an object's overall identity. + * + * @param obj the object (may be null) + * @return the object's identity as String representation, + * or an empty String if the object was null + */ + public static String identityToString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return obj.getClass().getName() + "@" + getIdentityHexString(obj); + } + + /** + * Return a hex String form of an object's identity hash code. + * + * @param obj the object + * @return the object's identity code in hex notation + */ + public static String getIdentityHexString(Object obj) { + return Integer.toHexString(System.identityHashCode(obj)); + } + + /** + * Return a content-based String representation if obj is + * not null; otherwise returns an empty String. + *

Differs from {@link #nullSafeToString(Object)} in that it returns + * an empty String rather than "null" for a null value. + * + * @param obj the object to build a display String for + * @return a display String representation of obj + * @see #nullSafeToString(Object) + */ + public static String getDisplayString(Object obj) { + if (obj == null) { + return EMPTY_STRING; + } + return nullSafeToString(obj); + } + + /** + * Determine the class name for the given object. + *

Returns "null" if obj is null. + * + * @param obj the object to introspect (may be null) + * @return the corresponding class name + */ + public static String nullSafeClassName(Object obj) { + return (obj != null ? obj.getClass().getName() : NULL_STRING); + } + + /** + * Return a String representation of the specified Object. + *

Builds a String representation of the contents in case of an array. + * Returns "null" if obj is null. + * + * @param obj the object to build a String representation for + * @return a String representation of obj + */ + public static String nullSafeToString(Object obj) { + if (obj == null) { + return NULL_STRING; + } + if (obj instanceof String) { + return (String) obj; + } + if (obj instanceof Object[]) { + return nullSafeToString((Object[]) obj); + } + if (obj instanceof boolean[]) { + return nullSafeToString((boolean[]) obj); + } + if (obj instanceof byte[]) { + return nullSafeToString((byte[]) obj); + } + if (obj instanceof char[]) { + return nullSafeToString((char[]) obj); + } + if (obj instanceof double[]) { + return nullSafeToString((double[]) obj); + } + if (obj instanceof float[]) { + return nullSafeToString((float[]) obj); + } + if (obj instanceof int[]) { + return nullSafeToString((int[]) obj); + } + if (obj instanceof long[]) { + return nullSafeToString((long[]) obj); + } + if (obj instanceof short[]) { + return nullSafeToString((short[]) obj); + } + String str = obj.toString(); + return (str != null ? str : EMPTY_STRING); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(Object[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(String.valueOf(array[i])); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(boolean[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(byte[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(char[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append("'").append(array[i]).append("'"); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(double[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(float[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(int[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(long[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + + /** + * Return a String representation of the contents of the specified array. + *

The String representation consists of a list of the array's elements, + * enclosed in curly braces ("{}"). Adjacent elements are separated + * by the characters ", " (a comma followed by a space). Returns + * "null" if array is null. + * + * @param array the array to build a String representation for + * @return a String representation of array + */ + public static String nullSafeToString(short[] array) { + if (array == null) { + return NULL_STRING; + } + int length = array.length; + if (length == 0) { + return EMPTY_ARRAY; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + if (i == 0) { + sb.append(ARRAY_START); + } else { + sb.append(ARRAY_ELEMENT_SEPARATOR); + } + sb.append(array[i]); + } + sb.append(ARRAY_END); + return sb.toString(); + } + +} diff --git a/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java b/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java new file mode 100644 index 00000000..0e41176a --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java @@ -0,0 +1,61 @@ +/* + * 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.lang; + +import java.security.Provider; +import java.security.Security; +import java.util.concurrent.atomic.AtomicBoolean; + +public class RuntimeEnvironment { + + private static final String BC_PROVIDER_CLASS_NAME = "org.bouncycastle.jce.provider.BouncyCastleProvider"; + + private static final AtomicBoolean bcLoaded = new AtomicBoolean(false); + + public static void enableBouncyCastleIfPossible() { + + if (bcLoaded.get()) { + return; + } + + try { + Class clazz = Classes.forName(BC_PROVIDER_CLASS_NAME); + + //check to see if the user has already registered the BC provider: + + Provider[] providers = Security.getProviders(); + + for(Provider provider : providers) { + if (clazz.isInstance(provider)) { + bcLoaded.set(true); + return; + } + } + + //bc provider not enabled - add it: + Security.addProvider((Provider)Classes.newInstance(clazz)); + bcLoaded.set(true); + + } catch (UnknownClassException e) { + //not available + } + } + + static { + enableBouncyCastleIfPossible(); + } + +} diff --git a/src/main/java/io/jsonwebtoken/lang/Strings.java b/src/main/java/io/jsonwebtoken/lang/Strings.java new file mode 100644 index 00000000..3fa1b6ca --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/Strings.java @@ -0,0 +1,1126 @@ +/* + * 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.lang; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.TreeSet; + +public abstract class Strings { + + private static final String FOLDER_SEPARATOR = "/"; + + private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + + private static final String TOP_PATH = ".."; + + private static final String CURRENT_PATH = "."; + + private static final char EXTENSION_SEPARATOR = '.'; + + //--------------------------------------------------------------------- + // General convenience methods for working with Strings + //--------------------------------------------------------------------- + + /** + * Check that the given CharSequence is neither null nor of length 0. + * Note: Will return true for a CharSequence that purely consists of whitespace. + *

+     * Strings.hasLength(null) = false
+     * Strings.hasLength("") = false
+     * Strings.hasLength(" ") = true
+     * Strings.hasLength("Hello") = true
+     * 
+ * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not null and has length + * @see #hasText(String) + */ + public static boolean hasLength(CharSequence str) { + return (str != null && str.length() > 0); + } + + /** + * Check that the given String is neither null nor of length 0. + * Note: Will return true for a String that purely consists of whitespace. + * @param str the String to check (may be null) + * @return true if the String is not null and has length + * @see #hasLength(CharSequence) + */ + public static boolean hasLength(String str) { + return hasLength((CharSequence) str); + } + + /** + * Check whether the given CharSequence has actual text. + * More specifically, returns true if the string not null, + * its length is greater than 0, and it contains at least one non-whitespace character. + *

+     * Strings.hasText(null) = false
+     * Strings.hasText("") = false
+     * Strings.hasText(" ") = false
+     * Strings.hasText("12345") = true
+     * Strings.hasText(" 12345 ") = true
+     * 
+ * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not null, + * its length is greater than 0, and it does not contain whitespace only + * @see java.lang.Character#isWhitespace + */ + public static boolean hasText(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String has actual text. + * More specifically, returns true if the string not null, + * its length is greater than 0, and it contains at least one non-whitespace character. + * @param str the String to check (may be null) + * @return true if the String is not null, its length is + * greater than 0, and it does not contain whitespace only + * @see #hasText(CharSequence) + */ + public static boolean hasText(String str) { + return hasText((CharSequence) str); + } + + /** + * Check whether the given CharSequence contains any whitespace characters. + * @param str the CharSequence to check (may be null) + * @return true if the CharSequence is not empty and + * contains at least 1 whitespace character + * @see java.lang.Character#isWhitespace + */ + public static boolean containsWhitespace(CharSequence str) { + if (!hasLength(str)) { + return false; + } + int strLen = str.length(); + for (int i = 0; i < strLen; i++) { + if (Character.isWhitespace(str.charAt(i))) { + return true; + } + } + return false; + } + + /** + * Check whether the given String contains any whitespace characters. + * @param str the String to check (may be null) + * @return true if the String is not empty and + * contains at least 1 whitespace character + * @see #containsWhitespace(CharSequence) + */ + public static boolean containsWhitespace(String str) { + return containsWhitespace((CharSequence) str); + } + + /** + * Trim leading and trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + public static String clean(String str) { + str = trimWhitespace(str); + if ("".equals(str)) { + return null; + } + return str; + } + + /** + * Trim all whitespace from the given String: + * leading, trailing, and inbetween characters. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimAllWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + int index = 0; + while (sb.length() > index) { + if (Character.isWhitespace(sb.charAt(index))) { + sb.deleteCharAt(index); + } + else { + index++; + } + } + return sb.toString(); + } + + /** + * Trim leading whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimLeadingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0))) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim trailing whitespace from the given String. + * @param str the String to check + * @return the trimmed String + * @see java.lang.Character#isWhitespace + */ + public static String trimTrailingWhitespace(String str) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1))) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + /** + * Trim all occurences of the supplied leading character from the given String. + * @param str the String to check + * @param leadingCharacter the leading character to be trimmed + * @return the trimmed String + */ + public static String trimLeadingCharacter(String str, char leadingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(0) == leadingCharacter) { + sb.deleteCharAt(0); + } + return sb.toString(); + } + + /** + * Trim all occurences of the supplied trailing character from the given String. + * @param str the String to check + * @param trailingCharacter the trailing character to be trimmed + * @return the trimmed String + */ + public static String trimTrailingCharacter(String str, char trailingCharacter) { + if (!hasLength(str)) { + return str; + } + StringBuilder sb = new StringBuilder(str); + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == trailingCharacter) { + sb.deleteCharAt(sb.length() - 1); + } + return sb.toString(); + } + + + /** + * Test if the given String starts with the specified prefix, + * ignoring upper/lower case. + * @param str the String to check + * @param prefix the prefix to look for + * @see java.lang.String#startsWith + */ + public static boolean startsWithIgnoreCase(String str, String prefix) { + if (str == null || prefix == null) { + return false; + } + if (str.startsWith(prefix)) { + return true; + } + if (str.length() < prefix.length()) { + return false; + } + String lcStr = str.substring(0, prefix.length()).toLowerCase(); + String lcPrefix = prefix.toLowerCase(); + return lcStr.equals(lcPrefix); + } + + /** + * Test if the given String ends with the specified suffix, + * ignoring upper/lower case. + * @param str the String to check + * @param suffix the suffix to look for + * @see java.lang.String#endsWith + */ + public static boolean endsWithIgnoreCase(String str, String suffix) { + if (str == null || suffix == null) { + return false; + } + if (str.endsWith(suffix)) { + return true; + } + if (str.length() < suffix.length()) { + return false; + } + + String lcStr = str.substring(str.length() - suffix.length()).toLowerCase(); + String lcSuffix = suffix.toLowerCase(); + return lcStr.equals(lcSuffix); + } + + /** + * Test whether the given string matches the given substring + * at the given index. + * @param str the original string (or StringBuilder) + * @param index the index in the original string to start matching against + * @param substring the substring to match at the given index + */ + public static boolean substringMatch(CharSequence str, int index, CharSequence substring) { + for (int j = 0; j < substring.length(); j++) { + int i = index + j; + if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { + return false; + } + } + return true; + } + + /** + * Count the occurrences of the substring in string s. + * @param str string to search in. Return 0 if this is null. + * @param sub string to search for. Return 0 if this is null. + */ + public static int countOccurrencesOf(String str, String sub) { + if (str == null || sub == null || str.length() == 0 || sub.length() == 0) { + return 0; + } + int count = 0; + int pos = 0; + int idx; + while ((idx = str.indexOf(sub, pos)) != -1) { + ++count; + pos = idx + sub.length(); + } + return count; + } + + /** + * Replace all occurences of a substring within a string with + * another string. + * @param inString String to examine + * @param oldPattern String to replace + * @param newPattern String to insert + * @return a String with the replacements + */ + public static String replace(String inString, String oldPattern, String newPattern) { + if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) { + return inString; + } + StringBuilder sb = new StringBuilder(); + int pos = 0; // our position in the old string + int index = inString.indexOf(oldPattern); + // the index of an occurrence we've found, or -1 + int patLen = oldPattern.length(); + while (index >= 0) { + sb.append(inString.substring(pos, index)); + sb.append(newPattern); + pos = index + patLen; + index = inString.indexOf(oldPattern, pos); + } + sb.append(inString.substring(pos)); + // remember to append any characters to the right of a match + return sb.toString(); + } + + /** + * Delete all occurrences of the given substring. + * @param inString the original String + * @param pattern the pattern to delete all occurrences of + * @return the resulting String + */ + public static String delete(String inString, String pattern) { + return replace(inString, pattern, ""); + } + + /** + * Delete any character in a given String. + * @param inString the original String + * @param charsToDelete a set of characters to delete. + * E.g. "az\n" will delete 'a's, 'z's and new lines. + * @return the resulting String + */ + public static String deleteAny(String inString, String charsToDelete) { + if (!hasLength(inString) || !hasLength(charsToDelete)) { + return inString; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); + if (charsToDelete.indexOf(c) == -1) { + sb.append(c); + } + } + return sb.toString(); + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with formatted Strings + //--------------------------------------------------------------------- + + /** + * Quote the given String with single quotes. + * @param str the input String (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or null if the input was null + */ + public static String quote(String str) { + return (str != null ? "'" + str + "'" : null); + } + + /** + * Turn the given Object into a String with single quotes + * if it is a String; keeping the Object as-is else. + * @param obj the input Object (e.g. "myString") + * @return the quoted String (e.g. "'myString'"), + * or the input object as-is if not a String + */ + public static Object quoteIfString(Object obj) { + return (obj instanceof String ? quote((String) obj) : obj); + } + + /** + * Unqualify a string qualified by a '.' dot character. For example, + * "this.name.is.qualified", returns "qualified". + * @param qualifiedName the qualified name + */ + public static String unqualify(String qualifiedName) { + return unqualify(qualifiedName, '.'); + } + + /** + * Unqualify a string qualified by a separator character. For example, + * "this:name:is:qualified" returns "qualified" if using a ':' separator. + * @param qualifiedName the qualified name + * @param separator the separator + */ + public static String unqualify(String qualifiedName, char separator) { + return qualifiedName.substring(qualifiedName.lastIndexOf(separator) + 1); + } + + /** + * Capitalize a String, changing the first letter to + * upper case as per {@link Character#toUpperCase(char)}. + * No other letters are changed. + * @param str the String to capitalize, may be null + * @return the capitalized String, null if null + */ + public static String capitalize(String str) { + return changeFirstCharacterCase(str, true); + } + + /** + * Uncapitalize a String, changing the first letter to + * lower case as per {@link Character#toLowerCase(char)}. + * No other letters are changed. + * @param str the String to uncapitalize, may be null + * @return the uncapitalized String, null if null + */ + public static String uncapitalize(String str) { + return changeFirstCharacterCase(str, false); + } + + private static String changeFirstCharacterCase(String str, boolean capitalize) { + if (str == null || str.length() == 0) { + return str; + } + StringBuilder sb = new StringBuilder(str.length()); + if (capitalize) { + sb.append(Character.toUpperCase(str.charAt(0))); + } + else { + sb.append(Character.toLowerCase(str.charAt(0))); + } + sb.append(str.substring(1)); + return sb.toString(); + } + + /** + * Extract the filename from the given path, + * e.g. "mypath/myfile.txt" -> "myfile.txt". + * @param path the file path (may be null) + * @return the extracted filename, or null if none + */ + public static String getFilename(String path) { + if (path == null) { + return null; + } + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + } + + /** + * Extract the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "txt". + * @param path the file path (may be null) + * @return the extracted filename extension, or null if none + */ + public static String getFilenameExtension(String path) { + if (path == null) { + return null; + } + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return null; + } + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return null; + } + return path.substring(extIndex + 1); + } + + /** + * Strip the filename extension from the given path, + * e.g. "mypath/myfile.txt" -> "mypath/myfile". + * @param path the file path (may be null) + * @return the path with stripped filename extension, + * or null if none + */ + public static String stripFilenameExtension(String path) { + if (path == null) { + return null; + } + int extIndex = path.lastIndexOf(EXTENSION_SEPARATOR); + if (extIndex == -1) { + return path; + } + int folderIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (folderIndex > extIndex) { + return path; + } + return path.substring(0, extIndex); + } + + /** + * Apply the given relative path to the given path, + * assuming standard Java folder separation (i.e. "/" separators). + * @param path the path to start from (usually a full file path) + * @param relativePath the relative path to apply + * (relative to the full file path above) + * @return the full file path that results from applying the relative path + */ + public static String applyRelativePath(String path, String relativePath) { + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + if (separatorIndex != -1) { + String newPath = path.substring(0, separatorIndex); + if (!relativePath.startsWith(FOLDER_SEPARATOR)) { + newPath += FOLDER_SEPARATOR; + } + return newPath + relativePath; + } + else { + return relativePath; + } + } + + /** + * Normalize the path by suppressing sequences like "path/.." and + * inner simple dots. + *

The result is convenient for path comparison. For other uses, + * notice that Windows separators ("\") are replaced by simple slashes. + * @param path the original path + * @return the normalized path + */ + public static String cleanPath(String path) { + if (path == null) { + return null; + } + String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + + // Strip prefix from path to analyze, to not treat it as part of the + // first path element. This is necessary to correctly parse paths like + // "file:core/../core/io/Resource.class", where the ".." should just + // strip the first "core" directory while keeping the "file:" prefix. + int prefixIndex = pathToUse.indexOf(":"); + String prefix = ""; + if (prefixIndex != -1) { + prefix = pathToUse.substring(0, prefixIndex + 1); + pathToUse = pathToUse.substring(prefixIndex + 1); + } + if (pathToUse.startsWith(FOLDER_SEPARATOR)) { + prefix = prefix + FOLDER_SEPARATOR; + pathToUse = pathToUse.substring(1); + } + + String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR); + List pathElements = new LinkedList(); + int tops = 0; + + for (int i = pathArray.length - 1; i >= 0; i--) { + String element = pathArray[i]; + if (CURRENT_PATH.equals(element)) { + // Points to current directory - drop it. + } + else if (TOP_PATH.equals(element)) { + // Registering top path found. + tops++; + } + else { + if (tops > 0) { + // Merging path element with element corresponding to top path. + tops--; + } + else { + // Normal path element found. + pathElements.add(0, element); + } + } + } + + // Remaining top paths need to be retained. + for (int i = 0; i < tops; i++) { + pathElements.add(0, TOP_PATH); + } + + return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); + } + + /** + * Compare two paths after normalization of them. + * @param path1 first path for comparison + * @param path2 second path for comparison + * @return whether the two paths are equivalent after normalization + */ + public static boolean pathEquals(String path1, String path2) { + return cleanPath(path1).equals(cleanPath(path2)); + } + + /** + * Parse the given localeString value into a {@link java.util.Locale}. + *

This is the inverse operation of {@link java.util.Locale#toString Locale's toString}. + * @param localeString the locale string, following Locale's + * toString() format ("en", "en_UK", etc); + * also accepts spaces as separators, as an alternative to underscores + * @return a corresponding Locale instance + */ + public static Locale parseLocaleString(String localeString) { + String[] parts = tokenizeToStringArray(localeString, "_ ", false, false); + String language = (parts.length > 0 ? parts[0] : ""); + String country = (parts.length > 1 ? parts[1] : ""); + validateLocalePart(language); + validateLocalePart(country); + String variant = ""; + if (parts.length >= 2) { + // There is definitely a variant, and it is everything after the country + // code sans the separator between the country code and the variant. + int endIndexOfCountryCode = localeString.indexOf(country) + country.length(); + // Strip off any leading '_' and whitespace, what's left is the variant. + variant = trimLeadingWhitespace(localeString.substring(endIndexOfCountryCode)); + if (variant.startsWith("_")) { + variant = trimLeadingCharacter(variant, '_'); + } + } + return (language.length() > 0 ? new Locale(language, country, variant) : null); + } + + private static void validateLocalePart(String localePart) { + for (int i = 0; i < localePart.length(); i++) { + char ch = localePart.charAt(i); + if (ch != '_' && ch != ' ' && !Character.isLetterOrDigit(ch)) { + throw new IllegalArgumentException( + "Locale part \"" + localePart + "\" contains invalid characters"); + } + } + } + + /** + * Determine the RFC 3066 compliant language tag, + * as used for the HTTP "Accept-Language" header. + * @param locale the Locale to transform to a language tag + * @return the RFC 3066 compliant language tag as String + */ + public static String toLanguageTag(Locale locale) { + return locale.getLanguage() + (hasText(locale.getCountry()) ? "-" + locale.getCountry() : ""); + } + + + //--------------------------------------------------------------------- + // Convenience methods for working with String arrays + //--------------------------------------------------------------------- + + /** + * Append the given String to the given String array, returning a new array + * consisting of the input array contents plus the given String. + * @param array the array to append to (can be null) + * @param str the String to append + * @return the new array (never null) + */ + public static String[] addStringToArray(String[] array, String str) { + if (Objects.isEmpty(array)) { + return new String[] {str}; + } + String[] newArr = new String[array.length + 1]; + System.arraycopy(array, 0, newArr, 0, array.length); + newArr[array.length] = str; + return newArr; + } + + /** + * Concatenate the given String arrays into one, + * with overlapping array elements included twice. + *

The order of elements in the original arrays is preserved. + * @param array1 the first array (can be null) + * @param array2 the second array (can be null) + * @return the new array (null if both given arrays were null) + */ + public static String[] concatenateStringArrays(String[] array1, String[] array2) { + if (Objects.isEmpty(array1)) { + return array2; + } + if (Objects.isEmpty(array2)) { + return array1; + } + String[] newArr = new String[array1.length + array2.length]; + System.arraycopy(array1, 0, newArr, 0, array1.length); + System.arraycopy(array2, 0, newArr, array1.length, array2.length); + return newArr; + } + + /** + * Merge the given String arrays into one, with overlapping + * array elements only included once. + *

The order of elements in the original arrays is preserved + * (with the exception of overlapping elements, which are only + * included on their first occurrence). + * @param array1 the first array (can be null) + * @param array2 the second array (can be null) + * @return the new array (null if both given arrays were null) + */ + public static String[] mergeStringArrays(String[] array1, String[] array2) { + if (Objects.isEmpty(array1)) { + return array2; + } + if (Objects.isEmpty(array2)) { + return array1; + } + List result = new ArrayList(); + result.addAll(Arrays.asList(array1)); + for (String str : array2) { + if (!result.contains(str)) { + result.add(str); + } + } + return toStringArray(result); + } + + /** + * Turn given source String array into sorted array. + * @param array the source array + * @return the sorted array (never null) + */ + public static String[] sortStringArray(String[] array) { + if (Objects.isEmpty(array)) { + return new String[0]; + } + Arrays.sort(array); + return array; + } + + /** + * Copy the given Collection into a String array. + * The Collection must contain String elements only. + * @param collection the Collection to copy + * @return the String array (null if the passed-in + * Collection was null) + */ + public static String[] toStringArray(Collection collection) { + if (collection == null) { + return null; + } + return collection.toArray(new String[collection.size()]); + } + + /** + * Copy the given Enumeration into a String array. + * The Enumeration must contain String elements only. + * @param enumeration the Enumeration to copy + * @return the String array (null if the passed-in + * Enumeration was null) + */ + public static String[] toStringArray(Enumeration enumeration) { + if (enumeration == null) { + return null; + } + List list = java.util.Collections.list(enumeration); + return list.toArray(new String[list.size()]); + } + + /** + * Trim the elements of the given String array, + * calling String.trim() on each of them. + * @param array the original String array + * @return the resulting array (of the same size) with trimmed elements + */ + public static String[] trimArrayElements(String[] array) { + if (Objects.isEmpty(array)) { + return new String[0]; + } + String[] result = new String[array.length]; + for (int i = 0; i < array.length; i++) { + String element = array[i]; + result[i] = (element != null ? element.trim() : null); + } + return result; + } + + /** + * Remove duplicate Strings from the given array. + * Also sorts the array, as it uses a TreeSet. + * @param array the String array + * @return an array without duplicates, in natural sort order + */ + public static String[] removeDuplicateStrings(String[] array) { + if (Objects.isEmpty(array)) { + return array; + } + Set set = new TreeSet(); + for (String element : array) { + set.add(element); + } + return toStringArray(set); + } + + /** + * Split a String at the first occurrence of the delimiter. + * Does not include the delimiter in the result. + * @param toSplit the string to split + * @param delimiter to split the string up with + * @return a two element array with index 0 being before the delimiter, and + * index 1 being after the delimiter (neither element includes the delimiter); + * or null if the delimiter wasn't found in the given input String + */ + public static String[] split(String toSplit, String delimiter) { + if (!hasLength(toSplit) || !hasLength(delimiter)) { + return null; + } + int offset = toSplit.indexOf(delimiter); + if (offset < 0) { + return null; + } + String beforeDelimiter = toSplit.substring(0, offset); + String afterDelimiter = toSplit.substring(offset + delimiter.length()); + return new String[] {beforeDelimiter, afterDelimiter}; + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A Properties instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

Will trim both the key and value before adding them to the + * Properties instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @return a Properties instance representing the array contents, + * or null if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { + return splitArrayElementsIntoProperties(array, delimiter, null); + } + + /** + * Take an array Strings and split each element based on the given delimiter. + * A Properties instance is then generated, with the left of the + * delimiter providing the key, and the right of the delimiter providing the value. + *

Will trim both the key and value before adding them to the + * Properties instance. + * @param array the array to process + * @param delimiter to split each element using (typically the equals symbol) + * @param charsToDelete one or more characters to remove from each element + * prior to attempting the split operation (typically the quotation mark + * symbol), or null if no removal should occur + * @return a Properties instance representing the array contents, + * or null if the array to process was null or empty + */ + public static Properties splitArrayElementsIntoProperties( + String[] array, String delimiter, String charsToDelete) { + + if (Objects.isEmpty(array)) { + return null; + } + Properties result = new Properties(); + for (String element : array) { + if (charsToDelete != null) { + element = deleteAny(element, charsToDelete); + } + String[] splittedElement = split(element, delimiter); + if (splittedElement == null) { + continue; + } + result.setProperty(splittedElement[0].trim(), splittedElement[1].trim()); + } + return result; + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + * Trims tokens and omits empty tokens. + *

The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter). + * @return an array of the tokens + * @see java.util.StringTokenizer + * @see java.lang.String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray(String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + *

The given delimiters string is supposed to consist of any number of + * delimiter characters. Each of those characters can be used to separate + * tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String + * (each of those characters is individually considered as delimiter) + * @param trimTokens trim the tokens via String's trim + * @param ignoreEmptyTokens omit empty tokens from the result array + * (only applies to tokens that are empty after trimming; StringTokenizer + * will not consider subsequent delimiters as token in the first place). + * @return an array of the tokens (null if the input String + * was null) + * @see java.util.StringTokenizer + * @see java.lang.String#trim() + * @see #delimitedListToStringArray + */ + public static String[] tokenizeToStringArray( + String str, String delimiters, boolean trimTokens, boolean ignoreEmptyTokens) { + + if (str == null) { + return null; + } + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + *

A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to tokenizeToStringArray. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter) { + return delimitedListToStringArray(str, delimiter, null); + } + + /** + * Take a String which is a delimited list and convert it to a String array. + *

A single delimiter can consists of more than one character: It will still + * be considered as single delimiter string, rather than as bunch of potential + * delimiter characters - in contrast to tokenizeToStringArray. + * @param str the input String + * @param delimiter the delimiter between elements (this is a single delimiter, + * rather than a bunch individual delimiter characters) + * @param charsToDelete a set of characters to delete. Useful for deleting unwanted + * line breaks: e.g. "\r\n\f" will delete all new lines and line feeds in a String. + * @return an array of the tokens in the list + * @see #tokenizeToStringArray + */ + public static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) { + if (str == null) { + return new String[0]; + } + if (delimiter == null) { + return new String[] {str}; + } + List result = new ArrayList(); + if ("".equals(delimiter)) { + for (int i = 0; i < str.length(); i++) { + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + } + } + else { + int pos = 0; + int delPos; + while ((delPos = str.indexOf(delimiter, pos)) != -1) { + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); + pos = delPos + delimiter.length(); + } + if (str.length() > 0 && pos <= str.length()) { + // Add rest of String, but not in case of empty input. + result.add(deleteAny(str.substring(pos), charsToDelete)); + } + } + return toStringArray(result); + } + + /** + * Convert a CSV list into an array of Strings. + * @param str the input String + * @return an array of Strings, or the empty array in case of empty input + */ + public static String[] commaDelimitedListToStringArray(String str) { + return delimitedListToStringArray(str, ","); + } + + /** + * Convenience method to convert a CSV string list to a set. + * Note that this will suppress duplicates. + * @param str the input String + * @return a Set of String entries in the list + */ + public static Set commaDelimitedListToSet(String str) { + Set set = new TreeSet(); + String[] tokens = commaDelimitedListToStringArray(str); + for (String token : tokens) { + set.add(token); + } + return set; + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @param prefix the String to start each element with + * @param suffix the String to end each element with + * @return the delimited String + */ + public static String collectionToDelimitedString(Collection coll, String delim, String prefix, String suffix) { + if (Collections.isEmpty(coll)) { + return ""; + } + StringBuilder sb = new StringBuilder(); + Iterator it = coll.iterator(); + while (it.hasNext()) { + sb.append(prefix).append(it.next()).append(suffix); + if (it.hasNext()) { + sb.append(delim); + } + } + return sb.toString(); + } + + /** + * Convenience method to return a Collection as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param coll the Collection to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String collectionToDelimitedString(Collection coll, String delim) { + return collectionToDelimitedString(coll, delim, "", ""); + } + + /** + * Convenience method to return a Collection as a CSV String. + * E.g. useful for toString() implementations. + * @param coll the Collection to display + * @return the delimited String + */ + public static String collectionToCommaDelimitedString(Collection coll) { + return collectionToDelimitedString(coll, ","); + } + + /** + * Convenience method to return a String array as a delimited (e.g. CSV) + * String. E.g. useful for toString() implementations. + * @param arr the array to display + * @param delim the delimiter to use (probably a ",") + * @return the delimited String + */ + public static String arrayToDelimitedString(Object[] arr, String delim) { + if (Objects.isEmpty(arr)) { + return ""; + } + if (arr.length == 1) { + return Objects.nullSafeToString(arr[0]); + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < arr.length; i++) { + if (i > 0) { + sb.append(delim); + } + sb.append(arr[i]); + } + return sb.toString(); + } + + /** + * Convenience method to return a String array as a CSV String. + * E.g. useful for toString() implementations. + * @param arr the array to display + * @return the delimited String + */ + public static String arrayToCommaDelimitedString(Object[] arr) { + return arrayToDelimitedString(arr, ","); + } + +} + diff --git a/src/main/java/io/jsonwebtoken/lang/UnknownClassException.java b/src/main/java/io/jsonwebtoken/lang/UnknownClassException.java new file mode 100644 index 00000000..a4eba997 --- /dev/null +++ b/src/main/java/io/jsonwebtoken/lang/UnknownClassException.java @@ -0,0 +1,61 @@ +/* + * 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.lang; + +/** + * A RuntimeException equivalent of the JDK's + * ClassNotFoundException, to maintain a RuntimeException paradigm. + * + * @since 0.1 + */ +public class UnknownClassException extends RuntimeException { + + /** + * Creates a new UnknownClassException. + */ + public UnknownClassException() { + super(); + } + + /** + * Constructs a new UnknownClassException. + * + * @param message the reason for the exception + */ + public UnknownClassException(String message) { + super(message); + } + + /** + * Constructs a new UnknownClassException. + * + * @param cause the underlying Throwable that caused this exception to be thrown. + */ + 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) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/src/test/groovy/io/jsonwebtoken/JWTsTest.groovy b/src/test/groovy/io/jsonwebtoken/JWTsTest.groovy new file mode 100644 index 00000000..4b67dff0 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/JWTsTest.groovy @@ -0,0 +1,244 @@ +/* + * 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 + +import org.testng.annotations.Test + +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.PrivateKey +import java.security.PublicKey +import java.security.SecureRandom + +import static org.testng.Assert.* + +class JWTsTest { + + @Test + void testPlaintextJwtString() { + + // Assert exact output per example at https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1 + + // The base64url encoding of the example claims set in the spec shows that their original payload ends lines with + // carriage return + newline, so we have to include them in the test payload to assert our encoded output + // matches what is in the spec: + + def payload = '{"iss":"joe",\r\n' + + ' "exp":1300819380,\r\n' + + ' "http://example.com/is_root":true}' + + String val = JWTs.builder().setPayload(payload).compact(); + + def specOutput = 'eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.' + + assertEquals val, specOutput + } + + @Test + void testParsePlaintextToken() { + + def claims = [iss: 'joe', exp: 1300819380, 'http://example.com/is_root':true] + + String jwt = JWTs.builder().setClaims(claims).compact(); + + def token = JWTs.parser().parse(jwt); + + assertEquals token.body, claims + } + + @Test(expectedExceptions = IllegalArgumentException) + void testParseNull() { + JWTs.parser().parse(null) + } + + @Test(expectedExceptions = IllegalArgumentException) + void testParseEmptyString() { + JWTs.parser().parse('') + } + + @Test(expectedExceptions = IllegalArgumentException) + void testParseWhitespaceString() { + JWTs.parser().parse(' ') + } + + @Test + void testParseWithNoPeriods() { + try { + JWTs.parser().parse('foo') + fail() + } catch (MalformedJwtException e) { + assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 0" + } + } + + @Test + void testParseWithOnePeriodOnly() { + try { + JWTs.parser().parse('.') + fail() + } catch (MalformedJwtException e) { + assertEquals e.message, "JWT strings must contain exactly 2 period characters. Found: 1" + } + } + + @Test + void testParseWithTwoPeriodsOnly() { + try { + JWTs.parser().parse('..') + fail() + } catch (MalformedJwtException e) { + assertEquals e.message, "JWT string '..' is missing a body/payload." + } + } + + @Test + void testParseWithHeaderOnly() { + try { + JWTs.parser().parse('foo..') + fail() + } catch (MalformedJwtException e) { + assertEquals e.message, "JWT string 'foo..' is missing a body/payload." + } + } + + @Test + void testParseWithSignatureOnly() { + try { + JWTs.parser().parse('..bar') + fail() + } catch (MalformedJwtException e) { + assertEquals e.message, "JWT string '..bar' is missing a body/payload." + } + } + + @Test + void testParseWithHeaderAndSignatureOnly() { + try { + JWTs.parser().parse('foo..bar') + fail() + } catch (MalformedJwtException e) { + assertEquals e.message, "JWT string 'foo..bar' is missing a body/payload." + } + } + + @Test + void testHS256() { + testHmac(SignatureAlgorithm.HS256); + } + + @Test + void testHS384() { + testHmac(SignatureAlgorithm.HS384); + } + + @Test + void testHS512() { + testHmac(SignatureAlgorithm.HS512); + } + + @Test + void testRS256() { + testRsa(SignatureAlgorithm.RS256); + } + + @Test + void testRS384() { + testRsa(SignatureAlgorithm.RS384); + } + + @Test + void testRS512() { + testRsa(SignatureAlgorithm.RS512); + } + + @Test + void testPS256() { + testRsa(SignatureAlgorithm.PS256); + } + + @Test + void testPS384() { + testRsa(SignatureAlgorithm.PS384); + } + + @Test + void testPS512() { + testRsa(SignatureAlgorithm.PS512, 2048, false); + } + + @Test + void testRSA256WithPrivateKeyValidation() { + testRsa(SignatureAlgorithm.RS256, 1024, true); + } + + @Test + void testRSA384WithPrivateKeyValidation() { + testRsa(SignatureAlgorithm.RS384, 1024, true); + } + + @Test + void testRSA512WithPrivateKeyValidation() { + testRsa(SignatureAlgorithm.RS512, 1024, true); + } + + static void testRsa(SignatureAlgorithm alg) { + testRsa(alg, 1024, false); + } + + static void testRsa(SignatureAlgorithm alg, int keySize, boolean verifyWithPrivateKey) { + + KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); + keyGenerator.initialize(keySize); + + KeyPair kp = keyGenerator.genKeyPair(); + PublicKey publicKey = kp.getPublic(); + PrivateKey privateKey = kp.getPrivate(); + + def claims = [iss: 'joe', exp: 1300819380, 'http://example.com/is_root':true] + + String jwt = JWTs.builder().setClaims(claims).signWith(alg, privateKey).compact(); + + def key = publicKey; + if (verifyWithPrivateKey) { + key = privateKey; + } + + def token = JWTs.parser().setSigningKey(key).parse(jwt); + + assertEquals token.header, [alg: alg.name()] + + assertEquals token.body, claims + + } + + static void testHmac(SignatureAlgorithm alg) { + //create random signing key for testing: + Random random = new SecureRandom(); + byte[] key = new byte[64]; + random.nextBytes(key); + + def claims = [iss: 'joe', exp: 1300819380, 'http://example.com/is_root':true] + + String jwt = JWTs.builder().setClaims(claims).signWith(alg, key).compact(); + + def token = JWTs.parser().setSigningKey(key).parse(jwt) + + assertEquals token.header, [alg: alg.name()] + + assertEquals token.body, claims + } +} + diff --git a/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy b/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy new file mode 100644 index 00000000..acea7635 --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy @@ -0,0 +1,44 @@ +/* + * 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 + +import org.testng.annotations.Test +import static org.testng.Assert.* + +class SignatureAlgorithmTest { + + @Test + void testNames() { + def algNames = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', + 'ES256', 'ES384', 'ES512', 'PS256', 'PS384', 'PS512', 'NONE'] + + for( String name : algNames ) { + testName(name) + } + } + + private static void testName(String name) { + def alg = SignatureAlgorithm.forName(name); + def namedAlg = name as SignatureAlgorithm //Groovy type coercion FTW! + assertTrue alg == namedAlg + assert alg.description != null && alg.description != "" + } + + @Test(expectedExceptions = SignatureException) + void testUnrecognizedAlgorithmName() { + SignatureAlgorithm.forName('whatever') + } +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy new file mode 100644 index 00000000..1c8e3d8f --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy @@ -0,0 +1,46 @@ +/* + * 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.crypto + +import io.jsonwebtoken.SignatureAlgorithm +import org.testng.annotations.Test + +import javax.crypto.spec.SecretKeySpec + +import static org.testng.Assert.* + +class DefaultSignerFactoryTest { + + private static final Random rng = new Random(); //doesn't need to be secure - we're just testing + + @Test + void testCreateSignerWithNoneAlgorithm() { + + byte[] keyBytes = new byte[32]; + rng.nextBytes(keyBytes); + SecretKeySpec key = new SecretKeySpec(keyBytes, "foo"); + + def factory = new DefaultSignerFactory(); + + try { + factory.createSigner(SignatureAlgorithm.NONE, key); + fail(); + } catch (IllegalArgumentException iae) { + assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing." + } + } + +} diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy new file mode 100644 index 00000000..c0a5745e --- /dev/null +++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy @@ -0,0 +1,74 @@ +/* + * 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.crypto + +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.SignatureException +import org.testng.annotations.Test +import static org.testng.Assert.* + +import javax.crypto.Mac +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException + +class MacSignerTest { + + private static final Random rng = new Random(); //doesn't need to be secure - we're just testing + + @Test + void testNoSuchAlgorithmException() { + byte[] key = new byte[32]; + byte[] data = new byte[32]; + rng.nextBytes(key); + rng.nextBytes(data); + + def s = new MacSigner(SignatureAlgorithm.HS256, key) { + @Override + protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { + throw new NoSuchAlgorithmException("foo"); + } + } + try { + s.sign(data); + fail(); + } catch (SignatureException e) { + assertTrue e.cause instanceof NoSuchAlgorithmException + assertEquals e.cause.message, 'foo' + } + } + + @Test + void testInvalidKeyException() { + byte[] key = new byte[32]; + byte[] data = new byte[32]; + rng.nextBytes(key); + rng.nextBytes(data); + + def s = new MacSigner(SignatureAlgorithm.HS256, key) { + @Override + protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException { + throw new InvalidKeyException("foo"); + } + } + try { + s.sign(data); + fail(); + } catch (SignatureException e) { + assertTrue e.cause instanceof InvalidKeyException + assertEquals e.cause.message, 'foo' + } + } +}