http://git-wip-us.apache.org/repos/asf/knox/blob/8affbc02/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java ---------------------------------------------------------------------- diff --cc gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java index 224eb1c,0000000..b73b1b7 mode 100644,000000..100644 --- a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java +++ b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java @@@ -1,307 -1,0 +1,510 @@@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.knox.gateway.service.knoxtoken; + +import org.apache.knox.gateway.service.knoxtoken.TokenResource; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.security.token.JWTokenAuthority; +import org.apache.knox.gateway.services.security.token.TokenServiceException; +import org.apache.knox.gateway.services.security.token.impl.JWT; +import org.apache.knox.gateway.services.security.token.impl.JWTToken; ++import org.apache.knox.gateway.security.PrimaryPrincipal; ++ +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; + +import java.util.Map; + +import javax.security.auth.Subject; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; + +import static org.junit.Assert.*; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; ++import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +/** + * Some tests for the token service + */ +public class TokenServiceResourceTest { + + protected static RSAPublicKey publicKey; + protected static RSAPrivateKey privateKey; + + @BeforeClass + public static void setup() throws Exception, NoSuchAlgorithmException { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(1024); + KeyPair KPair = kpg.generateKeyPair(); + + publicKey = (RSAPublicKey) KPair.getPublic(); + privateKey = (RSAPrivateKey) KPair.getPrivate(); + } + + @Test + public void testTokenService() throws Exception { + Assert.assertTrue(true); + } + + @Test + public void testClientData() throws Exception { + TokenResource tr = new TokenResource(); + + Map<String,Object> clientDataMap = new HashMap<>(); + tr.addClientDataToMap("cookie.name=hadoop-jwt,test=value".split(","), clientDataMap); + Assert.assertTrue(clientDataMap.size() == 2); + + clientDataMap = new HashMap<>(); + tr.addClientDataToMap("cookie.name=hadoop-jwt".split(","), clientDataMap); + Assert.assertTrue(clientDataMap.size() == 1); + + clientDataMap = new HashMap<>(); + tr.addClientDataToMap("".split(","), clientDataMap); + Assert.assertTrue(clientDataMap.size() == 0); + } + + @Test + public void testGetToken() throws Exception { + TokenResource tr = new TokenResource(); + + ServletContext context = EasyMock.createNiceMock(ServletContext.class); + //tr.context = context; + // tr.init(); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes(); + Principal principal = EasyMock.createNiceMock(Principal.class); + EasyMock.expect(principal.getName()).andReturn("alice").anyTimes(); + EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes(); + + GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); + EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services); + + JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey); + EasyMock.expect(services.getService(GatewayServices.TOKEN_SERVICE)).andReturn(authority); + + StringWriter writer = new StringWriter(); + PrintWriter printWriter = new PrintWriter(writer); + HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); + EasyMock.expect(response.getWriter()).andReturn(printWriter); + + EasyMock.replay(principal, services, context, request, response); + + tr.request = request; + tr.response = response; + + // Issue a token + Response retResponse = tr.doGet(); + + assertEquals(200, retResponse.getStatus()); + + // Parse the response + String retString = writer.toString(); + String accessToken = getTagValue(retString, "access_token"); + assertNotNull(accessToken); + String expiry = getTagValue(retString, "expires_in"); + assertNotNull(expiry); + + // Verify the token + JWTToken parsedToken = new JWTToken(accessToken); + assertEquals("alice", parsedToken.getSubject()); + assertTrue(authority.verifyToken(parsedToken)); + } + + @Test + public void testAudiences() throws Exception { + + ServletContext context = EasyMock.createNiceMock(ServletContext.class); + EasyMock.expect(context.getInitParameter("knox.token.audiences")).andReturn("recipient1,recipient2"); + EasyMock.expect(context.getInitParameter("knox.token.ttl")).andReturn(null); + EasyMock.expect(context.getInitParameter("knox.token.target.url")).andReturn(null); + EasyMock.expect(context.getInitParameter("knox.token.client.data")).andReturn(null); + + HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); + EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes(); + Principal principal = EasyMock.createNiceMock(Principal.class); + EasyMock.expect(principal.getName()).andReturn("alice").anyTimes(); + EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes(); + + GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); + EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services); + + JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey); + EasyMock.expect(services.getService(GatewayServices.TOKEN_SERVICE)).andReturn(authority); + + StringWriter writer = new StringWriter(); + PrintWriter printWriter = new PrintWriter(writer); + HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); + EasyMock.expect(response.getWriter()).andReturn(printWriter); + + EasyMock.replay(principal, services, context, request, response); + + TokenResource tr = new TokenResource(); + tr.request = request; + tr.response = response; + tr.context = context; + tr.init(); + + // Issue a token + Response retResponse = tr.doGet(); + + assertEquals(200, retResponse.getStatus()); + + // Parse the response + String retString = writer.toString(); + String accessToken = getTagValue(retString, "access_token"); + assertNotNull(accessToken); + String expiry = getTagValue(retString, "expires_in"); + assertNotNull(expiry); + + // Verify the token + JWTToken parsedToken = new JWTToken(accessToken); + assertEquals("alice", parsedToken.getSubject()); + assertTrue(authority.verifyToken(parsedToken)); + + // Verify the audiences + List<String> audiences = Arrays.asList(parsedToken.getAudienceClaims()); + assertEquals(2, audiences.size()); + assertTrue(audiences.contains("recipient1")); + assertTrue(audiences.contains("recipient2")); + } + ++ @Test ++ public void testAudiencesWhitespace() throws Exception { ++ ++ ServletContext context = EasyMock.createNiceMock(ServletContext.class); ++ EasyMock.expect(context.getInitParameter("knox.token.audiences")).andReturn(" recipient1, recipient2 "); ++ EasyMock.expect(context.getInitParameter("knox.token.ttl")).andReturn(null); ++ EasyMock.expect(context.getInitParameter("knox.token.target.url")).andReturn(null); ++ EasyMock.expect(context.getInitParameter("knox.token.client.data")).andReturn(null); ++ ++ HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); ++ EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes(); ++ Principal principal = EasyMock.createNiceMock(Principal.class); ++ EasyMock.expect(principal.getName()).andReturn("alice").anyTimes(); ++ EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes(); ++ ++ GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); ++ EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services); ++ ++ JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey); ++ EasyMock.expect(services.getService(GatewayServices.TOKEN_SERVICE)).andReturn(authority); ++ ++ StringWriter writer = new StringWriter(); ++ PrintWriter printWriter = new PrintWriter(writer); ++ HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); ++ EasyMock.expect(response.getWriter()).andReturn(printWriter); ++ ++ EasyMock.replay(principal, services, context, request, response); ++ ++ TokenResource tr = new TokenResource(); ++ tr.request = request; ++ tr.response = response; ++ tr.context = context; ++ tr.init(); ++ ++ // Issue a token ++ Response retResponse = tr.doGet(); ++ ++ assertEquals(200, retResponse.getStatus()); ++ ++ // Parse the response ++ String retString = writer.toString(); ++ String accessToken = getTagValue(retString, "access_token"); ++ assertNotNull(accessToken); ++ String expiry = getTagValue(retString, "expires_in"); ++ assertNotNull(expiry); ++ ++ // Verify the token ++ JWTToken parsedToken = new JWTToken(accessToken); ++ assertEquals("alice", parsedToken.getSubject()); ++ assertTrue(authority.verifyToken(parsedToken)); ++ ++ // Verify the audiences ++ List<String> audiences = Arrays.asList(parsedToken.getAudienceClaims()); ++ assertEquals(2, audiences.size()); ++ assertTrue(audiences.contains("recipient1")); ++ assertTrue(audiences.contains("recipient2")); ++ } ++ ++ @Test ++ public void testValidClientCert() throws Exception { ++ ++ ServletContext context = EasyMock.createNiceMock(ServletContext.class); ++ EasyMock.expect(context.getInitParameter("knox.token.client.cert.required")).andReturn("true"); ++ EasyMock.expect(context.getInitParameter("knox.token.allowed.principals")).andReturn("CN=localhost, OU=Test, O=Hadoop, L=Test, ST=Test, C=US"); ++ ++ HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); ++ EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes(); ++ X509Certificate trustedCertMock = EasyMock.createMock(X509Certificate.class); ++ EasyMock.expect(trustedCertMock.getSubjectDN()).andReturn(new PrimaryPrincipal("CN=localhost, OU=Test, O=Hadoop, L=Test, ST=Test, C=US")).anyTimes(); ++ ArrayList<X509Certificate> certArrayList = new ArrayList<X509Certificate>(); ++ certArrayList.add(trustedCertMock); ++ X509Certificate[] certs = {}; ++ EasyMock.expect(request.getAttribute("javax.servlet.request.X509Certificate")).andReturn(certArrayList.toArray(certs)).anyTimes(); ++ ++ Principal principal = EasyMock.createNiceMock(Principal.class); ++ EasyMock.expect(principal.getName()).andReturn("alice").anyTimes(); ++ EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes(); ++ ++ GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); ++ EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services); ++ ++ JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey); ++ EasyMock.expect(services.getService(GatewayServices.TOKEN_SERVICE)).andReturn(authority); ++ ++ StringWriter writer = new StringWriter(); ++ PrintWriter printWriter = new PrintWriter(writer); ++ HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); ++ EasyMock.expect(response.getWriter()).andReturn(printWriter); ++ ++ EasyMock.replay(principal, services, context, request, response, trustedCertMock); ++ ++ TokenResource tr = new TokenResource(); ++ tr.request = request; ++ tr.response = response; ++ tr.context = context; ++ tr.init(); ++ ++ // Issue a token ++ Response retResponse = tr.doGet(); ++ ++ assertEquals(200, retResponse.getStatus()); ++ ++ // Parse the response ++ String retString = writer.toString(); ++ String accessToken = getTagValue(retString, "access_token"); ++ assertNotNull(accessToken); ++ String expiry = getTagValue(retString, "expires_in"); ++ assertNotNull(expiry); ++ ++ // Verify the token ++ JWTToken parsedToken = new JWTToken(accessToken); ++ assertEquals("alice", parsedToken.getSubject()); ++ assertTrue(authority.verifyToken(parsedToken)); ++ } ++ ++ @Test ++ public void testValidClientCertWrongUser() throws Exception { ++ ++ ServletContext context = EasyMock.createNiceMock(ServletContext.class); ++ EasyMock.expect(context.getInitParameter("knox.token.client.cert.required")).andReturn("true"); ++ EasyMock.expect(context.getInitParameter("knox.token.allowed.principals")).andReturn("CN=remotehost, OU=Test, O=Hadoop, L=Test, ST=Test, C=US"); ++ ++ HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); ++ EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes(); ++ X509Certificate trustedCertMock = EasyMock.createMock(X509Certificate.class); ++ EasyMock.expect(trustedCertMock.getSubjectDN()).andReturn(new PrimaryPrincipal("CN=localhost, OU=Test, O=Hadoop, L=Test, ST=Test, C=US")).anyTimes(); ++ ArrayList<X509Certificate> certArrayList = new ArrayList<X509Certificate>(); ++ certArrayList.add(trustedCertMock); ++ X509Certificate[] certs = {}; ++ EasyMock.expect(request.getAttribute("javax.servlet.request.X509Certificate")).andReturn(certArrayList.toArray(certs)).anyTimes(); ++ ++ Principal principal = EasyMock.createNiceMock(Principal.class); ++ EasyMock.expect(principal.getName()).andReturn("alice").anyTimes(); ++ EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes(); ++ ++ GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); ++ EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services); ++ ++ JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey); ++ EasyMock.expect(services.getService(GatewayServices.TOKEN_SERVICE)).andReturn(authority); ++ ++ StringWriter writer = new StringWriter(); ++ PrintWriter printWriter = new PrintWriter(writer); ++ HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); ++ EasyMock.expect(response.getWriter()).andReturn(printWriter); ++ ++ EasyMock.replay(principal, services, context, request, response, trustedCertMock); ++ ++ TokenResource tr = new TokenResource(); ++ tr.request = request; ++ tr.response = response; ++ tr.context = context; ++ tr.init(); ++ ++ // Issue a token ++ Response retResponse = tr.doGet(); ++ ++ assertEquals(403, retResponse.getStatus()); ++ } ++ ++ @Test ++ public void testMissingClientCert() throws Exception { ++ ++ ServletContext context = EasyMock.createNiceMock(ServletContext.class); ++ EasyMock.expect(context.getInitParameter("knox.token.client.cert.required")).andReturn("true"); ++ EasyMock.expect(context.getInitParameter("knox.token.allowed.principals")).andReturn("CN=remotehost, OU=Test, O=Hadoop, L=Test, ST=Test, C=US"); ++ ++ HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); ++ EasyMock.expect(request.getServletContext()).andReturn(context).anyTimes(); ++ EasyMock.expect(request.getAttribute("javax.servlet.request.X509Certificate")).andReturn(null).anyTimes(); ++ ++ Principal principal = EasyMock.createNiceMock(Principal.class); ++ EasyMock.expect(principal.getName()).andReturn("alice").anyTimes(); ++ EasyMock.expect(request.getUserPrincipal()).andReturn(principal).anyTimes(); ++ ++ GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); ++ EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services); ++ ++ JWTokenAuthority authority = new TestJWTokenAuthority(publicKey, privateKey); ++ EasyMock.expect(services.getService(GatewayServices.TOKEN_SERVICE)).andReturn(authority); ++ ++ StringWriter writer = new StringWriter(); ++ PrintWriter printWriter = new PrintWriter(writer); ++ HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class); ++ EasyMock.expect(response.getWriter()).andReturn(printWriter); ++ ++ EasyMock.replay(principal, services, context, request, response); ++ ++ TokenResource tr = new TokenResource(); ++ tr.request = request; ++ tr.response = response; ++ tr.context = context; ++ tr.init(); ++ ++ // Issue a token ++ Response retResponse = tr.doGet(); ++ ++ assertEquals(403, retResponse.getStatus()); ++ } ++ + private String getTagValue(String token, String tagName) { + String searchString = tagName + "\":"; + String value = token.substring(token.indexOf(searchString) + searchString.length()); + if (value.startsWith("\"")) { + value = value.substring(1); + } + if (value.contains("\"")) { + return value.substring(0, value.indexOf("\"")); + } else if (value.contains(",")) { + return value.substring(0, value.indexOf(",")); + } else { + return value.substring(0, value.length() - 1); + } + } + + private static class TestJWTokenAuthority implements JWTokenAuthority { + + private RSAPublicKey publicKey; + private RSAPrivateKey privateKey; + + public TestJWTokenAuthority(RSAPublicKey publicKey, RSAPrivateKey privateKey) { + this.publicKey = publicKey; + this.privateKey = privateKey; + } + + @Override + public JWT issueToken(Subject subject, String algorithm) + throws TokenServiceException { + Principal p = (Principal) subject.getPrincipals().toArray()[0]; + return issueToken(p, algorithm); + } + + @Override + public JWT issueToken(Principal p, String algorithm) + throws TokenServiceException { + return issueToken(p, null, algorithm); + } + + @Override + public JWT issueToken(Principal p, String audience, String algorithm) + throws TokenServiceException { + return issueToken(p, audience, algorithm, -1); + } + + @Override + public boolean verifyToken(JWT token) throws TokenServiceException { + JWSVerifier verifier = new RSASSAVerifier(publicKey); + return token.verify(verifier); + } + + @Override + public JWT issueToken(Principal p, String audience, String algorithm, + long expires) throws TokenServiceException { + ArrayList<String> audiences = null; + if (audience != null) { + audiences = new ArrayList<String>(); + audiences.add(audience); + } + return issueToken(p, audiences, algorithm, expires); + } + + @Override + public JWT issueToken(Principal p, List<String> audiences, String algorithm, + long expires) throws TokenServiceException { + String[] claimArray = new String[4]; + claimArray[0] = "KNOXSSO"; + claimArray[1] = p.getName(); + claimArray[2] = null; + if (expires == -1) { + claimArray[3] = null; + } else { + claimArray[3] = String.valueOf(expires); + } + + JWTToken token = null; + if ("RS256".equals(algorithm)) { + token = new JWTToken("RS256", claimArray, audiences); + JWSSigner signer = new RSASSASigner(privateKey); + token.sign(signer); + } else { + throw new TokenServiceException("Cannot issue token - Unsupported algorithm"); + } + + return token; + } + + @Override + public JWT issueToken(Principal p, String algorithm, long expiry) + throws TokenServiceException { + return issueToken(p, Collections.<String>emptyList(), algorithm, expiry); + } + + @Override + public boolean verifyToken(JWT token, RSAPublicKey publicKey) throws TokenServiceException { + JWSVerifier verifier = new RSASSAVerifier(publicKey); + return token.verify(verifier); + } + + } + + +}
http://git-wip-us.apache.org/repos/asf/knox/blob/8affbc02/gateway-shell-release/pom.xml ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/knox/blob/8affbc02/gateway-util-urltemplate/src/main/java/org/apache/knox/gateway/util/urltemplate/Parser.java ---------------------------------------------------------------------- diff --cc gateway-util-urltemplate/src/main/java/org/apache/knox/gateway/util/urltemplate/Parser.java index 47ed00c,0000000..1d58978 mode 100644,000000..100644 --- a/gateway-util-urltemplate/src/main/java/org/apache/knox/gateway/util/urltemplate/Parser.java +++ b/gateway-util-urltemplate/src/main/java/org/apache/knox/gateway/util/urltemplate/Parser.java @@@ -1,345 -1,0 +1,349 @@@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.knox.gateway.util.urltemplate; + +import org.apache.knox.gateway.i18n.resources.ResourcesFactory; + +import java.net.URISyntaxException; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +//NOTE: Instances Not thread safe but reusable. Static parse method is thread safe. +//NOTE: Ignores matrix parameters at this point. +public class Parser { + + /* + ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? + 12 3 4 5 6 7 8 9 + + The numbers in the second line above are only to assist readability; + they indicate the reference points for each subexpression (i.e., each + paired parenthesis). We refer to the value matched for subexpression + <n> as $<n>. For example, matching the above expression to + + http://www.ics.uci.edu/pub/ietf/uri/#Related + + results in the following subexpression matches: + + $1 = http: + $2 = http + $3 = //www.ics.uci.edu + $4 = www.ics.uci.edu + $5 = /pub/ietf/uri/ + $6 = <undefined> + $7 = <undefined> + $8 = #Related + $9 = Related + + where <undefined> indicates that the component is not present, as is + the case for the query component in the above example. Therefore, we + can determine the value of the five components as + + scheme = $2 + authority = $4 + path = $5 + query = $7 + fragment = $9 + */ + + private static final Resources RES = ResourcesFactory.get( Resources.class ); + + public static final char TEMPLATE_OPEN_MARKUP = '{'; + public static final char TEMPLATE_CLOSE_MARKUP = '}'; + public static final char NAME_PATTERN_SEPARATOR = '='; + + private static final int MATCH_GROUP_SCHEME = 1; + private static final int MATCH_GROUP_SCHEME_NAKED = 2; + private static final int MATCH_GROUP_AUTHORITY = 3; + private static final int MATCH_GROUP_AUTHORITY_NAKED = 4; + private static final int MATCH_GROUP_PATH = 5; + private static final int MATCH_GROUP_QUERY = 6; + private static final int MATCH_GROUP_QUERY_NAKED = 7; + private static final int MATCH_GROUP_FRAGMENT = 8; + private static final int MATCH_GROUP_FRAGMENT_NAKED = 9; + + private static Pattern PATTERN = Pattern.compile( "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?" ); + + @Deprecated + public static final Template parse( String template ) throws URISyntaxException { + return Parser.parseTemplate( template ); + } + + public static final Template parseTemplate( final String template ) throws URISyntaxException { + Builder builder = new Builder( template ); + return parseInternal( builder ); + } + + public static final Template parseLiteral( final String literal ) throws URISyntaxException { + Builder builder = new Builder( literal ); + builder.setLiteral( true ); + return parseInternal( builder ); + } + + private static final Template parseInternal( final Builder builder ) throws URISyntaxException { + String original = builder.getOriginal(); + builder.setHasScheme( false ); + builder.setHasAuthority( false ); // Assume no until found otherwise. If true, will cause // in output URL. + builder.setIsAuthorityOnly( false ); + builder.setIsAbsolute( false ); // Assume relative until found otherwise. If true, will cause leading / in output URL. + builder.setIsDirectory( false ); // Assume a file path until found otherwise. If true, will cause trailing / in output URL. + builder.setHasQuery( false ); // Assume no ? until found otherwise. If true, will cause ? in output URL. + builder.setHasFragment( false ); // Assume no # until found otherwise. If true, will cause # in output URL. + Matcher match = PATTERN.matcher( original ); + if( match.matches() ) { + consumeSchemeMatch( builder, match ); + consumeAuthorityMatch( builder, match ); + consumePathMatch( builder, match ); + consumeQueryMatch( builder, match ); + consumeFragmentMatch( builder, match ); + fixNakedAuthority( builder ); + } else { + throw new URISyntaxException( original, RES.parseTemplateFailureReason( original ) ); + } + return builder.build(); + } + + private static final void fixNakedAuthority( final Builder builder ) { + if( builder.getHasScheme() && + !builder.getHasAuthority() && + !builder.getIsAbsolute() && + !builder.getIsDirectory() && + ( builder.getPath().size() == 1 ) && + !builder.getHasQuery() && + !builder.getHasFragment() ) { + final Scheme scheme = builder.getScheme(); + builder.setHasScheme( false ); + builder.setHost( makeTokenSingular( scheme.getToken() ) ); + Path path = builder.getPath().remove( 0 ); + builder.setPort( makeTokenSingular( path.getToken() ) ); + builder.setIsAuthorityOnly( true ); + } + } + + private static final Token makeTokenSingular( Token token ) { + final String effectivePattern = token.getEffectivePattern(); + if( Segment.GLOB_PATTERN.equals( effectivePattern ) ) { + token = new Token( token.getParameterName(), token.getOriginalPattern(), Segment.STAR_PATTERN, token.isLiteral() ); + } + return token; + } + +// private String makePatternSingular( String pattern ) { +// if( Segment.GLOB_PATTERN.equals( pattern ) ) { +// pattern = Segment.STAR_PATTERN; +// } +// return pattern; +// } + + private static void consumeSchemeMatch( final Builder builder, final Matcher match ) { + if( match.group( MATCH_GROUP_SCHEME ) != null ) { + builder.setHasScheme( true ); + consumeSchemeToken( builder, match.group( MATCH_GROUP_SCHEME_NAKED ) ); + } + } + + private static void consumeSchemeToken( final Builder builder, final String token ) { + if( token != null ) { + Token t = parseTemplateToken( builder, token, Segment.STAR_PATTERN ); + builder.setScheme( t ); + } + } + + private static void consumeAuthorityMatch( final Builder builder, final Matcher match ) { + if( match.group( MATCH_GROUP_AUTHORITY ) != null ) { + builder.setHasAuthority( true ); + consumeAuthorityToken( builder, match.group( MATCH_GROUP_AUTHORITY_NAKED ) ); + } + } + + private static void consumeAuthorityToken( final Builder builder, final String token ) { + if( token != null ) { + Token paramPattern; + String[] usernamePassword=null, hostPort; + String[] userAddr = split( token, '@' ); + if( userAddr.length == 1 ) { + hostPort = split( userAddr[ 0 ], ':' ); + } else { + usernamePassword = split( userAddr[ 0 ], ':' ); + hostPort = split( userAddr[ 1 ], ':' ); + } + if( usernamePassword != null ) { + if( usernamePassword[ 0 ].length() > 0 ) { + paramPattern = makeTokenSingular( parseTemplateToken( builder, usernamePassword[ 0 ], Segment.STAR_PATTERN ) ); + builder.setUsername( paramPattern ); + } + if( usernamePassword.length > 1 && usernamePassword[ 1 ].length() > 0 ) { + paramPattern = makeTokenSingular( parseTemplateToken( builder, usernamePassword[ 1 ], Segment.STAR_PATTERN ) ); + builder.setPassword( paramPattern ); + } + } + if( hostPort[ 0 ].length() > 0 ) { + paramPattern = makeTokenSingular( parseTemplateToken( builder, hostPort[ 0 ], Segment.STAR_PATTERN ) ); + builder.setHost( paramPattern ); + } + if( hostPort.length > 1 && hostPort[ 1 ].length() > 0 ) { + paramPattern = makeTokenSingular( parseTemplateToken( builder, hostPort[ 1 ], Segment.STAR_PATTERN ) ); + builder.setPort( paramPattern ); + } + } + } + + private static void consumePathMatch( final Builder builder, final Matcher match ) { + String path = match.group( MATCH_GROUP_PATH ); + if( path != null ) { + builder.setIsAbsolute( path.startsWith( "/" ) ); + builder.setIsDirectory( path.endsWith( "/" ) ); + consumePathToken( builder, path ); + } + } + + private static final void consumePathToken( final Builder builder, final String token ) { + if( token != null ) { + final StringTokenizer tokenizer = new StringTokenizer( token, "/" ); + while( tokenizer.hasMoreTokens() ) { + consumePathSegment( builder, tokenizer.nextToken() ); + } + } + } + + private static final void consumePathSegment( final Builder builder, final String token ) { + if( token != null ) { + final Token t = parseTemplateToken( builder, token, Segment.GLOB_PATTERN ); + builder.addPath( t ); + } + } + + private static void consumeQueryMatch( final Builder builder, Matcher match ) { + if( match.group( MATCH_GROUP_QUERY ) != null ) { + builder.setHasQuery( true ); + consumeQueryToken( builder, match.group( MATCH_GROUP_QUERY_NAKED ) ); + } + } + + private static void consumeQueryToken( final Builder builder, String token ) { + if( token != null ) { - StringTokenizer tokenizer = new StringTokenizer( token, "?&" ); - while( tokenizer.hasMoreTokens() ) { - consumeQuerySegment( builder, tokenizer.nextToken() ); ++ //add "&" as a delimiter ++ String[] tokens = token.split("(&|\\?|&)"); ++ if (tokens != null){ ++ for (String nextToken : tokens){ ++ consumeQuerySegment(builder,nextToken); ++ } + } ++ + } + } + + private static void consumeQuerySegment( final Builder builder, String token ) { + if( token != null && token.length() > 0 ) { + // Shorthand format {queryParam} == queryParam={queryParam=*} + if( TEMPLATE_OPEN_MARKUP == token.charAt( 0 ) ) { + Token paramPattern = parseTemplateToken( builder, token, Segment.GLOB_PATTERN ); + String paramName = paramPattern.parameterName; + if( paramPattern.originalPattern == null ) { + builder.addQuery( paramName, new Token( paramName, null, Segment.GLOB_PATTERN, builder.isLiteral() ) ); +// if( Segment.STAR_PATTERN.equals( paramName ) || Segment.GLOB_PATTERN.equals( paramName ) ) { +// builder.addQuery( paramName, new Token( paramName, null, Segment.GLOB_PATTERN ) ); +// } else { +// builder.addQuery( paramName, new Token( paramName, null, Segment.GLOB_PATTERN ) ); +// } + } else { + builder.addQuery( paramName, new Token( paramName, paramPattern.originalPattern, builder.isLiteral() ) ); + } + } else { + String nameValue[] = split( token, '=' ); + if( nameValue.length == 1 ) { + String queryName = nameValue[ 0 ]; + builder.addQuery( queryName, new Token( Segment.ANONYMOUS_PARAM, null, builder.isLiteral() ) ); + } else { + String queryName = nameValue[ 0 ]; + Token paramPattern = parseTemplateToken( builder, nameValue[ 1 ], Segment.GLOB_PATTERN ); + builder.addQuery( queryName, paramPattern ); + } + } + } + } + + private static void consumeFragmentMatch( final Builder builder, Matcher match ) { + if( match.group( MATCH_GROUP_FRAGMENT ) != null ) { + builder.setHasFragment( true ); + consumeFragmentToken( builder, match.group( MATCH_GROUP_FRAGMENT_NAKED ) ); + } + } + + private static void consumeFragmentToken( final Builder builder, String token ) { + if( token != null && token.length() > 0 ) { + Token t = parseTemplateToken( builder, token, Segment.STAR_PATTERN ); + builder.setFragment( t ); + } + } + + static final Token parseTemplateToken( final Builder builder, final String s, final String defaultEffectivePattern ) { + String paramName, actualPattern, effectivePattern; + final int l = s.length(); + // If the token isn't the empty string, then + if( l > 0 && !builder.isLiteral() ) { + final int b = ( s.charAt( 0 ) == TEMPLATE_OPEN_MARKUP ? 1 : -1 ); + final int e = ( s.charAt( l-1 ) == TEMPLATE_CLOSE_MARKUP ? l-1 : -1 ); + // If this is a parameter template, ie {...} + if( ( b > 0 ) && ( e > 0 ) && ( e > b ) ) { + final int i = s.indexOf( NAME_PATTERN_SEPARATOR, b ); + // If this is an anonymous template + if( i < 0 ) { + paramName = s.substring( b, e ); + actualPattern = null; + if( Segment.GLOB_PATTERN.equals( paramName ) ) { + effectivePattern = Segment.GLOB_PATTERN; + } else { + effectivePattern = defaultEffectivePattern; + } + // Otherwise populate the NVP. + } else { + paramName = s.substring( b, i ); + actualPattern = s.substring( i+1, e ); + effectivePattern = actualPattern; + } + // Otherwise it is just a pattern. + } else { + paramName = Segment.ANONYMOUS_PARAM; + actualPattern = s; + effectivePattern = actualPattern; + } + // Otherwise the token has no value. + } else { + paramName = Segment.ANONYMOUS_PARAM; + actualPattern = s; + effectivePattern = actualPattern; + } + final Token token = new Token( paramName, actualPattern, effectivePattern, builder.isLiteral() ); + return token; + } + + // Using this because String.split is very inefficient. + private static String[] split( String s, char d ) { + String[] a; + int i = s.indexOf( d ); + if( i < 0 ) { + a = new String[]{ s }; + } else { + a = new String[]{ s.substring( 0, i ), s.substring( i + 1 ) }; + } + return a; + } + +}