/*
* 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.handlers.collection;
import io.undertow.server.HttpServerExchange;
import java.time.Instant;
import java.util.List;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.bson.BsonValue;
import org.bson.types.ObjectId;
import org.restheart.Configuration;
import org.restheart.hal.AbstractRepresentationFactory;
import org.restheart.hal.Link;
import org.restheart.hal.Representation;
import org.restheart.hal.UnsupportedDocumentIdException;
import org.restheart.handlers.aggregation.AbstractAggregationOperation;
import org.restheart.handlers.metadata.InvalidMetadataException;
import org.restheart.metadata.checkers.RequestChecker;
import org.restheart.metadata.checkers.JsonSchemaChecker;
import org.restheart.handlers.IllegalQueryParamenterException;
import org.restheart.handlers.RequestContext;
import org.restheart.handlers.RequestContext.TYPE;
import org.restheart.handlers.document.DocumentRepresentationFactory;
import org.restheart.utils.URLUtils;
/**
*
* @author Andrea Di Cesare {@literal <andrea@softinstigate.com>}
*/
public class CollectionRepresentationFactory
extends AbstractRepresentationFactory {
public CollectionRepresentationFactory() {
}
/**
*
* @param exchange
* @param context
* @param embeddedData
* @param size
* @return
* @throws IllegalQueryParamenterException
*/
@Override
public Representation getRepresentation(
HttpServerExchange exchange,
RequestContext context,
List<BsonDocument> embeddedData,
long size)
throws IllegalQueryParamenterException {
final String requestPath = buildRequestPath(exchange);
final Representation rep;
if (context.isFullHalMode()) {
rep = createRepresentation(exchange, context, requestPath);
} else {
rep = createRepresentation(exchange, context, null);
}
if (!context.isNoProps()) {
addProperties(rep, context);
}
addSizeAndTotalPagesProperties(size, context, rep);
addAggregationsLinks(context, rep, requestPath);
addSchemaLinks(rep, context);
addEmbeddedData(embeddedData, rep, requestPath, exchange, context);
if (context.isFullHalMode()) {
addSpecialProperties(
rep,
context.getType(),
context.getCollectionProps());
addPaginationLinks(exchange, context, size, rep);
addLinkTemplates(context, rep, requestPath);
// curies
rep.addLink(new Link("rh", "curies", Configuration.RESTHEART_ONLINE_DOC_URL
+ "/{rel}.html", true), true);
}
return rep;
}
private void addProperties(
final Representation rep,
final RequestContext context) {
// add the collection properties
final BsonDocument collProps = context.getCollectionProps();
rep.addProperties(collProps);
}
public static void addSpecialProperties(
final Representation rep,
final RequestContext.TYPE type,
final BsonDocument data) {
rep.addProperty("_type", new BsonString(type.name()));
Object etag = data.get("_etag");
if (etag != null && etag instanceof ObjectId) {
if (data.get("_lastupdated_on") == null) {
// add the _lastupdated_on in case the _etag field is present and its value is an ObjectId
rep.addProperty(
"_lastupdated_on",
new BsonString(Instant.ofEpochSecond(
((ObjectId) etag).getTimestamp()).toString()));
}
}
}
private void addEmbeddedData(
List<BsonDocument> embeddedData,
final Representation rep,
final String requestPath,
final HttpServerExchange exchange,
final RequestContext context)
throws IllegalQueryParamenterException {
if (embeddedData != null) {
addReturnedProperty(embeddedData, rep);
if (!embeddedData.isEmpty()) {
embeddedDocuments(
embeddedData,
requestPath,
exchange,
context,
rep);
}
} else {
rep.addProperty("_returned", new BsonInt32(0));
}
}
private void addAggregationsLinks(
final RequestContext context,
final Representation rep,
final String requestPath) {
BsonValue _aggregations = context
.getCollectionProps()
.get(AbstractAggregationOperation.AGGREGATIONS_ELEMENT_NAME);
if (_aggregations != null && _aggregations.isArray()) {
BsonArray aggregations = _aggregations.asArray();
aggregations.forEach(q -> {
if (q.isDocument()) {
BsonValue _uri = q.asDocument().get("uri");
if (_uri != null && _uri.isString()) {
String uri = _uri.asString().getValue();
rep.addLink(
new Link(uri,
requestPath
+ "/"
+ RequestContext._AGGREGATIONS + "/"
+ uri));
}
}
});
}
}
private void addLinkTemplates(
final RequestContext context,
final Representation rep,
final String requestPath) {
// link templates and curies
if (context.isParentAccessible()) {
// this can happen due to mongo-mounts mapped URL
rep.addLink(new Link("rh:db", URLUtils.getParentPath(requestPath)));
}
if (TYPE.FILES_BUCKET.equals(context.getType())) {
rep.addLink(new Link(
"rh:bucket",
URLUtils.getParentPath(requestPath)
+ "/{bucketname}"
+ RequestContext.FS_FILES_SUFFIX,
true));
rep.addLink(new Link(
"rh:file",
requestPath + "/{fileid}{?id_type}",
true));
} else if (TYPE.COLLECTION.equals(context.getType())) {
rep.addLink(new Link(
"rh:coll",
URLUtils.getParentPath(requestPath) + "/{collname}",
true));
rep.addLink(new Link(
"rh:document",
requestPath + "/{docid}{?id_type}",
true));
}
rep.addLink(new Link("rh:indexes",
requestPath
+ "/"
+ context.getDBName()
+ "/" + context.getCollectionName()
+ "/_indexes"));
rep.addLink(new Link("rh:filter", requestPath + "{?filter}", true));
rep.addLink(new Link("rh:sort", requestPath + "{?sort_by}", true));
rep.addLink(new Link("rh:paging", requestPath + "{?page}{&pagesize}", true));
rep.addLink(new Link("rh:indexes", requestPath + "/_indexes"));
}
private void embeddedDocuments(
List<BsonDocument> embeddedData,
String requestPath,
HttpServerExchange exchange,
RequestContext context,
Representation rep)
throws IllegalQueryParamenterException {
for (BsonDocument d : embeddedData) {
BsonValue _id = d.get("_id");
if (_id != null
&& RequestContext.isReservedResourceCollection(
_id.toString())) {
rep.addWarning("filtered out reserved resource "
+ requestPath + "/"
+ _id.toString());
} else {
Representation nrep;
if (_id == null) {
nrep = new DocumentRepresentationFactory()
.getRepresentation(
requestPath + "/_null",
exchange,
context,
d);
} else {
nrep = new DocumentRepresentationFactory()
.getRepresentation(
URLUtils.getReferenceLink(requestPath, _id),
exchange,
context,
d);
}
if (context.getType() == RequestContext.TYPE.FILES_BUCKET) {
if (context.isFullHalMode()) {
DocumentRepresentationFactory.addSpecialProperties(
nrep,
TYPE.FILE,
d);
}
rep.addRepresentation("rh:file", nrep);
} else if (context.getType()
== RequestContext.TYPE.SCHEMA_STORE) {
if (context.isFullHalMode()) {
DocumentRepresentationFactory.addSpecialProperties(
nrep,
TYPE.SCHEMA,
d);
}
rep.addRepresentation("rh:schema", nrep);
} else {
if (context.isFullHalMode()) {
DocumentRepresentationFactory.addSpecialProperties(
nrep,
TYPE.DOCUMENT,
d);
}
rep.addRepresentation("rh:doc", nrep);
}
}
}
}
// TODO this is hardcoded, if name of checker is changed in conf file
// method won't work. need to get the name from the configuration
private static final String JSON_SCHEMA_NAME = "jsonSchema";
private static void addSchemaLinks(
Representation rep,
RequestContext context) {
try {
List<RequestChecker> checkers
= RequestChecker.getFromJson(context.getCollectionProps());
if (checkers != null) {
checkers
.stream().filter((RequestChecker c) -> {
return JSON_SCHEMA_NAME.equals(c.getName());
}).forEach((RequestChecker c) -> {
BsonValue schemaId = c.getArgs().asDocument()
.get(JsonSchemaChecker.SCHEMA_ID_PROPERTY);
BsonValue _schemaStoreDb = c.getArgs().asDocument()
.get(JsonSchemaChecker.SCHEMA_STORE_DB_PROPERTY);
// just in case the checker is missing the mandatory schemaId property
if (schemaId == null) {
return;
}
String schemaStoreDb;
if (_schemaStoreDb == null) {
schemaStoreDb = context.getDBName();
} else {
schemaStoreDb = _schemaStoreDb.toString();
}
try {
rep.addLink(new Link("schema", URLUtils
.getUriWithDocId(context,
schemaStoreDb, "_schemas", schemaId)));
} catch (UnsupportedDocumentIdException ex) {
}
});
}
} catch (InvalidMetadataException ime) {
// nothing to do
}
}
}