Skip to content

Commit 105a956

Browse files
authored
Merge pull request #39 from Systems-Modeling/feature/ST5AS-107
ST5AS-107 Implement cursor pagination in getElementsByProjectCommit endpoint
2 parents 64aaef3 + 0536d04 commit 105a956

7 files changed

Lines changed: 150 additions & 38 deletions

File tree

app/controllers/ElementController.java

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
package controllers;
2323

2424
import com.fasterxml.jackson.databind.JsonNode;
25+
import com.google.common.primitives.Bytes;
2526
import config.MetamodelProvider;
2627
import jackson.JacksonHelper;
2728
import jackson.JsonLdMofObjectAdornment;
@@ -36,10 +37,8 @@
3637
import services.ElementService;
3738

3839
import javax.inject.Inject;
39-
import java.util.List;
40-
import java.util.Optional;
41-
import java.util.Set;
42-
import java.util.UUID;
40+
import java.time.Instant;
41+
import java.util.*;
4342
import java.util.stream.Collectors;
4443

4544
import static jackson.JsonLdMofObjectAdornment.JSONLD_MIME_TYPE;
@@ -83,22 +82,70 @@ public Result create(Http.Request request) {
8382
}
8483

8584
public Result getElementsByProjectIdCommitId(UUID projectId, UUID commitId, Http.Request request) {
86-
Set<Element> elements = elementService.getElementsByProjectIdCommitId(projectId, commitId);
85+
UUID pageAfter = Optional.ofNullable(request.getQueryString("page[after]"))
86+
.map(ElementController::fromCursor)
87+
//.map(UUID::fromString)
88+
.orElse(null);
89+
UUID pageBefore = Optional.ofNullable(request.getQueryString("page[before]"))
90+
.map(ElementController::fromCursor)
91+
//.map(UUID::fromString)
92+
.orElse(null);
93+
int pageSize = Optional.ofNullable(request.getQueryString("page[size]"))
94+
.map(Integer::parseInt)
95+
.orElse(100);
96+
if (pageSize <= 0) {
97+
return Results.badRequest("Page size must be greater than zero.");
98+
}
99+
100+
List<Element> elements = elementService.getElementsByProjectIdCommitId(projectId, commitId, pageAfter, pageBefore, pageSize);
87101
boolean respondWithJsonLd = respondWithJsonLd(request);
102+
final String linkHeaderValue;
103+
if (elements.size() > 0) {
104+
boolean pageFull = elements.size() == pageSize;
105+
boolean hasNext = pageFull || pageBefore != null;
106+
boolean hasPrev = pageFull && pageBefore != null || pageAfter != null;
107+
// hasPrev -> !pageFull || pageAfter != null
108+
StringBuilder linkHeaderValueBuilder = new StringBuilder();
109+
if (hasNext) {
110+
linkHeaderValueBuilder.append(String.format("<http://%s/projects/%s/commits/%s/elements?page[after]=%s&page[size]=%s>; rel=\"next\"",
111+
request.host(),
112+
projectId,
113+
commitId,
114+
toCursor(elements.get(elements.size() - 1).getIdentifier()),
115+
pageSize));
116+
if (hasPrev) {
117+
linkHeaderValueBuilder.append(", ");
118+
}
119+
}
120+
if (hasPrev) {
121+
linkHeaderValueBuilder.append(String.format("<http://%s/projects/%s/commits/%s/elements?page[before]=%s&page[size]=%s>; rel=\"prev\"",
122+
request.host(),
123+
projectId,
124+
commitId,
125+
toCursor(elements.get(0).getIdentifier()),
126+
pageSize));
127+
}
128+
linkHeaderValue = linkHeaderValueBuilder.toString();
129+
}
130+
else {
131+
linkHeaderValue = null;
132+
}
133+
88134
return Optional.of(
89135
elements.stream()
90136
.map(e -> respondWithJsonLd ?
91137
adornMofObject(e, request, metamodelProvider, environment, projectId, commitId) :
92138
e
93139
)
94-
.collect(Collectors.toSet())
140+
.collect(Collectors.toList())
95141
)
96-
.map(set -> JacksonHelper.collectionToTree(set, Set.class, respondWithJsonLd ?
142+
.map(collection -> JacksonHelper.collectionToTree(collection, List.class, respondWithJsonLd ?
97143
JsonLdMofObjectAdornment.class :
98144
metamodelProvider.getImplementationClass(Element.class))
99145
)
100146
.map(Results::ok)
101147
.map(result -> respondWithJsonLd ? result.as(JSONLD_MIME_TYPE) : result)
148+
.map(result -> linkHeaderValue != null ? result.withHeader("Link", linkHeaderValue) : result)
102149
.orElseThrow();
103150
}
104151

@@ -123,7 +170,7 @@ static JsonLdMofObjectAdornment adornMofObject(MofObject mof, Http.Request reque
123170
}
124171

125172
public Result getRootsByProjectIdCommitId(UUID projectId, UUID commitId, Http.Request request) {
126-
Set<Element> roots = elementService.getRootsByProjectIdCommitId(projectId, commitId);
173+
List<Element> roots = elementService.getRootsByProjectIdCommitId(projectId, commitId);
127174
boolean respondWithJsonLd = respondWithJsonLd(request);
128175
return ok(JacksonHelper.collectionToTree(roots.stream()
129176
.map(e -> respondWithJsonLd ? adornMofObject(e, request, metamodelProvider, environment, projectId, commitId) : e)
@@ -135,4 +182,24 @@ public Result getRootsByProjectIdCommitId(UUID projectId, UUID commitId, Http.Re
135182
static boolean respondWithJsonLd(Http.Request request) {
136183
return request.accepts(JSONLD_MIME_TYPE);
137184
}
185+
186+
protected static char CURSOR_SEPARATOR = '|';
187+
188+
protected static UUID fromCursor(String cursor) throws IllegalArgumentException {
189+
byte[] decoded = Base64.getUrlDecoder().decode(cursor);
190+
int separatorIndex = Bytes.indexOf(decoded, (byte) CURSOR_SEPARATOR);
191+
if (separatorIndex < 0) {
192+
throw new IllegalArgumentException("Cursor separator missing");
193+
}
194+
return UUID.fromString(
195+
new String(decoded, separatorIndex + 1, decoded.length - separatorIndex - 1)
196+
);
197+
}
198+
199+
protected static String toCursor(UUID id) throws IllegalArgumentException {
200+
String unencoded = String.valueOf(Instant.now().toEpochMilli()) +
201+
CURSOR_SEPARATOR +
202+
id.toString();
203+
return Base64.getUrlEncoder().withoutPadding().encodeToString(unencoded.getBytes());
204+
}
138205
}

app/controllers/QueryController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public Result getQueryResultsByProjectIdQuery(UUID projectId, @SuppressWarnings(
106106
}
107107

108108
private Result buildResponse(QueryService.QueryResults result, UUID projectId, Http.Request request) {
109-
Set<Element> elements = result.getElements();
109+
List<Element> elements = result.getElements();
110110
AllowedPropertyFilter filter = result.getPropertyFilter();
111111
boolean respondWithJsonLd = ElementController.respondWithJsonLd(request);
112112
JsonNode json = JacksonHelper.collectionToTree(elements.stream()

app/dao/ElementDao.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@
2525
import org.omg.sysml.metamodel.Element;
2626
import org.omg.sysml.query.Query;
2727

28+
import java.util.List;
2829
import java.util.Optional;
29-
import java.util.Set;
3030
import java.util.UUID;
3131

3232
public interface ElementDao extends Dao<Element> {
3333

34-
Set<Element> findAllByCommit(Commit commit);
34+
List<Element> findAllByCommit(Commit commit, UUID after, UUID before, int maxResults);
3535

3636
Optional<Element> findByCommitAndId(Commit commit, UUID id);
3737

38-
Set<Element> findRootsByCommit(Commit commit);
38+
List<Element> findRootsByCommit(Commit commit);
3939

40-
Set<Element> findByCommitAndQuery(Commit commit, Query query);
40+
List<Element> findByCommitAndQuery(Commit commit, Query query);
4141
}

app/dao/impl/jpa/JpaElementDao.java

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import javax.persistence.EntityManager;
4848
import javax.persistence.EntityTransaction;
4949
import javax.persistence.NoResultException;
50+
import javax.persistence.TypedQuery;
5051
import javax.persistence.criteria.*;
5152
import java.beans.PropertyDescriptor;
5253
import java.util.*;
@@ -101,7 +102,9 @@ public List<Element> findAll() {
101102
CriteriaQuery<MofObjectImpl> query = builder.createQuery(MofObjectImpl.class);
102103
Root<MofObjectImpl> root = query.from(MofObjectImpl.class);
103104
query.select(root).where(getTypeExpression(builder, root));
104-
return em.createQuery(query).getResultStream().map(o -> (Element) o).collect(Collectors.toList());
105+
return em.createQuery(query).getResultStream()
106+
.map(o -> (Element) o)
107+
.collect(Collectors.toList());
105108
});
106109
}
107110

@@ -112,21 +115,50 @@ public void deleteAll() {
112115
CriteriaDelete<MofObjectImpl> query = builder.createCriteriaDelete(MofObjectImpl.class);
113116
Root<MofObjectImpl> root = query.from(MofObjectImpl.class);
114117
query.where(getTypeExpression(builder, root));
115-
return ((Stream<?>) em.createQuery(query).getResultStream()).map(o -> (Element) o).collect(Collectors.toList());
118+
return ((Stream<?>) em.createQuery(query).getResultStream())
119+
.map(o -> (Element) o)
120+
.collect(Collectors.toList());
116121
});
117122
}
118123

119-
@Override
120-
public Set<Element> findAllByCommit(Commit commit) {
124+
public List<Element> findAllByCommit(Commit commit, UUID after, UUID before, int maxResults) {
121125
return jpaManager.transact(em -> {
122126
// TODO Commit is detached at this point. This ternary mitigates by requerying for the Commit in this transaction. A better solution would be moving transaction handling up to service layer (supported by general wisdom) and optionally migrating to using Play's @Transactional/JPAApi. Pros would include removal of repetitive transaction handling at the DAO layer and ability to interface with multiple DAOs in the same transaction (consistent view). Cons include increased temptation to keep transaction open for longer than needed, e.g. during JSON serialization due to the convenience of @Transactional (deprecated in >= 2.8.x), and the service, a higher level of abstraction, becoming aware of transactions. An alternative would be DAO-to-DAO calls (generally discouraged) and delegating to non-transactional versions of methods.
123127
Commit c = em.contains(commit) ? commit : em.find(metamodelProvider.getImplementationClass(Commit.class), commit.getId());
124-
return getCommitIndex(c, em).getWorkingElementVersions().stream()
128+
CommitIndex commitIndex = getCommitIndex(c, em);
129+
130+
CriteriaBuilder builder = em.getCriteriaBuilder();
131+
CriteriaQuery<ElementVersionImpl> query = builder.createQuery(ElementVersionImpl.class);
132+
Root<CommitIndexImpl> commitIndexRoot = query.from(CommitIndexImpl.class);
133+
SetJoin<CommitIndexImpl, ElementVersionImpl> workingElementVersionsJoin = commitIndexRoot.join(CommitIndexImpl_.workingElementVersions);
134+
Join<ElementVersionImpl, ElementIdentityImpl> elementIdentityJoin = workingElementVersionsJoin.join(ElementVersionImpl_.identity);
135+
query.select(workingElementVersionsJoin);
136+
Expression<Boolean> whereExpression = builder.equal(commitIndexRoot.get(CommitIndexImpl_.id), commitIndex.getId());
137+
if (after != null) {
138+
whereExpression = builder.and(whereExpression, builder.greaterThan(elementIdentityJoin.get(ElementIdentityImpl_.id), after));
139+
}
140+
if (before != null) {
141+
whereExpression = builder.and(whereExpression, builder.lessThan(elementIdentityJoin.get(ElementIdentityImpl_.id), before));
142+
}
143+
query.where(whereExpression);
144+
boolean flip = after == null && before != null;
145+
Function<Path<UUID>, Order> orderFunction = flip ? builder::desc : builder::asc;
146+
query.orderBy((orderFunction).apply(elementIdentityJoin.get(ElementIdentityImpl_.id)));
147+
TypedQuery<ElementVersionImpl> typedQuery = em.createQuery(query);
148+
if (maxResults >= 0) {
149+
typedQuery.setMaxResults(maxResults);
150+
}
151+
List<Element> result = typedQuery
152+
.getResultStream()
125153
.map(ElementVersion::getData)
126154
.filter(mof -> mof instanceof Element)
127155
.map(mof -> (Element) mof)
128156
.map(PROXY_RESOLVER)
129-
.collect(Collectors.toSet());
157+
.collect(Collectors.toList());
158+
if (flip) {
159+
Collections.reverse(result);
160+
}
161+
return result;
130162
});
131163
}
132164

@@ -159,7 +191,7 @@ public Optional<Element> findByCommitAndId(Commit commit, UUID id) {
159191
}
160192

161193
@Override
162-
public Set<Element> findRootsByCommit(Commit commit) {
194+
public List<Element> findRootsByCommit(Commit commit) {
163195
return jpaManager.transact(em -> {
164196
// TODO Commit is detached at this point. This ternary mitigates by requerying for the Commit in this transaction. A better solution would be moving transaction handling up to service layer (supported by general wisdom) and optionally migrating to using Play's @Transactional/JPAApi. Pros would include removal of repetitive transaction handling at the DAO layer and ability to interface with multiple DAOs in the same transaction (consistent view). Cons include increased temptation to keep transaction open for longer than needed, e.g. during JSON serialization due to the convenience of @Transactional (deprecated in >= 2.8.x), and the service, a higher level of abstraction, becoming aware of transactions. An alternative would be DAO-to-DAO calls (generally discouraged) and delegating to non-transactional versions of methods.
165197
Commit c = em.contains(commit) ? commit : em.find(metamodelProvider.getImplementationClass(Commit.class), commit.getId());
@@ -169,12 +201,12 @@ public Set<Element> findRootsByCommit(Commit commit) {
169201
.map(mof -> (Element) mof)
170202
.filter(element -> element.getOwner() == null)
171203
.map(PROXY_RESOLVER)
172-
.collect(Collectors.toSet());
204+
.collect(Collectors.toList());
173205
});
174206
}
175207

176208
@Override
177-
public Set<Element> findByCommitAndQuery(Commit commit, Query query) {
209+
public List<Element> findByCommitAndQuery(Commit commit, Query query) {
178210
return jpaManager.transact(em -> {
179211
// TODO Commit is detached at this point. This ternary mitigates by requerying for the Commit in this transaction. A better solution would be moving transaction handling up to service layer (supported by general wisdom) and optionally migrating to using Play's @Transactional/JPAApi. Pros would include removal of repetitive transaction handling at the DAO layer and ability to interface with multiple DAOs in the same transaction (consistent view). Cons include increased temptation to keep transaction open for longer than needed, e.g. during JSON serialization due to the convenience of @Transactional (deprecated in >= 2.8.x), and the service, a higher level of abstraction, becoming aware of transactions. An alternative would be DAO-to-DAO calls (generally discouraged) and delegating to non-transactional versions of methods.
180212
Commit c = em.contains(commit) ? commit : em.find(metamodelProvider.getImplementationClass(Commit.class), commit.getId());
@@ -186,7 +218,7 @@ public Set<Element> findByCommitAndQuery(Commit commit, Query query) {
186218
.map(mof -> (Element) mof)
187219
.filter(constrain(q.getWhere()))
188220
.map(PROXY_RESOLVER)
189-
.collect(Collectors.toSet());
221+
.collect(Collectors.toList());
190222
});
191223
}
192224

app/services/ElementService.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030
import javax.inject.Inject;
3131
import javax.inject.Singleton;
3232
import java.util.Collections;
33+
import java.util.List;
3334
import java.util.Optional;
34-
import java.util.Set;
3535
import java.util.UUID;
3636

3737
@Singleton
@@ -53,21 +53,16 @@ public Optional<Element> create(Element element) {
5353
return element.getIdentifier() != null ? dao.update(element) : dao.persist(element);
5454
}
5555

56-
public Set<Element> getByCommitId(UUID commitId) {
57-
return commitDao.findById(commitId)
58-
.map(dao::findAllByCommit).orElse(Collections.emptySet());
59-
}
60-
6156
public Optional<Element> getByCommitIdAndId(UUID commitId, UUID elementId) {
6257
return commitDao.findById(commitId)
6358
.flatMap(m -> dao.findByCommitAndId(m, elementId));
6459
}
6560

66-
public Set<Element> getElementsByProjectIdCommitId(UUID projectId, UUID commitId) {
61+
public List<Element> getElementsByProjectIdCommitId(UUID projectId, UUID commitId, UUID after, UUID before, int maxResults) {
6762
return projectDao.findById(projectId)
6863
.flatMap(project -> commitDao.findByProjectAndId(project, commitId))
69-
.map(dao::findAllByCommit)
70-
.orElse(Collections.emptySet());
64+
.map(commit -> dao.findAllByCommit(commit, after, before, maxResults))
65+
.orElse(Collections.emptyList());
7166
}
7267

7368
public Optional<Element> getElementsByProjectIdCommitIdElementId(UUID projectId, UUID commitId, UUID elementId) {
@@ -76,10 +71,10 @@ public Optional<Element> getElementsByProjectIdCommitIdElementId(UUID projectId,
7671
.flatMap(commit -> dao.findByCommitAndId(commit, elementId));
7772
}
7873

79-
public Set<Element> getRootsByProjectIdCommitId(UUID projectId, UUID commitId) {
74+
public List<Element> getRootsByProjectIdCommitId(UUID projectId, UUID commitId) {
8075
return projectDao.findById(projectId)
8176
.flatMap(project -> commitDao.findByProjectAndId(project, commitId))
8277
.map(dao::findRootsByCommit)
83-
.orElse(Collections.emptySet());
78+
.orElse(Collections.emptyList());
8479
}
8580
}

app/services/QueryService.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
import javax.annotation.Nullable;
3535
import javax.inject.Inject;
3636
import javax.inject.Singleton;
37-
import java.util.*;
37+
import java.util.Collections;
38+
import java.util.List;
39+
import java.util.Optional;
40+
import java.util.UUID;
3841
import java.util.function.Function;
3942

4043
@Singleton
@@ -90,17 +93,17 @@ private QueryResults getQueryResults(UUID projectId, Function<Project, Query> qu
9093
}
9194

9295
public static class QueryResults {
93-
private final Set<Element> elements;
96+
private final List<Element> elements;
9497
private final Commit commit;
9598
private final AllowedPropertyFilter propertyFilter;
9699

97-
public QueryResults(Set<Element> elements, Commit commit, AllowedPropertyFilter propertyFilter) {
100+
public QueryResults(List<Element> elements, Commit commit, AllowedPropertyFilter propertyFilter) {
98101
this.elements = elements;
99102
this.commit = commit;
100103
this.propertyFilter = propertyFilter;
101104
}
102105

103-
public Set<Element> getElements() {
106+
public List<Element> getElements() {
104107
return elements;
105108
}
106109

public/swagger/openapi.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,21 @@ paths:
217217
type: string
218218
format: uuid
219219
required: true
220+
- name: page[after]
221+
in: query
222+
description: Page after
223+
type: string
224+
required: false
225+
- name: page[before]
226+
in: query
227+
description: Page before
228+
type: string
229+
required: false
230+
- name: page[size]
231+
in: query
232+
description: Page size
233+
type: integer
234+
required: false
220235
get:
221236
tags:
222237
- Element

0 commit comments

Comments
 (0)