-
Notifications
You must be signed in to change notification settings - Fork 71
/
AnalysisCollection.java
176 lines (145 loc) · 6.2 KB
/
AnalysisCollection.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
package com.conveyal.analysis.persistence;
import com.conveyal.analysis.AnalysisServerException;
import com.conveyal.analysis.UserPermissions;
import com.conveyal.analysis.models.BaseModel;
import com.conveyal.analysis.util.JsonUtil;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import org.bson.conversions.Bson;
import org.bson.types.ObjectId;
import spark.Request;
import spark.Response;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.mongodb.client.model.Filters.and;
import static com.mongodb.client.model.Filters.eq;
public class AnalysisCollection<T extends BaseModel> {
public static final String MONGO_PROP_ACCESS_GROUP = "accessGroup";
public final MongoCollection<T> collection;
private final Class<T> type;
private AnalysisServerException invalidAccessGroup() {
return AnalysisServerException.forbidden("Permission denied. Invalid access group.");
}
public AnalysisCollection(MongoCollection<T> collection, Class<T> clazz) {
this.collection = collection;
this.type = clazz;
}
public DeleteResult delete (T value) {
return collection.deleteOne(eq("_id", value._id));
}
public DeleteResult deleteByIdParamIfPermitted (Request request) {
String _id = request.params("_id");
UserPermissions user = UserPermissions.from(request);
return collection.deleteOne(and(eq("_id", new ObjectId(_id)), eq("accessGroup", user.accessGroup)));
}
public List<T> findPermitted(Bson query, UserPermissions userPermissions) {
return find(and(eq(MONGO_PROP_ACCESS_GROUP, userPermissions.accessGroup), query));
}
public List<T> find(Bson query) {
MongoCursor<T> cursor = collection.find(query).cursor();
List<T> found = new ArrayList<>();
while (cursor.hasNext()) {
found.add(cursor.next());
}
return found;
}
public T findById(String _id) {
return findById(new ObjectId(_id));
}
public T findById(ObjectId _id) {
return collection.find(eq("_id", _id)).first();
}
public T findByIdIfPermitted (String _id, UserPermissions userPermissions) {
T item = findById(_id);
if (item.accessGroup.equals(userPermissions.accessGroup)) {
return item;
} else {
// TODO: To simplify stack traces this should be refactored to "throw new InvalidAccessGroupException()"
// which should be a subtype of AnalysisServerException with methods like getHttpCode().
throw invalidAccessGroup();
}
}
public T create(T newModel, UserPermissions userPermissions) {
newModel.accessGroup = userPermissions.accessGroup;
newModel.createdBy = userPermissions.email;
newModel.updatedBy = userPermissions.email;
// This creates the `_id` automatically if it is missing
collection.insertOne(newModel);
return newModel;
}
/**
* Note that if the supplied model has _id = null, the Mongo insertOne method will overwrite it with a new
* ObjectId(). We consider it good practice to set the _id for any model object ourselves, avoiding this behavior.
* It looks like we could remove the OBJECT_ID_GENERATORS convention to force explicit ID creation.
* https://mongodb.github.io/mongo-java-driver/3.11/bson/pojos/#conventions
*/
public void insert (T model) {
collection.insertOne(model);
}
public void insertMany (List<? extends T> models) {
collection.insertMany(models);
}
public T update(T value) {
return update(value, value.accessGroup);
}
public T update(T value, String accessGroup) {
// Store the current nonce for querying and to check later if needed.
ObjectId oldNonce = value.nonce;
value.nonce = new ObjectId();
UpdateResult result = collection.replaceOne(and(
eq("_id", value._id),
eq("nonce", oldNonce),
eq(MONGO_PROP_ACCESS_GROUP, accessGroup)
), value);
// If no documents were modified try to find the document to find out why
if (result.getModifiedCount() != 1) {
T model = findById(value._id);
if (model == null) {
throw AnalysisServerException.notFound(type.getName() + " was not found.");
} else if (model.nonce != oldNonce) {
throw AnalysisServerException.nonce();
} else if (!model.accessGroup.equals(accessGroup)) {
throw invalidAccessGroup();
} else {
throw AnalysisServerException.unknown("Unable to update model.");
}
}
return value;
}
// TODO should all below be static helpers on HttpController? Passing the whole request in seems to defy encapsulation.
// On the other hand, making them instance methods reduces the number of parameters and gives access to Class<T>.
/**
* Controller creation helper.
*/
public T create(Request req, Response res) throws IOException {
T value = JsonUtil.objectMapper.readValue(req.body(), type);
return create(value, UserPermissions.from(req));
}
/**
* Helper for HttpControllers - find a document by the _id path parameter in the request, checking permissions.
*/
public T findPermittedByRequestParamId (Request req) {
UserPermissions user = UserPermissions.from(req);
T value = findById(req.params("_id"));
// Throw if or does not have permission
if (!value.accessGroup.equals(user.accessGroup)) {
throw invalidAccessGroup();
}
return value;
}
/**
* Controller update helper.
*/
public T update(Request req, Response res) throws IOException {
T value = JsonUtil.objectMapper.readValue(req.body(), type);
final UserPermissions user = UserPermissions.from(req);
value.updatedBy = user.email;
if (!value.accessGroup.equals(user.accessGroup)) {
throw invalidAccessGroup();
}
return update(value, user.accessGroup);
}
}