/*
* 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.metadata;
import org.restheart.handlers.metadata.InvalidMetadataException;
import org.restheart.handlers.RequestContext;
import org.restheart.utils.URLUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonValue;
import org.restheart.hal.UnsupportedDocumentIdException;
import org.restheart.utils.JsonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author Andrea Di Cesare {@literal <andrea@softinstigate.com>}
*/
public class Relationship {
private static final Logger LOGGER = LoggerFactory.getLogger(Relationship.class);
public enum TYPE {
ONE_TO_ONE,
ONE_TO_MANY,
MANY_TO_ONE,
MANY_TO_MANY
};
public enum ROLE {
OWNING,
INVERSE
};
public static final String RELATIONSHIPS_ELEMENT_NAME = "rels";
public static final String REL_ELEMENT_NAME = "rel";
public static final String TYPE_ELEMENT_NAME = "type";
public static final String ROLE_ELEMENT_NAME = "role";
public static final String TARGET_DB_ELEMENT_NAME = "target-db";
public static final String TARGET_COLLECTION_ELEMENT_NAME = "target-coll";
public static final String REF_ELEMENT_NAME = "ref-field";
private final String rel;
private final TYPE type;
private final ROLE role;
private final String targetDb;
private final String targetCollection;
private final String referenceField;
/**
*
* @param rel
* @param type
* @param role
* @param targetDb
* @param targetCollection
* @param referenceField
*/
public Relationship(
String rel,
TYPE type,
ROLE role,
String targetDb,
String targetCollection,
String referenceField) {
this.rel = rel;
this.type = type;
this.role = role;
this.targetDb = targetDb;
this.targetCollection = targetCollection;
this.referenceField = referenceField;
}
/**
*
* @param rel
* @param type
* @param role
* @param targetDb
* @param targetCollection
* @param referenceField
* @throws InvalidMetadataException
*/
public Relationship(
String rel,
String type,
String role,
String targetDb,
String targetCollection,
String referenceField)
throws InvalidMetadataException {
this.rel = rel;
try {
this.type = TYPE.valueOf(type);
} catch (IllegalArgumentException iae) {
throw new InvalidMetadataException(
"invalid type value: "
+ type
+ ". valid values are "
+ Arrays.toString(TYPE.values()),
iae);
}
try {
this.role = ROLE.valueOf(role);
} catch (IllegalArgumentException iae) {
throw new InvalidMetadataException(
"invalid role value "
+ role
+ ". valid values are "
+ Arrays.toString(ROLE.values()),
iae);
}
this.targetDb = targetDb;
this.targetCollection = targetCollection;
this.referenceField = referenceField;
}
/**
*
* @param collProps
* @return
* @throws InvalidMetadataException
*/
public static List<Relationship> getFromJson(BsonDocument collProps)
throws InvalidMetadataException {
if (collProps == null) {
return null;
}
ArrayList<Relationship> ret = new ArrayList<>();
BsonValue _rels = collProps.get(RELATIONSHIPS_ELEMENT_NAME);
if (_rels == null) {
return ret;
}
if (!_rels.isArray()) {
throw new InvalidMetadataException(
"element '"
+ RELATIONSHIPS_ELEMENT_NAME
+ "' is not an array list."
+ _rels);
}
BsonArray rels = _rels.asArray();
for (BsonValue _rel : rels.getValues()) {
if (!_rel.isDocument()) {
throw new InvalidMetadataException(
"element '"
+ RELATIONSHIPS_ELEMENT_NAME
+ "' is not valid."
+ _rel);
}
BsonDocument rel = _rel.asDocument();
ret.add(getRelFromJson(rel));
}
return ret;
}
private static Relationship getRelFromJson(BsonDocument content)
throws InvalidMetadataException {
BsonValue _rel = content.get(REL_ELEMENT_NAME);
BsonValue _type = content.get(TYPE_ELEMENT_NAME);
BsonValue _role = content.get(ROLE_ELEMENT_NAME);
BsonValue _targetDb = content.get(TARGET_DB_ELEMENT_NAME);
BsonValue _targetCollection = content.get(TARGET_COLLECTION_ELEMENT_NAME);
BsonValue _referenceField = content.get(REF_ELEMENT_NAME);
if (_rel == null || !_rel.isString()) {
throw new InvalidMetadataException(
(_rel == null ? "missing " : "invalid ")
+ REL_ELEMENT_NAME
+ " element.");
}
if (_type == null || !_type.isString()) {
throw new InvalidMetadataException(
(_type == null ? "missing " : "invalid ")
+ TYPE_ELEMENT_NAME
+ " element.");
}
if (_role == null || !_role.isString()) {
throw new InvalidMetadataException(
(_role == null ? "missing " : "invalid ")
+ ROLE_ELEMENT_NAME
+ " element.");
}
if (_targetDb != null && !_targetDb.isString()) {
throw new InvalidMetadataException(
"invalid "
+ TARGET_DB_ELEMENT_NAME
+ " field.");
}
if (_targetCollection == null || !_targetCollection.isString()) {
throw new InvalidMetadataException(
(_targetCollection == null ? "missing " : "invalid ")
+ TARGET_COLLECTION_ELEMENT_NAME
+ " element.");
}
if (_referenceField == null || !_referenceField.isString()) {
throw new InvalidMetadataException(
(_referenceField == null ? "missing " : "invalid ")
+ REF_ELEMENT_NAME
+ " element.");
}
String rel = _rel.asString().getValue();
String type = _type.asString().getValue();
String role = _role.asString().getValue();
String targetDb = _targetDb == null
? null
: _targetDb.asString().getValue();
String targetCollection = _targetCollection.asString().getValue();
String referenceField = _referenceField.asString().getValue();
return new Relationship(
rel,
type,
role,
targetDb,
targetCollection,
referenceField);
}
/**
*
* @param context
* @param dbName
* @param collName
* @param data
* @return
* @throws IllegalArgumentException
* @throws org.restheart.hal.UnsupportedDocumentIdException
*/
public String getRelationshipLink(
RequestContext context,
String dbName,
String collName,
BsonDocument data)
throws IllegalArgumentException, UnsupportedDocumentIdException {
BsonValue _referenceValue
= getReferenceFieldValue(referenceField, data);
String db = (targetDb == null ? dbName : targetDb);
// check _referenceValue
if (role == ROLE.OWNING) {
if (_referenceValue == null) {
return null; // the reference field is missing or it value is null => do not generate a link
}
if (type == TYPE.ONE_TO_ONE || type == TYPE.MANY_TO_ONE) {
BsonValue id = _referenceValue;
// can be an array if ref-field is a json path expression
if (id.isArray() && id.asArray().size() == 1) {
id = id.asArray().get(0);
}
return URLUtils.getUriWithDocId(context, db, targetCollection, id);
} else {
if (!_referenceValue.isArray()) {
throw new IllegalArgumentException(
"in resource "
+ dbName
+ "/"
+ collName
+ "/"
+ data.get("_id")
+ " the "
+ type.name()
+ " relationship ref-field "
+ this.referenceField
+ " should be an array, but it is "
+ _referenceValue);
}
BsonValue[] ids = _referenceValue
.asArray()
.getValues()
.toArray(new BsonValue[0]);
return URLUtils.getUriWithFilterMany(context, db, targetCollection, ids);
}
} else {
// INVERSE
BsonValue id = data.get("_id");
if (type == TYPE.ONE_TO_ONE || type == TYPE.ONE_TO_MANY) {
return URLUtils.getUriWithFilterOne(
context,
db,
targetCollection,
referenceField,
id);
} else if (type == TYPE.MANY_TO_ONE || type == TYPE.MANY_TO_MANY) {
return URLUtils.getUriWithFilterManyInverse(
context,
db,
targetCollection,
referenceField,
id);
}
}
LOGGER.debug("returned null link. this = {}, data = {}", this, data);
return null;
}
/**
*
* @returns the reference field value, either it is an object or, in case
* referenceField is a json path, a BsonDocument
*
*
*/
private BsonValue getReferenceFieldValue(
String referenceField,
BsonDocument data) {
if (referenceField.startsWith("$.")) {
// it is a json path expression
List<Optional<BsonValue>> objs;
try {
objs = JsonUtils.getPropsFromPath(data, referenceField);
} catch (IllegalArgumentException ex) {
return null;
}
if (objs == null) {
return null;
}
BsonArray ret = new BsonArray();
objs.stream().forEach((Optional<BsonValue> obj) -> {
if (obj != null && obj.isPresent()) {
ret.add(obj.get());
} else {
LOGGER.trace(
"the reference field {} resolved to {} from {}",
referenceField,
objs,
data);
}
});
if (ret.isEmpty()) {
return null;
} else {
return ret;
}
} else {
return data.get(referenceField);
}
}
/**
* @return the rel
*/
public String getRel() {
return rel;
}
/**
* @return the type
*/
public TYPE getType() {
return type;
}
/**
* @return the role
*/
public ROLE getRole() {
return role;
}
/**
* @return the targetDb
*/
public String getTargetDb() {
return targetDb;
}
/**
* @return the targetCollection
*/
public String getTargetCollection() {
return targetCollection;
}
/**
* @return the referenceField
*/
public String getReferenceField() {
return referenceField;
}
}