/*
* RESTHeart - the Web API for MongoDB
* Copyright (C) SoftInstigate Srl
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.restheart.utils;
import io.undertow.server.HttpServerExchange;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Date;
import java.util.Deque;
import java.util.Objects;
import org.bson.BsonBoolean;
import org.bson.BsonDateTime;
import org.bson.BsonDouble;
import org.bson.BsonInt32;
import org.bson.BsonInt64;
import org.bson.BsonMaxKey;
import org.bson.BsonMinKey;
import org.bson.BsonNull;
import org.bson.BsonNumber;
import org.bson.BsonObjectId;
import org.bson.BsonString;
import org.bson.BsonType;
import org.bson.BsonValue;
import org.bson.types.MaxKey;
import org.bson.types.MinKey;
import org.bson.types.ObjectId;
import org.restheart.hal.UnsupportedDocumentIdException;
import org.restheart.handlers.RequestContext;
import org.restheart.handlers.RequestContext.DOC_ID_TYPE;
import static org.restheart.handlers.RequestContext.DOC_ID_TYPE.STRING;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.restheart.handlers.RequestContext.DOC_ID_TYPE_QPARAM_KEY;
/**
*
* @author Andrea Di Cesare {@literal <andrea@softinstigate.com>}
*/
public class URLUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(URLUtils.class);
public static String getReferenceLink(
RequestContext context,
String parentUrl,
BsonValue docId) {
if (context == null || parentUrl == null) {
LOGGER.error("error creating URI, null arguments: "
+ "context = {}, parentUrl = {}, docId = {}",
context,
parentUrl,
docId);
return "";
}
String uri = "#";
if (docId == null) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("_null");
} else if (docId.isString()
&& ObjectId.isValid(docId.asString().getValue())) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.asString().getValue())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.STRING.name());
} else if (docId.isString()) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.asString().getValue());
} else if (docId.isObjectId()) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.asObjectId().getValue().toString());
} else if (docId.isBoolean()) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("_" + docId.asBoolean().getValue());
} else if (docId.isInt32()) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("" + docId.asInt32().getValue())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.NUMBER.name());
} else if (docId.isInt64()) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("" + docId.asInt64().getValue())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.NUMBER.name());
} else if (docId.isDouble()) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("" + docId.asDouble().getValue())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.NUMBER.name());
} else if (docId.isNull()) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/_null");
} else if (docId instanceof BsonMaxKey) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/_MaxKey");
} else if (docId instanceof BsonMinKey) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/_MinKey");
} else if (docId.isDateTime()) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("" + docId.asDateTime().getValue())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.DATE.name());
} else {
String _id;
try {
_id = getIdString(docId);
} catch(UnsupportedDocumentIdException uie) {
_id = docId.toString();
}
context.addWarning("resource with _id: "
+ _id + " does not have an URI "
+ "since the _id is of type "
+ docId.getClass().getSimpleName());
}
return uri;
}
public static String getReferenceLink(String parentUrl, Object docId) {
if (parentUrl == null) {
LOGGER.error("error creating URI, null arguments: "
+ "parentUrl = {}, docId = {}",
parentUrl,
docId);
return "";
}
String uri;
if (docId == null) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("_null");
} else if (docId instanceof String && ObjectId.isValid((String) docId)) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.toString())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.STRING.name());
} else if (docId instanceof String || docId instanceof ObjectId) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.toString());
} else if (docId instanceof BsonObjectId) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(((BsonObjectId) docId).getValue().toString());
} else if (docId instanceof BsonString) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(((BsonString) docId).getValue());
} else if (docId instanceof BsonBoolean) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("_" + ((BsonBoolean) docId).getValue());
} else if (docId instanceof BsonInt32) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("" + ((BsonNumber) docId).asInt32().getValue());
} else if (docId instanceof BsonInt64) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("" + ((BsonNumber) docId).asInt64().getValue());
} else if (docId instanceof BsonDouble) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("" + ((BsonDouble) docId).asDouble().getValue());
} else if (docId instanceof BsonNull) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/_null");
} else if (docId instanceof BsonMaxKey) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/_MaxKey");
} else if (docId instanceof BsonMinKey) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/_MinKey");
} else if (docId instanceof BsonDateTime) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("" + ((BsonDateTime) docId).getValue());
} else if (docId instanceof Integer) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.toString())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.NUMBER.name());
} else if (docId instanceof Long) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.toString())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.NUMBER.name());
} else if (docId instanceof Float) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.toString())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.NUMBER.name());
} else if (docId instanceof Double) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(docId.toString())
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.NUMBER.name());
} else if (docId instanceof MinKey) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("_MinKey");
} else if (docId instanceof MaxKey) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("_MaxKey");
} else if (docId instanceof Date) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat(((Date) docId).getTime() + "")
.concat("?")
.concat(DOC_ID_TYPE_QPARAM_KEY)
.concat("=")
.concat(DOC_ID_TYPE.DATE.name());
} else if (docId instanceof Boolean) {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("_" + (boolean) docId);
} else {
uri = URLUtils.removeTrailingSlashes(parentUrl)
.concat("/")
.concat("_? (unsuppored _id type)");
}
return uri;
}
public static DOC_ID_TYPE checkId(BsonValue id)
throws UnsupportedDocumentIdException {
Objects.nonNull(id);
BsonType type = id.getBsonType();
switch (type) {
case STRING:
return DOC_ID_TYPE.STRING;
case OBJECT_ID:
return DOC_ID_TYPE.OID;
case BOOLEAN:
return DOC_ID_TYPE.BOOLEAN;
case NULL:
return DOC_ID_TYPE.NULL;
case INT32:
return DOC_ID_TYPE.NUMBER;
case INT64:
return DOC_ID_TYPE.NUMBER;
case DOUBLE:
return DOC_ID_TYPE.NUMBER;
case MAX_KEY:
return DOC_ID_TYPE.MAXKEY;
case MIN_KEY:
return DOC_ID_TYPE.MINKEY;
case DATE_TIME:
return DOC_ID_TYPE.DATE;
case TIMESTAMP:
return DOC_ID_TYPE.DATE;
default:
throw new UnsupportedDocumentIdException(
"unknown _id type: "
+ id.getClass()
.getSimpleName());
}
}
/**
* Gets the id as object from its string representation in the document URI
* NOTE: for POST the special string id are checked by
* BodyInjectorHandler.checkSpecialStringId()
*
* @param id
* @param type
* @return
* @throws UnsupportedDocumentIdException
*/
public static BsonValue getDocumentIdFromURI(String id, DOC_ID_TYPE type)
throws UnsupportedDocumentIdException {
if (id == null) {
return null;
}
if (type == null) {
type = DOC_ID_TYPE.STRING_OID;
}
// MaxKey can be also determined from the _id
if (RequestContext.MAX_KEY_ID.equalsIgnoreCase(id)) {
return new BsonMaxKey();
}
// MaxKey can be also determined from the _id
if (RequestContext.MIN_KEY_ID.equalsIgnoreCase(id)) {
return new BsonMinKey();
}
// null can be also determined from the _id
if (RequestContext.NULL_KEY_ID.equalsIgnoreCase(id)) {
return new BsonNull();
}
// true can be also determined from the _id
if (RequestContext.TRUE_KEY_ID.equalsIgnoreCase(id)) {
return new BsonBoolean(true);
}
// false can be also determined from the _id
if (RequestContext.FALSE_KEY_ID.equalsIgnoreCase(id)) {
return new BsonBoolean(false);
}
try {
switch (type) {
case STRING_OID:
return getIdAsStringOrObjectId(id);
case OID:
return getIdAsObjectId(id);
case STRING:
return new BsonString(id);
case NUMBER:
return getIdAsNumber(id);
case MINKEY:
return new BsonMinKey();
case MAXKEY:
return new BsonMaxKey();
case DATE:
return getIdAsDate(id);
case BOOLEAN:
return getIdAsBoolean(id);
}
} catch (IllegalArgumentException iar) {
throw new UnsupportedDocumentIdException(iar);
}
return new BsonString(id);
}
/**
* given string /ciao/this/has/trailings///// returns
* /ciao/this/has/trailings
*
* @param s
* @return the string s without the trailing slashes
*/
static public String removeTrailingSlashes(String s) {
if (s == null || s.length() < 2) {
return s;
}
if (s.trim().charAt(s.length() - 1) == '/') {
return removeTrailingSlashes(s.substring(0, s.length() - 1));
} else {
return s.trim();
}
}
/**
* decode the percent encoded query string
*
* @param qs
* @return the undecoded string
*/
static public String decodeQueryString(String qs) {
try {
return URLDecoder.decode(
qs.replace("+", "%2B"), "UTF-8").replace("%2B", "+");
} catch (UnsupportedEncodingException ex) {
return null;
}
}
/**
*
* @param path
* @return
*/
static public String getParentPath(String path) {
if (path == null || path.isEmpty() || path.equals("/")) {
return path;
}
int lastSlashPos = path.lastIndexOf('/');
if (lastSlashPos > 0) {
return path.substring(0, lastSlashPos); //strip off the slash
} else if (lastSlashPos == 0) {
return "/";
} else {
return ""; //we expect people to add + "/somedir on their own
}
}
/**
*
* @param exchange
* @return
*/
static public String getPrefixUrl(HttpServerExchange exchange) {
return exchange.getRequestURL()
.replaceAll(exchange.getRelativePath(), "");
}
/**
*
* @param context
* @param dbName
* @param collName
* @param id
* @return
* @throws org.restheart.hal.UnsupportedDocumentIdException
*/
static public String getUriWithDocId(
RequestContext context,
String dbName,
String collName,
BsonValue id)
throws UnsupportedDocumentIdException {
DOC_ID_TYPE docIdType = URLUtils.checkId(id);
StringBuilder sb = new StringBuilder();
sb
.append("/")
.append(dbName)
.append("/")
.append(collName)
.append("/")
.append(getIdAsStringNoBrachets(id));
if (docIdType == DOC_ID_TYPE.STRING
&& ObjectId.isValid(id.asString().getValue())) {
sb.append("?id_type=STRING");
} else if (docIdType != DOC_ID_TYPE.STRING
&& docIdType != DOC_ID_TYPE.OID) {
sb.append("?id_type=").append(docIdType.name());
}
return context.mapUri(sb.toString());
}
/**
*
* @param context
* @param dbName
* @param collName
* @param ids
* @return
* @throws org.restheart.hal.UnsupportedDocumentIdException
*/
static public String getUriWithFilterMany(
RequestContext context,
String dbName,
String collName,
BsonValue[] ids)
throws UnsupportedDocumentIdException {
StringBuilder sb = new StringBuilder();
///db/coll/?filter={"ref":{"$in":{"a","b","c"}}
sb.append("/").append(dbName).append("/").append(collName).append("?")
.append("filter={").append("'")
.append("_id").append("'").append(":")
.append("{'$in'").append(":")
.append(getIdsString(ids)).append("}}");
return context.mapUri(sb.toString());
}
/**
*
* @param context
* @param dbName
* @param collName
* @param referenceField
* @param id
* @return
* @throws org.restheart.hal.UnsupportedDocumentIdException
*/
static public String getUriWithFilterOne(
RequestContext context,
String dbName,
String collName,
String referenceField,
BsonValue id)
throws UnsupportedDocumentIdException {
StringBuilder sb = new StringBuilder();
///db/coll/?filter={"ref":{"$in":{"a","b","c"}}
sb.append("/").append(dbName).append("/").append(collName).append("?")
.append("filter={").append("'")
.append(referenceField).append("'")
.append(":")
.append(getIdString(id))
.append("}");
return context.mapUri(sb.toString());
}
/**
*
* @param context
* @param dbName
* @param collName
* @param referenceField
* @param id
* @return
* @throws org.restheart.hal.UnsupportedDocumentIdException
*/
static public String getUriWithFilterManyInverse(
RequestContext context,
String dbName,
String collName,
String referenceField,
BsonValue id)
throws UnsupportedDocumentIdException {
StringBuilder sb = new StringBuilder();
///db/coll/?filter={'referenceField':{"$elemMatch":{'ids'}}}
sb.append("/").append(dbName).append("/").append(collName).append("?")
.append("filter={'").append(referenceField)
.append("':{").append("'$elemMatch':{'$eq':")
.append(getIdString(id)).append("}}}");
return JsonUtils.minify(context.mapUri(sb.toString()));
}
/**
*
* @param exchange
* @param paramsToRemove
* @return
*/
public static String getQueryStringRemovingParams(HttpServerExchange exchange, String... paramsToRemove) {
String ret = exchange.getQueryString();
if (ret == null || ret.isEmpty() || paramsToRemove == null) {
return ret;
}
for (String key : paramsToRemove) {
Deque<String> values = exchange.getQueryParameters().get(key);
if (values != null) {
for (String value : values) {
ret = ret.replaceAll(key + "=" + value + "&", "");
ret = ret.replaceAll(key + "=" + value + "$", "");
}
}
}
return ret;
}
private static BsonNumber getIdAsNumber(String id) throws IllegalArgumentException {
BsonValue ret = JsonUtils.parse(id);
if (ret.isNumber()) {
return ret.asNumber();
} else {
throw new IllegalArgumentException("The id is not a valid number " + id);
}
}
private static BsonDateTime getIdAsDate(String id) throws IllegalArgumentException {
BsonValue ret = JsonUtils.parse(id);
if (ret.isDateTime()) {
return ret.asDateTime();
} else if (ret.isInt32()) {
return new BsonDateTime(0l + ret.asInt32().getValue());
} else if (ret.isInt64()) {
return new BsonDateTime(ret.asInt64().getValue());
} else {
throw new IllegalArgumentException("The id is not a valid number " + id);
}
}
private static BsonBoolean getIdAsBoolean(String id) throws IllegalArgumentException {
if (id.equals(RequestContext.TRUE_KEY_ID)) {
return new BsonBoolean(true);
}
if (id.equals(RequestContext.FALSE_KEY_ID)) {
return new BsonBoolean(false);
}
return null;
}
private static BsonObjectId getIdAsObjectId(String id)
throws IllegalArgumentException {
if (!ObjectId.isValid(id)) {
throw new IllegalArgumentException("The id is not a valid ObjectId " + id);
}
return new BsonObjectId(new ObjectId(id));
}
private static BsonValue getIdAsStringOrObjectId(String id) {
if (ObjectId.isValid(id)) {
return getIdAsObjectId(id);
}
return new BsonString(id);
}
private static String getIdAsStringNoBrachets(BsonValue id)
throws UnsupportedDocumentIdException {
if (id == null) {
return null;
} else if (id.isString()) {
return id.asString().getValue();
} else {
return JsonUtils.minify(
JsonUtils.toJson(id));
}
}
private static String getIdString(BsonValue id)
throws UnsupportedDocumentIdException {
if (id == null) {
return null;
} else if (id.isString()) {
return "'" + id.asString().getValue() + "'";
} else {
return JsonUtils.minify(JsonUtils.toJson(id)
.replace("\"", "'"));
}
}
private static String getIdsString(BsonValue[] ids)
throws UnsupportedDocumentIdException {
if (ids == null) {
return null;
}
int cont = 0;
String[] _ids = new String[ids.length];
for (BsonValue id : ids) {
_ids[cont] = getIdString(id);
cont++;
}
return JsonUtils.minify(Arrays.toString(_ids));
}
private URLUtils() {
}
}