package org.rascalmpl.value.impl.primitive; import java.net.URI; import java.net.URISyntaxException; import java.util.regex.Pattern; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; /* * Not supported: in URI class, scheme is case insensitive, but this is already kinda broken, since on windows & osx, so should path's be. */ /*package*/ class SourceLocationURIValues { private static final Pattern schemePattern = Pattern.compile("[A-Za-z][A-Za-z0-9+\\-.]*"); private static final Pattern doubleSlashes = Pattern.compile("//+"); static IURI newURI(String scheme, String authority, String path, String query, String fragment) throws URISyntaxException { scheme = nullifyIfEmpty(scheme); authority = nullifyIfEmpty(authority); if (path != null) { if (path.isEmpty() || path.equals("/")) { path = null; } else if (!path.startsWith("/")) { path = "/" + path; } if (path != null && path.contains("//")) { // normalize double or longer slashes path = doubleSlashes.matcher(path).replaceAll("/"); } } query = nullifyIfEmpty(query); fragment = nullifyIfEmpty(fragment); if (scheme == null || scheme.equals("")) { throw new URISyntaxException(scheme, "scheme cannot be empty or null"); } if (INTERNED_SCHEMES.getIfPresent(scheme) == null && !schemePattern.matcher(scheme).matches()) { throw new URISyntaxException(scheme, "Scheme is not a valid scheme"); } if (authority == null) { if (path == null) { if (query == null) { if (fragment == null) { return new SourceLocationURIValues.BaseURI(scheme); } return new SourceLocationURIValues.FragmentURI(scheme, fragment); } if (fragment == null) { return new SourceLocationURIValues.QueryURI(scheme, query); } return new SourceLocationURIValues.FragmentQueryURI(scheme, query, fragment); } if (query == null) { if (fragment == null) { return new SourceLocationURIValues.PathURI(scheme, path); } return new SourceLocationURIValues.FragmentPathURI(scheme, path, fragment); } if (fragment == null) { return new SourceLocationURIValues.QueryPathURI(scheme, path, query); } return new SourceLocationURIValues.FragmentQueryPathURI(scheme, path, query, fragment); } if (path == null) { if (query == null) { if (fragment == null) { return new SourceLocationURIValues.AuthorityURI(scheme, authority); } return new SourceLocationURIValues.FragmentAuthorityURI(scheme, authority, fragment); } if (fragment == null) { return new SourceLocationURIValues.QueryAuthorityURI(scheme, authority, query); } return new SourceLocationURIValues.FragmentQueryAuthorityURI(scheme, authority, query, fragment); } if (query == null) { if (fragment == null) { return new SourceLocationURIValues.PathAuthorityURI(scheme, authority, path); } return new SourceLocationURIValues.FragmentPathAuthorityURI(scheme, authority, path, fragment); } if (fragment == null) { return new SourceLocationURIValues.QueryPathAuthorityURI(scheme, authority, path, query); } return new SourceLocationURIValues.FragmentQueryPathAuthorityURI(scheme, authority, path, query, fragment); } private static String nullifyIfEmpty(String str) { if (str == null || str.isEmpty()) { return null; } return str; } private static final LoadingCache<String, String> INTERNED_SCHEMES = Caffeine.newBuilder().build(s -> s); private static class BaseURI implements IURI { protected final String scheme; public BaseURI(String scheme) { this.scheme = INTERNED_SCHEMES.get(scheme); } public URI getURI() { try { return new URI(scheme,"","/",null,null); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ return scheme == ((BaseURI)obj).scheme; } return false; } @Override public int hashCode() { return scheme.hashCode(); } @Override public String getScheme() { return scheme; } @Override public String getAuthority() { return ""; } @Override public String getPath() { return "/"; } @Override public String getFragment() { return ""; } @Override public String getQuery() { return ""; } @Override public Boolean hasAuthority() { return false; } @Override public Boolean hasPath() { return true; } @Override public Boolean hasFragment() { return false; } @Override public Boolean hasQuery() { return false; } } private static final Pattern squareBrackets = Pattern.compile("(\\[|\\])"); private static URI buildURIWithAuthority(String scheme, String authority, String path, String query, String fragment) { try { return new URI(scheme, authority, path, query, fragment); } catch (URISyntaxException e) { if (authority != null && squareBrackets.matcher(authority).find()) { // Java URI do not correctly quote the brackets inside the authority // even though RFC2732 specifies this. // it has to do with the fact that the encoding/quotation is a single pass // and that authority is actually ambigious, so it requires backtracking to // decide which alternative of the authority part is used, and the quoting rules are // slightly different. // so if it fails to parse, we put some placeholder chars, which get encoded, // we then replace the encoded values with the correct encoded values // and create a new URI out of this. (to avoid double encoding) authority = hideBrackets(authority); URI temp = buildURIWithAuthority(scheme, authority, path, query, fragment); return unhideBrackets(temp); } throw new RuntimeException("Internal state corrupted?", e); } } private static final Pattern squareBracketOpenPlaceholder = Pattern.compile("%00%00%EF%BF%B0%00%00"); private static final Pattern squareBracketClosePlaceholder = Pattern.compile("%00%00%EF%BF%B1%00%00"); private static URI unhideBrackets(URI temp) { String newURI = temp.toASCIIString(); newURI = squareBracketOpenPlaceholder.matcher(newURI).replaceAll("%5B"); newURI = squareBracketClosePlaceholder.matcher(newURI).replaceAll("%5D"); try { return new URI(newURI); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } private static final Pattern squareBracketOpen = Pattern.compile("\\["); private static final Pattern squareBracketClose = Pattern.compile("\\]"); private static String hideBrackets(String authority) { authority = squareBracketOpen.matcher(authority).replaceAll("\0\0\uFFF0\0\0"); return squareBracketClose.matcher(authority).replaceAll("\0\0\uFFF1\0\0"); } private static final LoadingCache<String, String> INTERNED_AUTHORIES = Caffeine.newBuilder().build(s -> s); private static class AuthorityURI extends BaseURI { protected final String authority; public AuthorityURI(String scheme, String authority) { super(scheme); this.authority = INTERNED_AUTHORIES.get(authority); } @Override public URI getURI() { return buildURIWithAuthority(scheme, authority, null,null,null); } @Override public Boolean hasPath() { return false; } @Override public Boolean hasAuthority() { return true; } @Override public String getAuthority() { return authority; } @Override public int hashCode() { return scheme.hashCode() + authority.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ AuthorityURI u = (AuthorityURI)obj; return scheme == u.scheme && authority == u.authority ; } return false; } } private static class PathURI extends BaseURI { protected final String path; private int hash = 0; // we can cache the hash code since the 8-byte alignment leaves room for one public PathURI(String scheme, String path) { super(scheme); this.path = path; } @Override public URI getURI() { try { return new URI(scheme, "", path, null, null); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } @Override public Boolean hasPath() { return true; } @Override public String getPath() { return path; } @Override public int hashCode() { if (hash == 0) { hash = scheme.hashCode() + path.hashCode(); } return hash; } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ PathURI u = (PathURI)obj; if (hash != 0 && u.hash != 0 && hash != u.hash) return false; return scheme == u.scheme && path.equals(u.path); } return false; } } private static class PathAuthorityURI extends AuthorityURI { protected final String path; public PathAuthorityURI(String scheme, String authority, String path) { super(scheme, authority); this.path = path; } @Override public URI getURI() { return buildURIWithAuthority(scheme, authority, path,null,null); } @Override public Boolean hasPath() { return true; } @Override public String getPath() { return path; } @Override public int hashCode() { return scheme.hashCode() + authority.hashCode() + path.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ PathAuthorityURI u = (PathAuthorityURI)obj; return scheme == u.scheme && authority == u.authority && path.equals(u.path); } return false; } } private static class QueryURI extends BaseURI { protected final String query; public QueryURI(String scheme, String query) { super(scheme); this.query = query; } @Override public URI getURI() { try { return new URI(scheme, "", "/", query, null); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } @Override public Boolean hasQuery() { return true; } @Override public String getQuery() { return query; } @Override public int hashCode() { return scheme.hashCode() + query.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ QueryURI u = (QueryURI)obj; return scheme == u.scheme && query.equals(u.query) ; } return false; } } private static class QueryAuthorityURI extends AuthorityURI { protected final String query; public QueryAuthorityURI(String scheme, String authority, String query) { super(scheme, authority); this.query = query; } @Override public URI getURI() { return buildURIWithAuthority(scheme, authority, null, query, null); } @Override public Boolean hasQuery() { return true; } @Override public String getQuery() { return query; } @Override public int hashCode() { return scheme.hashCode() + authority.hashCode() + query.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ QueryAuthorityURI u = (QueryAuthorityURI)obj; return scheme == u.scheme && authority == u.authority && query.equals(u.query) ; } return false; } } private static class QueryPathURI extends PathURI { protected final String query; public QueryPathURI(String scheme, String path, String query) { super(scheme, path); this.query = query; } @Override public URI getURI() { try { return new URI(scheme, "", path, query, null); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } @Override public Boolean hasQuery() { return true; } @Override public String getQuery() { return query; } @Override public int hashCode() { return scheme.hashCode() + path.hashCode() + query.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ QueryPathURI u = (QueryPathURI)obj; return scheme == u.scheme && path.equals(u.path) && query.equals(u.query) ; } return false; } } private static class QueryPathAuthorityURI extends PathAuthorityURI { protected final String query; public QueryPathAuthorityURI(String scheme, String authority, String path, String query) { super(scheme, authority, path); this.query = query; } @Override public URI getURI() { return buildURIWithAuthority(scheme, authority, path,query,null); } @Override public Boolean hasQuery() { return true; } @Override public String getQuery() { return query; } @Override public int hashCode() { return scheme.hashCode() + authority.hashCode() + path.hashCode() + query.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ QueryPathAuthorityURI u = (QueryPathAuthorityURI)obj; return scheme == u.scheme && authority == u.authority && path.equals(u.path) && query.equals(u.query) ; } return false; } } private static class FragmentURI extends BaseURI { protected final String fragment; public FragmentURI(String scheme, String fragment) { super(scheme); this.fragment = fragment; } @Override public URI getURI() { try { return new URI(scheme, "", "/", null, fragment); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } @Override public Boolean hasFragment() { return true; } @Override public String getFragment() { return fragment; } @Override public int hashCode() { return scheme.hashCode() + fragment.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ FragmentURI u = (FragmentURI)obj; return scheme == u.scheme && fragment.equals(u.fragment) ; } return false; } } private static class FragmentAuthorityURI extends AuthorityURI { protected final String fragment; public FragmentAuthorityURI(String scheme, String authority, String fragment) { super(scheme, authority); this.fragment = fragment; } @Override public URI getURI() { return buildURIWithAuthority(scheme, authority, null, null, fragment); } @Override public Boolean hasFragment() { return true; } @Override public String getFragment() { return fragment; } @Override public int hashCode() { return scheme.hashCode() + authority.hashCode() + fragment.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ FragmentAuthorityURI u = (FragmentAuthorityURI)obj; return scheme == u.scheme && authority == u.authority && fragment.equals(u.fragment) ; } return false; } } private static class FragmentPathURI extends PathURI { protected final String fragment; public FragmentPathURI(String scheme, String path, String fragment) { super(scheme, path); this.fragment = fragment; } @Override public URI getURI() { try { return new URI(scheme, "", path, null, fragment); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } @Override public Boolean hasFragment() { return true; } @Override public String getFragment() { return fragment; } @Override public int hashCode() { return scheme.hashCode() + path.hashCode() + fragment.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ FragmentPathURI u = (FragmentPathURI)obj; return scheme == u.scheme && path.equals(u.path) && fragment.equals(u.fragment) ; } return false; } } private static class FragmentPathAuthorityURI extends PathAuthorityURI { protected final String fragment; public FragmentPathAuthorityURI(String scheme, String authority, String path, String fragment) { super(scheme, authority, path); this.fragment = fragment; } @Override public URI getURI() { return buildURIWithAuthority(scheme, authority, path, null, fragment); } @Override public Boolean hasFragment() { return true; } @Override public String getFragment() { return fragment; } @Override public int hashCode() { return scheme.hashCode() + authority.hashCode() + path.hashCode() + fragment.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ FragmentPathAuthorityURI u = (FragmentPathAuthorityURI)obj; return scheme == u.scheme && authority == u.authority && path.equals(u.path) && fragment.equals(u.fragment) ; } return false; } } private static class FragmentQueryURI extends QueryURI { protected final String fragment; public FragmentQueryURI(String scheme, String query, String fragment) { super(scheme, query); this.fragment = fragment; } @Override public URI getURI() { try { return new URI(scheme, "", "/", query, fragment); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } @Override public Boolean hasFragment() { return true; } @Override public String getFragment() { return fragment; } @Override public int hashCode() { return scheme.hashCode() + query.hashCode() + fragment.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ FragmentQueryURI u = (FragmentQueryURI)obj; return scheme == u.scheme && query.equals(u.query) && fragment.equals(u.fragment) ; } return false; } } private static class FragmentQueryAuthorityURI extends QueryAuthorityURI { protected final String fragment; public FragmentQueryAuthorityURI(String scheme, String authority, String query, String fragment) { super(scheme, authority, query); this.fragment = fragment; } @Override public URI getURI() { return buildURIWithAuthority(scheme, authority, null, query, fragment); } @Override public Boolean hasFragment() { return true; } @Override public String getFragment() { return fragment; } @Override public int hashCode() { return scheme.hashCode() + authority.hashCode() + query.hashCode() + fragment.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ FragmentQueryAuthorityURI u = (FragmentQueryAuthorityURI)obj; return scheme == u.scheme && authority == u.authority && query.equals(u.query) && fragment.equals(u.fragment) ; } return false; } } private static class FragmentQueryPathURI extends QueryPathURI { protected final String fragment; public FragmentQueryPathURI(String scheme, String path, String query, String fragment) { super(scheme, path, query); this.fragment = fragment; } @Override public URI getURI() { try { return new URI(scheme, "", path, query, fragment); } catch (URISyntaxException e) { throw new RuntimeException("Internal state corrupted?", e); } } @Override public Boolean hasFragment() { return true; } @Override public String getFragment() { return fragment; } @Override public int hashCode() { return scheme.hashCode() + path.hashCode() + query.hashCode() + fragment.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ FragmentQueryPathURI u = (FragmentQueryPathURI)obj; return scheme == u.scheme && path.equals(u.path) && query.equals(u.query) && fragment.equals(u.fragment) ; } return false; } } private static class FragmentQueryPathAuthorityURI extends QueryPathAuthorityURI { protected final String fragment; public FragmentQueryPathAuthorityURI(String scheme, String authority, String path, String query, String fragment) { super(scheme, authority, path, query); this.fragment = fragment; } @Override public URI getURI() { return buildURIWithAuthority(scheme, authority, path, query, fragment); } @Override public Boolean hasFragment() { return true; } @Override public String getFragment() { return fragment; } @Override public int hashCode() { return scheme.hashCode() + authority.hashCode() + path.hashCode() + query.hashCode() + fragment.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) return true; if(obj.getClass() == getClass()){ FragmentQueryPathAuthorityURI u = (FragmentQueryPathAuthorityURI)obj; return scheme == u.scheme && authority == u.authority && path.equals(u.path) && query.equals(u.query) && fragment.equals(u.fragment) ; } return false; } } }