All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.restheart.exchange.MongoRequest Maven / Gradle / Ivy

There is a newer version: 8.1.7
Show newest version
/*-
 * ========================LICENSE_START=================================
 * restheart-commons
 * %%
 * Copyright (C) 2019 - 2024 SoftInstigate
 * %%
 * 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.
 * =========================LICENSE_END==================================
 */
package org.restheart.exchange;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Deque;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonInvalidOperationException;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.json.JsonMode;
import org.bson.json.JsonParseException;
import static org.restheart.exchange.ExchangeKeys.ADMIN;
import static org.restheart.exchange.ExchangeKeys.BINARY_CONTENT;
import static org.restheart.exchange.ExchangeKeys.CACHE_QPARAM_KEY;
import static org.restheart.exchange.ExchangeKeys.COLL_META_DOCID_PREFIX;
import static org.restheart.exchange.ExchangeKeys.CONFIG;
import static org.restheart.exchange.ExchangeKeys.DB_META_DOCID;
import org.restheart.exchange.ExchangeKeys.DOC_ID_TYPE;
import static org.restheart.exchange.ExchangeKeys.ETAG_CHECK_QPARAM_KEY;
import static org.restheart.exchange.ExchangeKeys.FS_CHUNKS_SUFFIX;
import static org.restheart.exchange.ExchangeKeys.FS_FILES_SUFFIX;
import org.restheart.exchange.ExchangeKeys.HAL_MODE;
import static org.restheart.exchange.ExchangeKeys.JSON_MODE_QPARAM_KEY;
import static org.restheart.exchange.ExchangeKeys.LOCAL;
import static org.restheart.exchange.ExchangeKeys.META_COLLNAME;
import static org.restheart.exchange.ExchangeKeys.NO_CACHE_QPARAM_KEY;
import static org.restheart.exchange.ExchangeKeys.NO_PROPS_KEY;
import static org.restheart.exchange.ExchangeKeys.NUL;
import static org.restheart.exchange.ExchangeKeys.READ_CONCERN_QPARAM_KEY;
import static org.restheart.exchange.ExchangeKeys.READ_PREFERENCE_QPARAM_KEY;
import org.restheart.exchange.ExchangeKeys.REPRESENTATION_FORMAT;
import static org.restheart.exchange.ExchangeKeys.RESOURCES_WILDCARD_KEY;
import static org.restheart.exchange.ExchangeKeys.SYSTEM;
import org.restheart.exchange.ExchangeKeys.TYPE;
import static org.restheart.exchange.ExchangeKeys.WRITE_CONCERN_QPARAM_KEY;
import org.restheart.exchange.ExchangeKeys.WRITE_MODE;
import static org.restheart.exchange.ExchangeKeys.WRITE_MODE_QPARAM_KEY;
import static org.restheart.exchange.ExchangeKeys.WRITE_MODE_SHORT_QPARAM_KEY;
import static org.restheart.exchange.ExchangeKeys._AGGREGATIONS;
import static org.restheart.exchange.ExchangeKeys._INDEXES;
import static org.restheart.exchange.ExchangeKeys._META;
import static org.restheart.exchange.ExchangeKeys._SCHEMAS;
import static org.restheart.exchange.ExchangeKeys._SESSIONS;
import static org.restheart.exchange.ExchangeKeys._SIZE;
import static org.restheart.exchange.ExchangeKeys._STREAMS;
import static org.restheart.exchange.ExchangeKeys._TRANSACTIONS;
import org.restheart.mongodb.RSOps;
import org.restheart.mongodb.db.sessions.ClientSessionImpl;
import static org.restheart.utils.BsonUtils.document;
import org.restheart.utils.MongoServiceAttachments;
import org.restheart.utils.URLUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import io.undertow.util.PathTemplateMatch;

/**
 *
 * Request implementation used by MongoService and backed by BsonValue that
 * provides simplified methods to deal with headers and query parameters
 * specific to mongo requests
 *
 * @author Andrea Di Cesare {@literal }
 */
public class MongoRequest extends BsonRequest {
    private static final Logger LOGGER = LoggerFactory.getLogger(MongoRequest.class);

    private final String whereUri;
    private final String whatUri;

    private final String[] pathTokens;

    private BsonDocument dbProps;
    private BsonDocument collectionProps;

    private InputStream fileInputStream;

    private int page = 1;
    private int pagesize = 100;
    private boolean count = false;
    private boolean etagCheckRequired = false;
    private WRITE_MODE writeMode = null;
    private boolean cache;
    private Deque filter = null;
    private BsonDocument aggregationVars = null; // aggregation vars
    private Deque keys = null;
    private Deque sortBy = null;
    private Deque hint = null;
    private DOC_ID_TYPE docIdType = DOC_ID_TYPE.STRING_OID;
    private final TYPE type;

    private REPRESENTATION_FORMAT representationFormat;

    private BsonValue documentId;

    private String mappedUri = null;
    private String unmappedUri = null;

    private final String etag;

    private boolean forceEtagCheck = false;

    private BsonDocument shardKey = null;

    private boolean noProps = false;

    private ClientSessionImpl clientSession = null;

    /**
     * the HAL mode
     */
    private HAL_MODE halMode = HAL_MODE.FULL;

    private final long requestStartTime = System.currentTimeMillis();

    /**
     * path template match
     */
    private final PathTemplateMatch pathTemplateMatch;

    /**
     * the json mode
     */
    private final JsonMode jsonMode;

    /**
     * noCache switch, e.g.: ?nocache=true
     */
    final boolean noCache;

    private final Optional rsOps;

