org.elastos.did.DIDURL Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of didsdk Show documentation
Show all versions of didsdk Show documentation
Elastos decentralized identity library
The newest version!
/*
* Copyright (c) 2019 Elastos Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.elastos.did;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.elastos.did.DIDEntity.SerializeContext;
import org.elastos.did.VerifiableCredential.Proof;
import org.elastos.did.exception.MalformedDIDURLException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
/**
* Class DIDURL represents a Uniform Resource Locator, a pointer to
* a specific DID resource. It can be used to retrieve things like
* representations of DID subjects, verification methods, services,
* specific parts of a DID document, or other resources.
*/
@JsonSerialize(using = DIDURL.Serializer.class)
@JsonDeserialize(using = DIDURL.Deserializer.class)
public class DIDURL implements Comparable {
private DID did;
private String path;
private Map query;
private String queryString;
private String fragment;
private String repr;
private AbstractMetadata metadata;
/**
* Constructs a DIDURL object with the given DID context and a url string.
*
* @param context a DID context of the DIDURL object, if the url is a relative DIDURL
* @param url a string representation of DIDURL
* @throws MalformedDIDURLException if the url in wrong syntax
*/
public DIDURL(DID context, String url) throws MalformedDIDURLException {
Parser parser = new Parser();
parser.parse(context, url);
}
/**
* Constructs a DIDURL object.
*
* @param url a string representation of DIDURL
* @throws MalformedDIDURLException if the url in wrong syntax
*/
public DIDURL(String url) throws MalformedDIDURLException {
this(null, url);
}
/**
* Constructs a DIDURL object with the given DID context and a DIDURL object.
*
* @param context a DID context of the DIDURL object, if the url is a relative DIDURL
* @param url a DIDURL object
*/
public DIDURL(DID context, DIDURL url) {
checkArgument(context != null || url != null, "Invalid context and url");
if (context != null)
this.did = context;
if (url != null) {
if (url.did != null)
this.did = url.did;
this.path = url.path;
this.query = url.query;
this.queryString = url.queryString;
this.fragment = url.fragment;
this.repr = url.repr;
this.metadata = url.metadata;
} else {
this.query = Collections.emptyMap();
}
}
/**
* Constructs a DIDURL object from DID object.
*
* @param did a DID context of the DIDURL object
*/
private DIDURL(DID context) {
this(context, (DIDURL)null);
}
private DIDURL() {
}
private DIDURL deepClone(boolean readonly) {
DIDURL result = new DIDURL();
result.did = this.did;
result.path = this.path;
result.query = (this.query.isEmpty() && readonly) ? Collections.emptyMap() :
new LinkedHashMap(this.query);
result.queryString = this.queryString;
result.fragment = this.fragment;
result.repr = this.repr;
return result;
}
/**
* Create a DIDURL object from the given string. The method will parse the
* DIDURL object from the string if the string is not empty. Otherwise
* will return null. If the parsed DIDURL object is relative, then use
* context as it's base DID.
*
* @param context a DID context of the DIDURL object, if the url is a relative DIDURL
* @param url a string representation of DIDURL
* @return a DIDURL object if url isn't null or empty; otherwise null
* @throws MalformedDIDURLException if the url in wrong syntax
*/
public static DIDURL valueOf(DID context, String url) throws MalformedDIDURLException {
return (url == null || url.isEmpty()) ? null : new DIDURL(context, url);
}
/**
* Create a DIDURL object from the given string. The method will parse the
* DIDURL object from the string if the string is not empty. Otherwise
* will return null. If the parsed DIDURL object is relative, then use
* context as it's base DID.
*
* @param context a DID context of the DIDURL object, if the url is a relative DIDURL
* @param url a string representation of DIDURL
* @return a DIDURL object if url isn't null or empty; otherwise null
* @throws MalformedDIDURLException if the url in wrong syntax
*/
public static DIDURL valueOf(String context, String url) throws MalformedDIDURLException {
return (url == null || url.isEmpty()) ? null : new DIDURL(DID.valueOf(context), url);
}
/**
* Create a DIDURL object from the given string. The method will parse the
* DIDURL object from the string if the string is not empty. Otherwise
* will return null.
*
* @param url a string representation of DIDURL
* @return a DIDURL object if url isn't null or empty; otherwise null
* @throws MalformedDIDURLException if the url in wrong syntax
*/
public static DIDURL valueOf(String url) throws MalformedDIDURLException {
return (url == null || url.isEmpty()) ? null : new DIDURL(url);
}
/**
* Get the base DID of the DIDURL object.
*
* @return the DID object
*/
public DID getDid() {
return did;
}
/**
* Set the base DID of the DIDURL object.
*
* @param did the DID Object, could be null
*/
protected void setDid(DID did) {
this.did = did;
}
private String mapToString(Map map, String sep) {
boolean init = true;
StringBuilder builder = new StringBuilder(512);
for (Map.Entry entry : map.entrySet()) {
if (init)
init = false;
else
builder.append(sep);
builder.append(entry.getKey());
if (entry.getValue() != null)
builder.append("=").append(entry.getValue());
}
return builder.toString();
}
/**
* Get the decoded path component of this DIDURL.
*
* @return the decoded path component of this DIDURL, or null if
* the path is undefined
*/
public String getPath() {
return path;
}
/**
* Get the decoded query component of this DIDURL.
*
* @return the decoded query component of this DIDURL,
* or null if the query is undefined
*/
public String getQueryString() {
if (query.isEmpty())
return null;
if (queryString == null)
queryString = mapToString(query, "&");
return queryString;
}
/**
* Get the query component of this DIDURL as a map.
*
* @return the decoded query component of this DIDURL,
* or null if the query is undefined
*/
public Map getQuery() {
return Collections.unmodifiableMap(query);
}
/**
* Get the value of the given query parameter.
*
* @param name the parameter name
* @return the value of parameter or null if the given parameter is undefined
*/
public String getQueryParameter(String name) {
checkArgument(name != null && !name.isEmpty(), "Invalid parameter name");
return query.get(name);
}
/**
* Check whether the given parameter exists in the query component.
*
* @param name the name of parameter
* @return true if the has a parameter with given name or false otherwise
*/
public boolean hasQueryParameter(String name) {
checkArgument(name != null && !name.isEmpty(), "Invalid parameter name");
return query.containsKey(name);
}
/**
* Get the decoded fragment component of this DIDURL.
*
* @return the fragment string
*/
public String getFragment() {
return fragment;
}
/**
* Set the metadata that related with this DIDURL object.
*
* @param metadata a metadata object
*/
protected void setMetadata(AbstractMetadata metadata) {
this.metadata = metadata;
}
/**
* Get the metadata object that associated with this DIDURL object.
*
* @return the metadata object
*/
public AbstractMetadata getMetadata() {
return metadata;
}
/**
* Return the string representation of this DIDURL object. If the base DID
* isn't null, the result will be a relative representation.
*
* @param context a context DID reference object for relative DIDURL
* @return a relative string representation of this DIDURL object
*/
protected String toString(DID context) {
StringBuilder builder = new StringBuilder(512);
if (did != null && (context == null || !did.equals(context)))
builder.append(did);
if (path != null && !path.isEmpty())
builder.append(path);
if (query != null && !query.isEmpty())
builder.append("?").append(getQueryString());
if (fragment != null && !fragment.isEmpty())
builder.append("#").append(getFragment());
return builder.toString();
}
/**
* Return the string representation of this DIDURL object.
*
* @return a string representation of this DIDURL object
*/
@Override
public String toString() {
if (repr == null)
repr = toString(null);
return repr;
}
/**
* Compares this DIDURL to the specified object. The result is true if and
* only if the argument is not null and is a DIDURL object that represents
* the same resource.
*
* @param obj the object to compare this DID against
* @return true if the given object represents a DIDURL equivalent to this
* resource, false otherwise
*/
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (obj instanceof DIDURL) {
DIDURL id = (DIDURL)obj;
return toString().equals(id.toString());
}
if (obj instanceof String) {
String url = (String)obj;
return toString().equals(url);
}
return false;
}
/**
* Compares this DIDURL with the specified DIDURL.
*
* @param id DIDURL to which this DIDURL is to be compared
* @return -1, 0 or 1 as this DIDURL is less than, equal to,
* or greater than id
*/
@Override
public int compareTo(DIDURL id) {
checkNotNull(id, "id is null");
return toString().compareTo(id.toString());
}
/**
* Returns a hash code for this DIDURL object.
*
* @return a hash code value for this object
*/
@Override
public int hashCode() {
return toString().hashCode();
}
/* =========================================================================
*
* DID and DIDURL syntax definition
*
* did:elastos:method-specific-string[;params][/path][?query][#fragment]
*
* didurl
* : did? ('/' path)? ('?' query)? ('#' fragment)? SPACE?
* ;
*
* did
* : 'did' ':' method ':' methodSpecificString
* ;
*
* method
* : STRING
* ;
*
* methodSpecificString
* : STRING
* ;
*
* path
* : STRING ('/' STRING)*
* ;
*
* query
* : queryParam ('&' queryParam)*
* ;
*
* queryParam
* : queryParamName ('=' queryParamValue)?
* ;
*
* queryParamName
* : STRING
* ;
*
* queryParamValue
* : STRING
* ;
*
* fragment
* : STRING
* ;
*
* STRING
* : ([a-zA-Z~0-9] | HEX) ([a-zA-Z0-9._\-] | HEX)*
* ;
*
* HEX
* : ('%' [a-fA-F0-9] [a-fA-F0-9])+
* ;
*
* SPACE
* : [ \t\n\r]+
* ;
*
=========================================================================*/
class Parser {
private boolean isHexChar(char ch) {
return ((ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f') ||
(ch >= '0' && ch <= '9'));
}
private boolean isTokenChar(char ch, boolean start) {
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
(ch >= '0' && ch <= '9'))
return true;
if (start)
return false;
else
return (ch == '.' || ch == '_' || ch == '-');
}
private int scanNextPart(String url, int start, int limit,
String partSeps, String tokenSeps) throws MalformedDIDURLException {
int nextPart = limit;
boolean tokenStart = true;
for (int i = start; i < limit; i++) {
char ch = url.charAt(i);
if (partSeps != null && partSeps.indexOf(ch) >= 0) {
nextPart = i;
break;
}
if (tokenSeps != null && tokenSeps.indexOf(ch) >= 0) {
if (tokenStart)
throw new MalformedDIDURLException("Invalid char at: " + i);
tokenStart = true;
continue;
}
if (isTokenChar(ch, tokenStart)) {
tokenStart = false;
continue;
}
if (ch == '%') {
if (i + 2 >= limit)
throw new MalformedDIDURLException("Invalid char at: " + i);
char seq = url.charAt(++i);
if (!isHexChar(seq))
throw new MalformedDIDURLException("Invalid hex char at: " + i);
seq = url.charAt(++i);
if (!isHexChar(seq))
throw new MalformedDIDURLException("Invalid hex char at: " + i);
tokenStart = false;
continue;
}
throw new MalformedDIDURLException("Invalid char at: " + i);
}
return nextPart;
}
public void parse(DID context, String url) throws MalformedDIDURLException {
DIDURL.this.did = context;
if (url == null)
throw new MalformedDIDURLException("null DIDURL string");
int start = 0;
int limit = url.length();
int nextPart;
// trim the leading and trailing spaces
while ((limit > 0) && (url.charAt(limit - 1) <= ' '))
limit--; //eliminate trailing whitespace
while ((start < limit) && (url.charAt(start) <= ' '))
start++; // eliminate leading whitespace
if (start == limit) // empty url string
throw new MalformedDIDURLException("empty DIDURL string");
int pos = start;
// DID
if (pos < limit && url.regionMatches(pos, "did:", 0, 4)) {
nextPart = scanNextPart(url, pos, limit, "/?#", ":");
try {
DIDURL.this.did = new DID(url, pos, nextPart);
} catch (Exception e) {
throw new MalformedDIDURLException("Invalid did at: " + pos, e);
}
pos = nextPart;
}
// path
if (pos < limit && url.charAt(pos) == '/') {
nextPart = scanNextPart(url, pos + 1, limit, "?#", "/");
DIDURL.this.path = url.substring(pos, nextPart);
pos = nextPart;
}
// query
if (pos < limit && url.charAt(pos) == '?') {
nextPart = scanNextPart(url, pos + 1, limit, "#", "&=");
String queryString = url.substring(pos + 1, nextPart);
pos = nextPart;
if (!queryString.isEmpty()) {
Map query = new LinkedHashMap();
String[] pairs = queryString.split("&");
for (String pair : pairs) {
String[] parts = pair.split("=");
if (parts.length > 0 && !parts[0].isEmpty()) {
String name = parts[0];
String value = parts.length == 2 ? parts[1] : null;
query.put(name, value);
}
}
DIDURL.this.query = query;
}
} else {
DIDURL.this.query = Collections.emptyMap();
}
// fragment
// condition: pos == start
// Compatible with v1, support fragment without leading '#'
if ((pos < limit && url.charAt(pos) == '#') || (pos == start)) {
if (url.charAt(pos) == '#')
pos++;
nextPart = scanNextPart(url, pos, limit, "", null);
String fragment = url.substring(pos, nextPart);
if (!fragment.isEmpty())
DIDURL.this.fragment = fragment;
}
}
}
static class Serializer extends StdSerializer {
private static final long serialVersionUID = -5560151545310632117L;
public Serializer() {
this(null);
}
public Serializer(Class t) {
super(t);
}
@Override
public void serialize(DIDURL id, JsonGenerator gen,
SerializerProvider provider) throws IOException {
SerializeContext context = (SerializeContext)provider.getConfig()
.getAttributes().getAttribute(DIDEntity.CONTEXT_KEY);
// TODO: checkme
DID base = null;
if (!context.isNormalized())
base = context.getDid() != null ? context.getDid() : id.getDid();
gen.writeString(id.toString(base));
}
}
static class NormalizedSerializer extends StdSerializer {
private static final long serialVersionUID = -5560151545310632117L;
public NormalizedSerializer() {
this(null);
}
public NormalizedSerializer(Class t) {
super(t);
}
@Override
public void serialize(DIDURL id, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeString(id.toString());
}
}
static class Deserializer extends StdDeserializer {
private static final long serialVersionUID = -3649714336670800081L;
public Deserializer() {
this(null);
}
public Deserializer(Class t) {
super(t);
}
@Override
public DIDURL deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonToken token = p.getCurrentToken();
if (!token.equals(JsonToken.VALUE_STRING))
throw ctxt.weirdStringException(p.getText(), DIDURL.class, "Invalid DIDURL");
String url = p.getText().trim();
return url.isEmpty() ? null : new DIDURL(null, url);
}
}
/**
* Builder class to create or modify a DIDURL.
*/
public static class Builder {
private DIDURL url;
/**
* Create DIDURL builder object with given url as default pattern.
*
* @param url a DIDURL object
*/
public Builder(DIDURL url) {
this.url = url.deepClone(false);
}
/**
* Create DIDURL builder object with given did as base DID.
*
* @param did a DID object as the base DID
*/
public Builder(DID did) {
this(new DIDURL(did));
}
/**
* Set the base DID object of the DIDURL that to be build.
*
* @param did a DID object, could be null
* @return the builder instance for method chaining
*/
public Builder setDid(DID did) {
url.setDid(did);
return this;
}
/**
* Set the base DID object of the DIDURL that to be build.
*
* @param did a string representation of DID, could be null
* @return the builder instance for method chaining
*/
public Builder setDid(String did) {
return setDid(DID.valueOf(did));
}
/**
* Set the path component of the DIDURL object.
*
* @param path a path string
* @return the builder instance for method chaining
*/
public Builder setPath(String path) {
url.path = path == null || path.isEmpty() ? null : path;
return this;
}
/**
* Sets a query parameter with given value.
*
* @param name a query parameter name
* @param value the parameter value
* @return the builder instance for method chaining
*/
public Builder setQueryParameter(String name, String value) {
checkArgument(name != null && !name.isEmpty(), "Invalid parameter name");
url.query.put(name, value);
return this;
}
/**
* Sets query parameters with given map object. All the previous
* parameters and values will be clear.
*
* @param params a string/string map object for query parameters
* @return the builder instance for method chaining
*/
public Builder setQueryParameters(Map params) {
url.query.clear();
if (params != null && params.size() > 0)
url.query.putAll(params);
return this;
}
/**
* Remove the specific parameter from the query parameters.
*
* @param name the parameter name to be remove
* @return the builder instance for method chaining
*/
public Builder removeQueryParameter(String name) {
checkArgument(name != null && !name.isEmpty(), "Invalid parameter name");
url.query.remove(name);
return this;
}
/**
* Remove all the existing parameters from the query parameters component.
*
* @return the builder instance for method chaining
*/
public Builder clearQueryParameters() {
url.query.clear();
return this;
}
/**
* Set the fragment component.
*
* @param fragment a fragment string
* @return the builder instance for method chaining
*/
public Builder setFragment(String fragment) {
url.fragment = fragment == null || fragment.isEmpty() ? null : fragment;
return this;
}
/**
* Get the DIDURL instance that created by this builder object.
*
*
* After build a DIDURL object from this Builder object,
* the builder still available with the same status that before call
* the build method.
*
*
* @return a DIDURL object
*/
public DIDURL build() {
return url.deepClone(true);
}
}
}