Skip to content

Commit 1a82392

Browse files
authored
O3-5396: Implement additional duplication safety (#91)
1 parent fd3edf0 commit 1a82392

File tree

20 files changed

+714
-247
lines changed

20 files changed

+714
-247
lines changed

api/src/main/java/org/openmrs/module/queue/api/QueueEntryService.java

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
import javax.validation.constraints.NotNull;
1313

14+
import java.util.Date;
1415
import java.util.List;
1516
import java.util.Optional;
1617

1718
import org.openmrs.Location;
19+
import org.openmrs.Patient;
1820
import org.openmrs.Visit;
1921
import org.openmrs.VisitAttributeType;
2022
import org.openmrs.annotation.Authorized;
@@ -46,6 +48,31 @@ public interface QueueEntryService {
4648
@Authorized({ PrivilegeConstants.GET_QUEUE_ENTRIES })
4749
Optional<QueueEntry> getQueueEntryById(@NotNull Integer id);
4850

51+
/**
52+
* Fetches the set of queue entries for this patient and queue that overlap with the give startedAt
53+
* and endedAt dates
54+
*
55+
* @param patient The patient this queue entry belongs to
56+
* @param queue The queue to query
57+
* @param startedAt The start point for this queue entry
58+
* @param endedAt The endpoint for this queue entry
59+
* @return A list of overlapping queue entries
60+
*/
61+
@Authorized(PrivilegeConstants.GET_QUEUE_ENTRIES)
62+
List<QueueEntry> getOverlappingQueueEntries(Patient patient, Queue queue, Date startedAt, Date endedAt);
63+
64+
/**
65+
* Given a specified queue entry Q, return its previous queue entry P, where P has same patient and
66+
* visit as Q, and P.endedAt time is same as Q.startedAt time, and P.queue is same as
67+
* Q.queueComingFrom
68+
*
69+
* @param queueEntry
70+
* @return the previous queue entry, null otherwise.
71+
* @throws IllegalStateException if multiple previous queue entries are identified
72+
*/
73+
@Authorized(PrivilegeConstants.GET_QUEUE_ENTRIES)
74+
QueueEntry getPreviousQueueEntry(@NotNull QueueEntry queueEntry);
75+
4976
/**
5077
* Saves a queue entry
5178
*
@@ -66,7 +93,7 @@ public interface QueueEntryService {
6693
QueueEntry transitionQueueEntry(@NotNull QueueEntryTransition queueEntryTransition);
6794

6895
/**
69-
* Undos a transition to the input queue entry by voiding it and making its previous queue entry
96+
* Undoes a transition to the input queue entry by voiding it and making its previous queue entry
7097
* active by setting the previous entry's end time to null.
7198
*
7299
* @see QueueEntryService#getPreviousQueueEntry(QueueEntry)
@@ -110,18 +137,19 @@ public interface QueueEntryService {
110137
Long getCountOfQueueEntries(@NotNull QueueEntrySearchCriteria searchCriteria);
111138

112139
/**
113-
* @param location
114-
* @param queue
140+
* @param location The location associated with the queue
141+
* @param queue The queue
115142
* @return VisitQueueNumber - used to identify patients in the queue instead of using patient name
116143
*/
117-
@Authorized({ org.openmrs.util.PrivilegeConstants.ADD_VISITS, org.openmrs.util.PrivilegeConstants.EDIT_VISITS })
144+
@Authorized({ PrivilegeConstants.MANAGE_QUEUE_ENTRIES, org.openmrs.util.PrivilegeConstants.ADD_VISITS,
145+
org.openmrs.util.PrivilegeConstants.EDIT_VISITS })
118146
String generateVisitQueueNumber(@NotNull Location location, @NotNull Queue queue, @NotNull Visit visit,
119147
@NotNull VisitAttributeType visitAttributeType);
120148

121149
/**
122150
* Closes all active queue entries
123151
*/
124-
@Authorized({ PrivilegeConstants.MANAGE_QUEUE_ENTRIES })
152+
@Authorized(PrivilegeConstants.MANAGE_QUEUE_ENTRIES)
125153
void closeActiveQueueEntries();
126154

127155
/**
@@ -138,16 +166,4 @@ String generateVisitQueueNumber(@NotNull Location location, @NotNull Queue queue
138166
* @param sortWeightGenerator the SortWeightGenerator to set
139167
*/
140168
void setSortWeightGenerator(SortWeightGenerator sortWeightGenerator);
141-
142-
/**
143-
* Given a specified queue entry Q, return its previous queue entry P, where P has same patient and
144-
* visit as Q, and P.endedAt time is same as Q.startedAt time, and P.queue is same as
145-
* Q.queueComingFrom
146-
*
147-
* @param queueEntry
148-
* @return the previous queue entry, null otherwise.
149-
* @throws IllegalStateException if multiple previous queue entries are identified
150-
*/
151-
@Authorized({ PrivilegeConstants.GET_QUEUE_ENTRIES })
152-
QueueEntry getPreviousQueueEntry(@NotNull QueueEntry queueEntry);
153169
}

api/src/main/java/org/openmrs/module/queue/api/QueueServicesWrapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import org.springframework.beans.factory.annotation.Qualifier;
3333
import org.springframework.stereotype.Component;
3434

35-
@Component
35+
@Component("queue.QueueServicesWrapper")
3636
@Getter
3737
public class QueueServicesWrapper {
3838

api/src/main/java/org/openmrs/module/queue/api/dao/QueueEntryDao.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,13 @@
1111

1212
import javax.validation.constraints.NotNull;
1313

14+
import java.util.Date;
1415
import java.util.List;
1516

16-
import org.openmrs.Auditable;
17-
import org.openmrs.OpenmrsObject;
1817
import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria;
1918
import org.openmrs.module.queue.model.QueueEntry;
2019

21-
public interface QueueEntryDao<Q extends OpenmrsObject & Auditable> extends BaseQueueDao<Q> {
20+
public interface QueueEntryDao extends BaseQueueDao<QueueEntry> {
2221

2322
/**
2423
* @return {@link List} of queue entries that match the given %{@link QueueEntrySearchCriteria}
@@ -31,4 +30,20 @@ public interface QueueEntryDao<Q extends OpenmrsObject & Auditable> extends Base
3130
*/
3231
Long getCountOfQueueEntries(@NotNull QueueEntrySearchCriteria searchCriteria);
3332

33+
List<QueueEntry> getOverlappingQueueEntries(@NotNull QueueEntrySearchCriteria searchCriteria);
34+
35+
/**
36+
* Flushes the current session to ensure pending changes are persisted to the database.
37+
*/
38+
void flushSession();
39+
40+
/**
41+
* Updates the queue entry only if it hasn't been modified since it was loaded. This provides
42+
* optimistic locking to prevent concurrent modifications.
43+
*
44+
* @param queueEntry the queue entry to update
45+
* @param expectedDateChanged the dateChanged value that was present when the entry was loaded
46+
* @return true if the update succeeded, false if the entry was modified by another transaction
47+
*/
48+
boolean updateIfUnmodified(QueueEntry queueEntry, Date expectedDateChanged);
3449
}

api/src/main/java/org/openmrs/module/queue/api/dao/impl/QueueEntryDaoImpl.java

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,31 @@
99
*/
1010
package org.openmrs.module.queue.api.dao.impl;
1111

12+
import javax.persistence.criteria.CriteriaBuilder;
13+
import javax.persistence.criteria.CriteriaQuery;
14+
import javax.persistence.criteria.Predicate;
15+
import javax.persistence.criteria.Root;
16+
17+
import java.util.ArrayList;
18+
import java.util.Collection;
19+
import java.util.Date;
1220
import java.util.List;
1321

1422
import org.hibernate.Criteria;
23+
import org.hibernate.Session;
1524
import org.hibernate.SessionFactory;
1625
import org.hibernate.criterion.Order;
1726
import org.hibernate.criterion.Projections;
1827
import org.hibernate.criterion.Restrictions;
28+
import org.openmrs.Patient;
1929
import org.openmrs.module.queue.api.dao.QueueEntryDao;
2030
import org.openmrs.module.queue.api.search.QueueEntrySearchCriteria;
31+
import org.openmrs.module.queue.model.Queue;
2132
import org.openmrs.module.queue.model.QueueEntry;
2233
import org.springframework.beans.factory.annotation.Qualifier;
2334

2435
@SuppressWarnings("unchecked")
25-
public class QueueEntryDaoImpl extends AbstractBaseQueueDaoImpl<QueueEntry> implements QueueEntryDao<QueueEntry> {
36+
public class QueueEntryDaoImpl extends AbstractBaseQueueDaoImpl<QueueEntry> implements QueueEntryDao {
2637

2738
public QueueEntryDaoImpl(@Qualifier("sessionFactory") SessionFactory sessionFactory) {
2839
super(sessionFactory);
@@ -45,6 +56,76 @@ public Long getCountOfQueueEntries(QueueEntrySearchCriteria searchCriteria) {
4556
return (Long) criteria.uniqueResult();
4657
}
4758

59+
@Override
60+
public List<QueueEntry> getOverlappingQueueEntries(QueueEntrySearchCriteria searchCriteria) {
61+
Session session = getSessionFactory().getCurrentSession();
62+
CriteriaBuilder cb = session.getCriteriaBuilder();
63+
CriteriaQuery<QueueEntry> query = cb.createQuery(QueueEntry.class);
64+
Root<QueueEntry> root = query.from(QueueEntry.class);
65+
List<Predicate> predicates = new ArrayList<>();
66+
67+
predicates.add(cb.equal(root.get("voided"), false));
68+
69+
Collection<Queue> queues = searchCriteria.getQueues();
70+
if (queues != null) {
71+
if (queues.isEmpty()) {
72+
predicates.add(root.get("queue").isNull());
73+
} else {
74+
predicates.add(root.get("queue").in(searchCriteria.getQueues()));
75+
}
76+
}
77+
78+
Patient patient = searchCriteria.getPatient();
79+
if (patient != null) {
80+
predicates.add(cb.equal(root.get("patient"), patient));
81+
}
82+
83+
Date startedAt = searchCriteria.getStartedOn();
84+
if (startedAt != null) {
85+
// any queue entries that have either not ended or end after this queue entry starts
86+
predicates.add(cb.or(root.get("endedAt").isNull(), cb.greaterThan(root.get("endedAt"), startedAt)));
87+
}
88+
89+
query.where(cb.and(predicates.toArray(new Predicate[0])));
90+
91+
return session.createQuery(query).list();
92+
}
93+
94+
@Override
95+
public void flushSession() {
96+
getSessionFactory().getCurrentSession().flush();
97+
}
98+
99+
@Override
100+
public boolean updateIfUnmodified(QueueEntry queueEntry, Date expectedDateChanged) {
101+
Session session = getSessionFactory().getCurrentSession();
102+
103+
// Evict the entity to prevent Hibernate from auto-flushing changes
104+
session.evict(queueEntry);
105+
106+
// Build conditional update query - only succeeds if dateChanged matches expected value
107+
StringBuilder jpql = new StringBuilder();
108+
jpql.append("UPDATE QueueEntry qe SET ");
109+
jpql.append("qe.endedAt = :endedAt ");
110+
jpql.append("WHERE qe.queueEntryId = :id ");
111+
112+
if (expectedDateChanged == null) {
113+
jpql.append("AND qe.dateChanged IS NULL");
114+
} else {
115+
jpql.append("AND qe.dateChanged = :expectedDateChanged");
116+
}
117+
118+
javax.persistence.Query query = session.createQuery(jpql.toString());
119+
query.setParameter("endedAt", queueEntry.getEndedAt());
120+
query.setParameter("id", queueEntry.getQueueEntryId());
121+
if (expectedDateChanged != null) {
122+
query.setParameter("expectedDateChanged", expectedDateChanged);
123+
}
124+
125+
int rowsUpdated = query.executeUpdate();
126+
return rowsUpdated > 0;
127+
}
128+
48129
/**
49130
* Convert the given {@link QueueEntrySearchCriteria} into ORM criteria
50131
*/

api/src/main/java/org/openmrs/module/queue/api/digitalSignage/QueueTicketAssignments.java

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@
99
*/
1010
package org.openmrs.module.queue.api.digitalSignage;
1111

12-
import java.io.BufferedReader;
13-
import java.io.IOException;
1412
import java.util.HashMap;
1513
import java.util.Map;
1614

15+
import lombok.extern.slf4j.Slf4j;
1716
import org.apache.commons.lang3.StringUtils;
1817

1918
/**
2019
* A utility class for updating details of active queue tickets
2120
*/
21+
@Slf4j
2222
public class QueueTicketAssignments {
2323

2424
/**
@@ -68,27 +68,7 @@ synchronized public static void updateTicketAssignment(String servicePointName,
6868
}
6969

7070
public static Map<String, TicketAssignment> getActiveTicketAssignments() {
71-
return ACTIVE_QUEUE_TICKETS;
72-
}
73-
74-
/**
75-
* Extracts the request body and returns it as a string
76-
*
77-
* @param reader
78-
* @return
79-
*/
80-
public static String fetchRequestBody(BufferedReader reader) {
81-
StringBuilder requestBodyJsonStr = new StringBuilder();
82-
try {
83-
String output;
84-
while ((output = reader.readLine()) != null) {
85-
requestBodyJsonStr.append(output);
86-
}
87-
}
88-
catch (IOException e) {
89-
System.out.println("IOException: " + e.getMessage());
90-
}
91-
return requestBodyJsonStr.toString();
71+
return new HashMap<>(ACTIVE_QUEUE_TICKETS);
9272
}
9373

9474
public static class TicketAssignment {

0 commit comments

Comments
 (0)