    protected MongoRequest(HttpServerExchange exchange, String requestUri, String resourceUri) {
        super(exchange);

        this.whereUri = URLUtils.removeTrailingSlashes(requestUri == null
            ? null
            : requestUri.startsWith("/") ? requestUri
            : "/" + requestUri);

        this.whatUri = URLUtils.removeTrailingSlashes(resourceUri == null ? null
            : resourceUri.startsWith("/") || "*".equals(resourceUri) ? resourceUri
            : "/" + resourceUri);

        this.mappedUri = exchange.getRequestPath();

        if (exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY) != null) {
            this.pathTemplateMatch = exchange.getAttachment(PathTemplateMatch.ATTACHMENT_KEY);
        } else {
            this.pathTemplateMatch = null;
        }

        this.unmappedUri = unmapUri(exchange.getRequestPath());

        // "/db/collection/document" --> { "", "mappedDbName", "collection", "document" }
        this.pathTokens = this.unmappedUri.split(SLASH);

        this.type = selectRequestType(pathTokens);

        // etag
        var etagHvs = getHeaders() == null ? null : getHeaders().get(Headers.IF_MATCH);

        this.etag = etagHvs == null || etagHvs.getFirst() == null ? null : etagHvs.getFirst();

        this.forceEtagCheck = exchange.getQueryParameters().get(ETAG_CHECK_QPARAM_KEY) != null;

        this.cache = exchange.getQueryParameters().get(CACHE_QPARAM_KEY) != null;

        this.noProps = exchange.getQueryParameters().get(NO_PROPS_KEY) != null;

        var _jsonMode = exchange.getQueryParameters().containsKey(JSON_MODE_QPARAM_KEY)
            ? exchange.getQueryParameters().get(JSON_MODE_QPARAM_KEY).getFirst().toUpperCase()
            : null;

        if (_jsonMode != null) {
            JsonMode mode;

            try {
                mode = JsonMode.valueOf(_jsonMode.toUpperCase());
            } catch (IllegalArgumentException iae) {
                mode = null;
            }

            this.jsonMode = mode;
        } else {
            this.jsonMode = null;
        }

        this.noCache = exchange.getQueryParameters().get(NO_CACHE_QPARAM_KEY) != null;

        // writeMode
        var _writeMode = exchange.getQueryParameters().containsKey(WRITE_MODE_QPARAM_KEY)
            ? exchange.getQueryParameters().get(WRITE_MODE_QPARAM_KEY).getFirst().toUpperCase()
            : exchange.getQueryParameters().containsKey(WRITE_MODE_SHORT_QPARAM_KEY)
            ? exchange.getQueryParameters().get(WRITE_MODE_SHORT_QPARAM_KEY).getFirst().toUpperCase()
            : defaultWriteMode();

        WRITE_MODE mode;

        try {
            mode = WRITE_MODE.valueOf(_writeMode.toUpperCase());
        } catch (IllegalArgumentException iae) {
            mode = WRITE_MODE.valueOf(defaultWriteMode());
        }

        this.writeMode = mode;

        var anyRsSet = false;
        var _rsOps = new RSOps();

        // readConcern
        if (exchange.getQueryParameters().containsKey(READ_CONCERN_QPARAM_KEY)) {
            try {
                _rsOps = _rsOps.withReadConcern(exchange.getQueryParameters().get(READ_CONCERN_QPARAM_KEY).getFirst());
                anyRsSet = true;
            } catch (IllegalArgumentException iae) {
                // nothing to do
            }
        }

        // readPreference
        if (exchange.getQueryParameters().containsKey(READ_PREFERENCE_QPARAM_KEY)) {
            try {
                _rsOps = _rsOps.withReadPreference(exchange.getQueryParameters().get(READ_PREFERENCE_QPARAM_KEY).getFirst());
                anyRsSet = true;
            } catch (IllegalArgumentException iae) {
                // nothing to do
            }
        }

        // writeConcern
        if (exchange.getQueryParameters().containsKey(WRITE_CONCERN_QPARAM_KEY)) {
            _rsOps = _rsOps.withWriteConcern(exchange.getQueryParameters().get(WRITE_CONCERN_QPARAM_KEY).getFirst());
            anyRsSet = true;
        }

        this.rsOps = anyRsSet ? Optional.of(_rsOps) : Optional.empty();
        LOGGER.debug("ReplicaSet connection options: {}", _rsOps);
    }

    private String defaultWriteMode() {
        if (isPost()) {
            return WRITE_MODE.INSERT.name();
        } else if (isPatch() || isPut()) {
            return WRITE_MODE.UPDATE.name();
        } else {
            return WRITE_MODE.UPSERT.name();
        }
    }

    /**
     *
     * @param exchange
     *
     * the exchange request path (mapped uri) is rewritten replacing the
     * resourceUri with the requestUri
     *
     * the special resourceUri value * means any resource: the requestUri is
     * mapped to the root resource /
     *
     * example 1
     *
     * resourceUri = /db/mycollection requestUri = /
     *
     * then the requestPath / is rewritten to /db/mycollection
     *
     * example 2
     *
     * resourceUri = * requestUri = /data
     *
     * then the requestPath /data is rewritten to /
     *
     * @param requestUri the request URI to map to the resource URI
     * @param resourceUri the resource URI identifying a resource in the DB
     * @return the MongoRequest
     */
    public static MongoRequest init(HttpServerExchange exchange, String requestUri, String resourceUri) {
        return new MongoRequest(exchange, requestUri, resourceUri);
    }

    public static MongoRequest of(HttpServerExchange exchange) {
        return of(exchange, MongoRequest.class);
    }

    /**
     *
     * @param dbName
     * @return true if the dbName is a reserved resource
     */
    public static boolean isReservedDbName(String dbName) {
        return dbName == null
            ? false
            : "".equals(dbName)
                || dbName.equalsIgnoreCase(ADMIN)
                || dbName.equalsIgnoreCase(CONFIG)
                || dbName.equalsIgnoreCase(LOCAL)
                || dbName.startsWith(SYSTEM);
    }

    /**
     *
     * @param collectionName
     * @return true if the collectionName is a reserved resource
     */
    public static boolean isReservedCollectionName(String collectionName) {
        return collectionName == null
            ? false
            : "".equals(collectionName)
                || collectionName.startsWith(SYSTEM)
                || collectionName.endsWith(FS_CHUNKS_SUFFIX)
                || collectionName.equals(META_COLLNAME);
    }

    /**
     *
     * @param type
     * @param documentId
     * @return true if the documentIdRaw is a reserved resource
     */
    @SuppressWarnings("deprecation")
    public static boolean isReservedDocumentId(TYPE type, BsonValue documentId) {
        if (documentId == null || !documentId.isString()) {
            return false;
        }

        var sdi = documentId.asString().getValue();

        if ((type == TYPE.COLLECTION_META && sdi.startsWith(COLL_META_DOCID_PREFIX))
            || (type == TYPE.DB_META && sdi.startsWith(DB_META_DOCID))
            || (type == TYPE.BULK_DOCUMENTS && RESOURCES_WILDCARD_KEY.equals(sdi))
            || (type == TYPE.COLLECTION_SIZE && _SIZE.equalsIgnoreCase(sdi))
            || (type == TYPE.INDEX && _INDEXES.equalsIgnoreCase(sdi))
            || (type == TYPE.COLLECTION_META && _META.equalsIgnoreCase(sdi))
            || (type == TYPE.INVALID && _AGGREGATIONS.equalsIgnoreCase(sdi))
            || (type == TYPE.INVALID && _STREAMS.equalsIgnoreCase(sdi))) {
            return false;
        } else {
            return DB_META_DOCID.equalsIgnoreCase(sdi) || sdi.startsWith(COLL_META_DOCID_PREFIX);
        }
    }

    /**
     *
     * @return type
     */
    public TYPE getType() {
        return type;
    }

    @SuppressWarnings("deprecation")
    static TYPE selectRequestType(String[] pathTokens) {
        TYPE type;

        if (pathTokens.length > 0 && pathTokens[pathTokens.length - 1].equalsIgnoreCase(_SIZE)) {
            if (pathTokens.length == 2) {
                type = TYPE.ROOT_SIZE;
            } else if (pathTokens.length == 3) {
                type = TYPE.DB_SIZE;
            } else if (pathTokens.length == 4 && pathTokens[2].endsWith(FS_FILES_SUFFIX)) {
                type = TYPE.FILES_BUCKET_SIZE;
            } else if (pathTokens.length == 4 && pathTokens[2].equalsIgnoreCase(_SCHEMAS)) {
                type = TYPE.SCHEMA_STORE_SIZE;
            } else if (pathTokens.length == 4) {
                type = TYPE.COLLECTION_SIZE;
            } else {
                type = TYPE.INVALID;
            }
        } else if (pathTokens.length > 2 && pathTokens[pathTokens.length - 1].equalsIgnoreCase(_META)) {
            if (pathTokens.length == 3) {
                type = TYPE.DB_META;
            } else if (pathTokens.length == 4 && pathTokens[2].endsWith(FS_FILES_SUFFIX)) {
                type = TYPE.FILES_BUCKET_META;
            } else if (pathTokens.length == 4 && pathTokens[2].equalsIgnoreCase(_SCHEMAS)) {
                type = TYPE.SCHEMA_STORE_META;
            } else if (pathTokens.length == 4) {
                type = TYPE.COLLECTION_META;
            } else {
                type = TYPE.INVALID;
            }
        } else if (pathTokens.length < 2) {
            type = TYPE.ROOT;
        } else if (pathTokens.length == 2 && pathTokens[pathTokens.length - 1].equalsIgnoreCase(_SESSIONS)) {
            type = TYPE.SESSIONS;
        } else if (pathTokens.length == 3 && pathTokens[pathTokens.length - 2].equalsIgnoreCase(_SESSIONS)) {
            type = TYPE.SESSION;
        } else if (pathTokens.length == 4 && pathTokens[pathTokens.length - 3].equalsIgnoreCase(_SESSIONS) && pathTokens[pathTokens.length - 1].equalsIgnoreCase(_TRANSACTIONS)) {
            type = TYPE.TRANSACTIONS;
        } else if (pathTokens.length == 5 && pathTokens[pathTokens.length - 4].equalsIgnoreCase(_SESSIONS) && pathTokens[pathTokens.length - 2].equalsIgnoreCase(_TRANSACTIONS)) {
            type = TYPE.TRANSACTION;
        } else if (pathTokens.length < 3) {
            type = TYPE.DB;
        } else if (pathTokens.length >= 3 && pathTokens[2].endsWith(FS_FILES_SUFFIX)) {
            if (pathTokens.length == 3) {
                type = TYPE.FILES_BUCKET;
            } else if (pathTokens.length == 4 && pathTokens[3].equalsIgnoreCase(_INDEXES)) {
                type = TYPE.COLLECTION_INDEXES;
            } else if (pathTokens.length == 4 && !pathTokens[3].equalsIgnoreCase(_INDEXES) && !pathTokens[3].equals(RESOURCES_WILDCARD_KEY)) {
                type = TYPE.FILE;
            } else if (pathTokens.length > 4 && pathTokens[3].equalsIgnoreCase(_INDEXES)) {
                type = TYPE.INDEX;
            } else if (pathTokens.length > 4 && pathTokens[3].equalsIgnoreCase(_AGGREGATIONS)) {
                type = TYPE.AGGREGATION;
            } else if (pathTokens.length > 4 && !pathTokens[3].equalsIgnoreCase(_INDEXES) && !pathTokens[4].equalsIgnoreCase(BINARY_CONTENT)) {
                type = TYPE.FILE;
            } else if (pathTokens.length == 5 && pathTokens[4].equalsIgnoreCase(BINARY_CONTENT)) {
                // URL: /db/bucket.filePath/xxx/binary
                type = TYPE.FILE_BINARY;
            } else {
                type = TYPE.DOCUMENT;
            }
        } else if (pathTokens.length >= 3 && pathTokens[2].equalsIgnoreCase(_SCHEMAS)) {
            if (pathTokens.length == 3) {
                type = TYPE.SCHEMA_STORE;
            } else if (pathTokens[3].equals(RESOURCES_WILDCARD_KEY)) {
                type = TYPE.BULK_DOCUMENTS;
            } else {
                type = TYPE.SCHEMA;
            }
        } else if (pathTokens.length < 4) {
            type = TYPE.COLLECTION;
        } else if (pathTokens.length == 4 && pathTokens[3].equalsIgnoreCase(_INDEXES)) {
            type = TYPE.COLLECTION_INDEXES;
        } else if (pathTokens.length == 4 && pathTokens[3].equals(RESOURCES_WILDCARD_KEY)) {
            type = TYPE.BULK_DOCUMENTS;
        } else if (pathTokens.length > 4 && pathTokens[3].equalsIgnoreCase(_INDEXES)) {
            type = TYPE.INDEX;
        } else if (pathTokens.length == 4 && pathTokens[3].equalsIgnoreCase(_AGGREGATIONS)) {
            type = TYPE.INVALID;
        } else if (pathTokens.length > 4 && pathTokens[3].equalsIgnoreCase(_AGGREGATIONS)) {
            type = TYPE.AGGREGATION;
        } else if (pathTokens.length == 4 && pathTokens[3].equalsIgnoreCase(_STREAMS)) {
            type = TYPE.INVALID;
        } else if (pathTokens.length > 4 && pathTokens[3].equalsIgnoreCase(_STREAMS)) {
            type = TYPE.CHANGE_STREAM;
        } else {
            type = TYPE.DOCUMENT;
        }

        return type;
    }

    /**
     * given a mapped uri (/some/mapping/coll) returns the canonical uri
     * (/db/coll) URLs are mapped to mongodb resources by using the mongo-mounts
     * configuration properties. note that the mapped uri can make use of path
     * templates (/some/{path}/template/*)
     *
     * @param mappedUri
     * @return
     */
    private String unmapUri(String mappedUri) {
        // don't unmap URIs statring with /_sessions
        if (mappedUri.startsWith("/".concat(_SESSIONS))) {
            return mappedUri;
        }

        if (this.pathTemplateMatch == null) {
            return unmapPathUri(mappedUri);
        } else {
            return unmapPathTemplateUri(mappedUri);
        }
    }

    private String unmapPathUri(String mappedUri) {
        var ret = URLUtils.removeTrailingSlashes(mappedUri);

        if (whatUri.equals("*")) {
            if (!this.whereUri.equals(SLASH)) {
                ret = ret.replaceFirst("^" + this.whereUri, "");
            }
        } else if (!this.whereUri.equals(SLASH)) {
            ret = URLUtils.removeTrailingSlashes(ret.replaceFirst("^" + this.whereUri, this.whatUri));
        } else {
            ret = URLUtils.removeTrailingSlashes(URLUtils.removeTrailingSlashes(this.whatUri) + ret);
        }

        return ret.isEmpty() ? SLASH : ret;
    }

    private String unmapPathTemplateUri(String mappedUri) {
        var ret = URLUtils.removeTrailingSlashes(mappedUri);
        var rewriteUri = replaceParamsWithActualValues();

        var replacedWhatUri = replaceParamsWithinWhatUri();
        // replace params with in whatUri
        // eg what: /{account}, where: /{account}/*

        // now replace mappedUri with resolved path template
        if (replacedWhatUri.equals("*")) {
            if (!this.whereUri.equals(SLASH)) {
                ret = ret.replaceFirst("^" + rewriteUri, "");
            }
        } else if (!this.whereUri.equals(SLASH)) {
            ret = URLUtils.removeTrailingSlashes(ret.replaceFirst("^" + rewriteUri, replacedWhatUri));
        } else {
            ret = URLUtils.removeTrailingSlashes(URLUtils.removeTrailingSlashes(replacedWhatUri) + ret);
        }

        return ret.isEmpty() ? SLASH : ret;
    }

    /**
     * given a canonical uri (/db/coll) returns the mapped uri
     * (/some/mapping/uri) relative to this context. URLs are mapped to mongodb
     * resources via the mongo-mounts configuration properties
     *
     * @param unmappedUri
     * @return
     */
    public String mapUri(String unmappedUri) {
        if (this.pathTemplateMatch == null) {
            return mapPathUri(unmappedUri);
        } else {
            return mapPathTemplateUri(unmappedUri);
        }
    }

    private String mapPathUri(String unmappedUri) {
        var ret = URLUtils.removeTrailingSlashes(unmappedUri);

        if (whatUri.equals("*")) {
            if (!this.whereUri.equals(SLASH)) {
                return this.whereUri + unmappedUri;
            }
        } else {
            ret = URLUtils.removeTrailingSlashes(ret.replaceFirst("^" + this.whatUri, this.whereUri));
        }

        if (ret.isEmpty()) {
            ret = SLASH;
        } else {
            ret = ret.replaceAll("//", "/");
        }

        return ret;
    }

    private String mapPathTemplateUri(String unmappedUri) {
        var ret = URLUtils.removeTrailingSlashes(unmappedUri);
        var rewriteUri = replaceParamsWithActualValues();
        var replacedWhatUri = replaceParamsWithinWhatUri();

        // now replace mappedUri with resolved path template
        if (replacedWhatUri.equals("*")) {
            if (!this.whereUri.equals(SLASH)) {
                return rewriteUri + unmappedUri;
            }
        } else {
            ret = URLUtils.removeTrailingSlashes(ret.replaceFirst("^" + replacedWhatUri, rewriteUri));
        }

        return ret.isEmpty() ? SLASH : ret;
    }

    private String replaceParamsWithinWhatUri() {
        String uri = this.whatUri;
        // replace params within whatUri
        // eg what: /{prefix}_db, where: /{prefix}/*
        for (var key : this.pathTemplateMatch.getParameters().keySet()) {
            uri = uri.replace("{".concat(key).concat("}"), this.pathTemplateMatch.getParameters().get(key));
        }
        return uri;
    }

    private String replaceParamsWithActualValues() {
        // path template with variables resolved to actual values
        var rewriteUri = this.pathTemplateMatch.getMatchedTemplate();
        // remove trailing wildcard from template
        if (rewriteUri.endsWith("/*")) {
            rewriteUri = rewriteUri.substring(0, rewriteUri.length() - 2);
        }
        // collect params
        this.pathTemplateMatch
            .getParameters()
            .keySet()
            .stream()
            .filter(key -> !key.equals("*"))
            .collect(Collectors.toMap(key -> key, key -> this.pathTemplateMatch.getParameters().get(key)));
        // replace params with actual values
        for (var key : this.pathTemplateMatch.getParameters().keySet()) {
            rewriteUri = rewriteUri.replace("{".concat(key).concat("}"), this.pathTemplateMatch.getParameters().get(key));
        }
        return rewriteUri;
    }

    /**
     * check if the parent of the requested resource is accessible in this
     * request context
     *
     * for instance if /db/mycollection is mapped to /coll then:
     *
     * the db is accessible from the collection the root is not accessible from
     * the collection (since / is actually mapped to the db)
     *
     * @return true if parent of the requested resource is accessible
     */
    public boolean isParentAccessible() {
        return getType() == TYPE.DB
            ? mappedUri.split(SLASH).length > 1
            : mappedUri.split(SLASH).length > 2;
    }

    /**
     *
     * @return DB Name
     */
    public String getDBName() {
        return getPathTokenAt(1);
    }

    /**
     *
     * @return collection name
     */
    public String getCollectionName() {
        return getPathTokenAt(2);
    }

    /**
     *
     * @return document id
     */
    public String getDocumentIdRaw() {
        return getPathTokenAt(3);
    }

    /**
     *
     * @return index id
     */
    public String getIndexId() {
        return getPathTokenAt(4);
    }

    /**
     *
     * @return the txn id or null if request type is not SESSIONS, TRANSACTIONS
     * or TRANSACTION
     */
    public String getSid() {
        return isTxn() || isTxns() || isSessions() ? getPathTokenAt(2) : null;
    }

    /**
     *
     * @return the txn id or null if request type is not TRANSACTION
     */
    public Long getTxnId() {
        return isTxn() ? Long.valueOf(getPathTokenAt(4)) : null;
    }

    /**
     *
     * @return collection name
     */
    public String getAggregationOperation() {
        return getPathTokenAt(4);
    }

    /**
     * @return change stream operation name
     */
    public String getChangeStreamOperation() {
        return getPathTokenAt(4);
    }

    /**
     *
     * @return URI
     * @throws URISyntaxException
     */
    public URI getUri() throws URISyntaxException {
        return new URI(Arrays.asList(pathTokens)
            .stream()
            .reduce(SLASH, (t1, t2) -> t1 + SLASH + t2));
    }

    /**
     *
     * @return isReservedResource
     */
    public boolean isReservedResource() {
        if (getType() == TYPE.ROOT) {
            return false;
        }

        return isReservedDbName(getDBName()) || isReservedCollectionName(getCollectionName()) || isReservedDocumentId(getType(), getDocumentId());
    }

    /**
     * @return the whereUri
     */
    public String getUriPrefix() {
        return whereUri;
    }

    /**
     * @return the whatUri
     */
    public String getMappingUri() {
        return whatUri;
    }

    /**
     * @return the page
     */
    public int getPage() {
        return page;
    }

    /**
     * @param page the page to set
     */
    public void setPage(int page) {
        this.page = page;
    }

    /**
     * @return the pagesize
     */
    public int getPagesize() {
        return pagesize;
    }

    /**
     * @param pagesize the pagesize to set
     */
    public void setPagesize(int pagesize) {
        this.pagesize = pagesize;
    }

    /**
     * @return the representationFormat
     */
    public REPRESENTATION_FORMAT getRepresentationFormat() {
        return representationFormat;
    }

    /**
     * sets representationFormat
     *
     * @param representationFormat
     */
    public void setRepresentationFormat(REPRESENTATION_FORMAT representationFormat) {
        this.representationFormat = representationFormat;
    }

    /**
     * @return the count
     */
    public boolean isCount() {
        return count
            || getType() == TYPE.ROOT_SIZE
            || getType() == TYPE.COLLECTION_SIZE
            || getType() == TYPE.FILES_BUCKET_SIZE
            || getType() == TYPE.SCHEMA_STORE_SIZE;
    }

    /**
     * @param count the count to set
     */
    public void setCount(boolean count) {
        this.count = count;
    }

    /**
     * @return the filter
     */
    public Deque getFilter() {
        return filter;
    }

    /**
     * @param filter the filter to set
     */
    public void setFilter(Deque filter) {
        this.filter = filter;
    }

    /**
     * @return the hint
     */
    public Deque getHint() {
        return hint;
    }

    /**
     * @param hint the hint to set
     */
    public void setHint(Deque hint) {
        this.hint = hint;
    }

    /**
     *
     * @return the $and composed filter qparam values
     */
    public BsonDocument getFiltersDocument() throws JsonParseException {
        final var filterQuery = new BsonDocument();

        if (filter != null) {
            if (filter.size() > 1) {
                var _filters = new BsonArray();

                filter.stream().forEach(f -> _filters.add(BsonDocument.parse(f)));

                filterQuery.put("$and", _filters);
            } else if (filter.size() == 1) {
                filterQuery.putAll(BsonDocument.parse(filter.getFirst()));  // this can throw JsonParseException for invalid filter parameters
            } else {
                return filterQuery;
            }
        }

        return filterQuery;
    }

    /**
     *
     * @return @throws JsonParseException
     */
    public BsonDocument getSortByDocument() throws JsonParseException {
        if (sortBy == null) {
            return document().put("_id", -1).get();
        } else {
            var ret = document();

            sortBy.stream().map(String::trim).forEach((s) -> {
                // manage the case where sort_by is a json object
                try {
                    var _s = BsonDocument.parse(s);
                    ret.putAll(_s.asDocument());
                } catch (JsonParseException e) {
                    // if we cannot parse it as a document,
                    // assume it as a string property name
                    // unless it starts with {
                    if (s.startsWith("{")) {
                        throw new JsonParseException("Invalid sort parameter", e);
                    } else if (s.startsWith("-")) {
                        ret.put(s.substring(1), -1);
                    } else if (s.startsWith("+")) {
                        ret.put(s.substring(1), 1);
                    } else {
                        ret.put(s, 1);
                    }
                } catch(BsonInvalidOperationException biop) {
                    throw new JsonParseException("Invalid sort parameter", biop);
                }
            });

            return ret.get();
        }
    }

    /**
     *
     * @return @throws JsonParseException
     */
    public BsonDocument getHintDocument() throws JsonParseException {
        if (hint == null || hint.isEmpty()) {
            return null;
        } else {
            var ret = new BsonDocument();
            hint.stream().forEach(s -> {
                var _s = s.strip(); // the + sign is decoded into a space, in case remove it

                // manage the case where hint is a json object
                try {
                    var _hint = BsonDocument.parse(_s);

                    ret.putAll(_hint);
                } catch (JsonParseException e) {
                    // ret is just a string, i.e. an index name
                    if (_s.startsWith("-")) {
                        ret.put(_s.substring(1), new BsonInt32(-1));
                    } else if (_s.startsWith("+")) {
                        ret.put(_s.substring(1), new BsonInt32(11));
                    } else {
                        ret.put(_s, new BsonInt32(1));
                    }
                }
            });

            return ret;
        }
    }

    /**
     *
     * @return @throws JsonParseException
     */
    public BsonDocument getProjectionDocument() throws JsonParseException {
        if (keys == null || keys.isEmpty()) {
            return null;
        } else {
            final var projection = new BsonDocument();
            // this can throw JsonParseException for invalid keys parameters
            keys.stream().forEach(f -> projection.putAll(BsonDocument.parse(f)));
            return projection;
        }
    }

    /**
     * @return the aggregationVars
     */
    public BsonDocument getAggregationVars() {
        return aggregationVars;
    }

    /**
     * @param aggregationVars the aggregationVars to set
     */
    public void setAggregationVars(BsonDocument aggregationVars) {
        this.aggregationVars = aggregationVars;
    }

    /**
     * @return the sortBy
     */
    public Deque getSortBy() {
        return sortBy;
    }

    /**
     * @param sortBy the sortBy to set
     */
    public void setSortBy(Deque sortBy) {
        this.sortBy = sortBy;
    }

    /**
     * @return the collectionProps
     */
    public BsonDocument getCollectionProps() {
        return collectionProps;
    }

    /**
     * @param collectionProps the collectionProps to set
     */
    public void setCollectionProps(BsonDocument collectionProps) {
        this.collectionProps = collectionProps;
    }

    /**
     * @return the dbProps
     */
    public BsonDocument getDbProps() {
        return dbProps;
    }

    /**
     * @param dbProps the dbProps to set
     */
    public void setDbProps(BsonDocument dbProps) {
        this.dbProps = dbProps;
    }

    /**
     *
     * The unmapped uri is the cononical uri of a mongodb resource (e.g.
     * /db/coll).
     *
     * @return the unmappedUri
     */
    public String getUnmappedRequestUri() {
        return unmappedUri;
    }

    /**
     * The mapped uri is the exchange request uri. This is "mapped" by the
     * mongo-mounts mapping paramenters.
     *
     * @return the mappedUri
     */
    public String getMappedRequestUri() {
        return mappedUri;
    }

    /**
     * if mongo-mounts specifies a path template (i.e. /{foo}/*) this returns
     * the request template parameters {@literal (/x/y => foo=x, *=y) }
     *
     * @return
     */
    public Map getPathTemplateParamenters() {
        if (this.pathTemplateMatch == null) {
            return null;
        } else {
            return this.pathTemplateMatch.getParameters();
        }
    }

    /**
     *
     * @param index
     * @return pathTokens[index] if pathTokens.length > index, else null
     */
    private String getPathTokenAt(int index) {
        return pathTokens.length > index ? pathTokens[index] : null;
    }

    /**
     *
     * @return the cache
     */
    public boolean isCache() {
        return cache;
    }

    /**
     * @param cache true to use caching
     */
    public void setCache(boolean cache) {
        this.cache = cache;
    }

    /**
     * @return the docIdType
     */
    public DOC_ID_TYPE getDocIdType() {
        return docIdType;
    }

    /**
     * @param docIdType the docIdType to set
     */
    public void setDocIdType(DOC_ID_TYPE docIdType) {
        this.docIdType = docIdType;
    }

    /**
     * @param documentId the documentId to set
     */
    public void setDocumentId(BsonValue documentId) {
        this.documentId = documentId;
    }

    /**
     * @return the documentId
     */
    public BsonValue getDocumentId() {
        if (isDbMeta()) {
            return new BsonString(DB_META_DOCID);
        } else if (isCollectionMeta()) {
            return new BsonString(COLL_META_DOCID_PREFIX.concat(getPathTokenAt(2)));
        } else {
            return documentId;
        }
    }

    /**
     * @return the clientSession
     */
    public ClientSessionImpl getClientSession() {
        return this.clientSession;
    }

    /**
     * @param clientSession the clientSession to set
     */
    public void setClientSession(ClientSessionImpl clientSession) {
        this.clientSession = clientSession;
    }

    /**
     * @return the jsonMode as specified by jsonMode query paramter
     */
    public JsonMode getJsonMode() {
        return jsonMode;
    }

    @Override
    public BsonValue parseContent() throws BadRequestException, IOException {
        // the MongoRequest content can have been already attached to the exchange
        // with MongoServiceAttachments.attachBsonContent()
        // for instance, by an Interceptor at interceptPoint=BEFORE_EXCHANGE_INIT
        var attacheBsonContent = MongoServiceAttachments.attachedBsonContent(wrapped);
        return attacheBsonContent == null
            ? MongoRequestContentInjector.inject(wrapped)
            : attacheBsonContent;
    }

    /**
     *
     * @return the fileInputStream in case of a file resouces the fileInputStream, null othewise
     * @throws org.restheart.exchange.BadRequestException
     * @throws java.io.IOException
     */
    public InputStream getFileInputStream() throws BadRequestException, IOException {
        if (!isContentInjected()) {
            LOGGER.debug("getFileInputStream() called but content has not been injected yet. Let's inject it.");
            MongoRequestContentInjector.inject(wrapped); // inject calls request.setFileInputStream(fileInputStream);
        }

        return fileInputStream;
    }


    /**
     * @param fileInputStream the fileInputStream to set
     */
    public void setFileInputStream(InputStream fileInputStream) {
        this.fileInputStream = fileInputStream;
        setContentInjected(true);
    }

    /**
     * @return keys
     */
    public Deque getKeys() {
        return keys;
    }

    /**
     * @param keys keys to set
     */
    public void setKeys(Deque keys) {
        this.keys = keys;
    }

    /**
     * @return the halMode
     */
    public HAL_MODE getHalMode() {
        return halMode;
    }

    /**
     *
     * @return
     */
    public boolean isFullHalMode() {
        return halMode == HAL_MODE.FULL || halMode == HAL_MODE.F;
    }

    /**
     * @param halMode the halMode to set
     */
    public void setHalMode(HAL_MODE halMode) {
        this.halMode = halMode;
    }

    /**
     * Seehttps://docs.mongodb.org/v3.2/reference/limits/#naming-restrictions
     * @return
     */
    public boolean isDbNameInvalid() {
        return isDbNameInvalid(getDBName());
    }

    /**
     *
     * @return
     */
    public long getRequestStartTime() {
        return requestStartTime;
    }

    /**
     * @param dbName
     * Seehttps://docs.mongodb.org/v3.2/reference/limits/#naming-restrictions
     * @return
     */
    public boolean isDbNameInvalid(String dbName) {
        return (dbName == null
            || dbName.contains(NUL)
            || dbName.contains(" ")
            || dbName.contains("/")
            || dbName.contains("\\")
            || dbName.contains(".")
            || dbName.contains("\"")
            || dbName.contains("$")
            || dbName.length() > 64
            || dbName.length() == 0);
    }

    /**
     * Seehttps://docs.mongodb.org/v3.2/reference/limits/#naming-restrictions
     * @return
     */
    public boolean isDbNameInvalidOnWindows() {
        return isDbNameInvalidOnWindows(getDBName());
    }

    /**
     * @param dbName
     * Seehttps://docs.mongodb.org/v3.2/reference/limits/#naming-restrictions
     * @return
     */
    public boolean isDbNameInvalidOnWindows(String dbName) {
        return (isDbNameInvalid()
            || dbName.contains("*")
            || dbName.contains("<")
            || dbName.contains(">")
            || dbName.contains(":")
            || dbName.contains(".")
            || dbName.contains("|")
            || dbName.contains("?"));
    }

    /**
     * Seehttps://docs.mongodb.org/v3.2/reference/limits/#naming-restrictions
     * @return
     */
    public boolean isCollectionNameInvalid() {
        return isCollectionNameInvalid(getCollectionName());
    }

    /**
     * @param collectionName
     * Seehttps://docs.mongodb.org/v3.2/reference/limits/#naming-restrictions
     * @return
     */
    public boolean isCollectionNameInvalid(String collectionName) {
        // collection starting with system. will return FORBIDDEN

        return (collectionName == null
            || collectionName.contains(NUL)
            || collectionName.contains("$")
            || collectionName.length() == 64);
    }

    /**
     *
     * @return
     */
    public String getETag() {
        return etag;
    }

    /**
     * @return the shardKey
     */
    public BsonDocument getShardKey() {
        return shardKey;
    }

    /**
     * @param shardKey the shardKey to set
     */
    public void setShardKey(BsonDocument shardKey) {
        this.shardKey = shardKey;
    }

    /**
     * @return the noProps
     */
    public boolean isNoProps() {
        return noProps;
    }

    /**
     * @param noProps the noProps to set
     */
    public void setNoProps(boolean noProps) {
        this.noProps = noProps;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.AGGREGATION
     */
    public boolean isAggregation() {
        return getType() == TYPE.AGGREGATION;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.BULK_DOCUMENTS
     */
    public boolean isBulkDocuments() {
        return getType() == TYPE.BULK_DOCUMENTS;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.COLLECTION
     */
    public boolean isCollection() {
        return getType() == TYPE.COLLECTION;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.COLLECTION_INDEXES
     */
    public boolean isCollectionIndexes() {
        return getType() == TYPE.COLLECTION_INDEXES;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.DB
     */
    public boolean isDb() {
        return getType() == TYPE.DB;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.DOCUMENT
     */
    public boolean isDocument() {
        return getType() == TYPE.DOCUMENT;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.FILE
     */
    public boolean isFile() {
        return getType() == TYPE.FILE;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.FILES_BUCKET
     */
    public boolean isFilesBucket() {
        return getType() == TYPE.FILES_BUCKET;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.FILE_BINARY
     */
    public boolean isFileBinary() {
        return getType() == TYPE.FILE_BINARY;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.INDEX
     */
    public boolean isIndex() {
        return getType() == TYPE.INDEX;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.ROOT
     */
    public boolean isRoot() {
        return getType() == TYPE.ROOT;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.SESSIONS
     */
    public boolean isSessions() {
        return getType() == TYPE.SESSIONS;
    }

     /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.SESSION
     */
    public boolean isSession() {
        return getType() == TYPE.SESSION;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.TRANSACTIONS
     */
    public boolean isTxns() {
        return getType() == TYPE.TRANSACTIONS;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.TRANSACTION
     */
    public boolean isTxn() {
        return getType() == TYPE.TRANSACTION;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.SCHEMA
     */
    public boolean isSchema() {
        return getType() == TYPE.SCHEMA;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.SCHEMA_STORE
     */
    public boolean isSchemaStore() {
        return getType() == TYPE.SCHEMA_STORE;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.ROOT_SIZE
     */
    public boolean isRootSize() {
        return getType() == TYPE.ROOT_SIZE;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.DB_SIZE
     */
    public boolean isDbSize() {
        return getType() == TYPE.DB_SIZE;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.DB_META
     */
    public boolean isDbMeta() {
        return getType() == TYPE.DB_META;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.COLLECTION_SIZE
     */
    public boolean isCollectionSize() {
        return getType() == TYPE.COLLECTION_SIZE;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.COLLECTION_META
     */
    public boolean isCollectionMeta() {
        return getType() == TYPE.COLLECTION_META;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.FILES_BUCKET_SIZE
     */
    public boolean isFilesBucketSize() {
        return getType() == TYPE.FILES_BUCKET_SIZE;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.FILES_BUCKET_META
     */
    public boolean isFilesBucketMeta() {
        return getType() == TYPE.FILES_BUCKET_META;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.SCHEMA_STORE_SIZE
     */
    public boolean isSchemaStoreSize() {
        return getType() == TYPE.SCHEMA_STORE_SIZE;
    }

    /**
     * helper method to check request resource type
     *
     * @return true if type is TYPE.SCHEMA_STORE_SIZE
     */
    public boolean isSchemaStoreMeta() {
        return getType() == TYPE.SCHEMA_STORE_META;
    }

    /**
     * helper method to check if a request writes a document or a file or a
     * schema
     *
     * @return true if the request writes a document or a file or a schema
     */
    public boolean isWriteDocument() {
        return (((isPut() || isPatch()) && (isFile() || isDocument() || isSchema()))
            || isPost()
            && (isCollection() || isFilesBucket() || isSchemaStore()));
    }

    /**
     * @return the isETagCheckRequired
     */
    public boolean isETagCheckRequired() {
        return etagCheckRequired;
    }

    /**
     * @param etagCheckRequired
     */
    public void setETagCheckRequired(boolean etagCheckRequired) {
        this.etagCheckRequired = etagCheckRequired;
    }

    /**
     * @return the write mode
     */
    public WRITE_MODE getWriteMode() {
        return writeMode;
    }

    /**
     * @param writeMode the write mode to set
     */
    public void setWriteMode(WRITE_MODE writeMode) {
        this.writeMode = writeMode;
    }

    /**
     * @return the forceEtagCheck
     */
    public boolean isForceEtagCheck() {
        return forceEtagCheck;
    }

    /**
     * @return the noCache
     */
    public boolean isNoCache() {
        return noCache;
    }

    /**
     * ReplicaSet connection otpions
     *
     * @return the _rsOps
     */
    public Optional rsOps() {
        return rsOps;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy