Skip to content
This repository was archived by the owner on May 5, 2021. It is now read-only.

Commit 22f7c50

Browse files
author
Thomas Broyer
committed
Address review comments
* rename contentType → mimeType * merge DocumentContext into DocumentRelatedEntityType * document two-phase deletion process * add EntityExistsException message * add pseudonimization * fix file deletion upon transaction rollback * rename getDeletedDocuments → getDocumentsMarkedForDeletion * create storage dir during server-setup.sh Change-Id: Ic2fc9a0497aa372340b2dc6ebadfe5320aaf132c
1 parent fb0cc72 commit 22f7c50

18 files changed

Lines changed: 151 additions & 133 deletions

File tree

sormas-api/src/main/java/de/symeda/sormas/api/document/DocumentContext.java

Lines changed: 0 additions & 40 deletions
This file was deleted.

sormas-api/src/main/java/de/symeda/sormas/api/document/DocumentDto.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414
*/
1515
package de.symeda.sormas.api.document;
1616

17-
import de.symeda.sormas.api.EntityDto;
1817
import de.symeda.sormas.api.user.UserReferenceDto;
1918
import de.symeda.sormas.api.utils.DataHelper;
2019
import de.symeda.sormas.api.utils.Required;
20+
import de.symeda.sormas.api.utils.pseudonymization.PseudonymizableDto;
2121

22-
public class DocumentDto extends EntityDto {
22+
public class DocumentDto extends PseudonymizableDto {
2323

2424
public static final String UPLOADING_USER = "uploadingUser";
2525
public static final String NAME = "name";
@@ -33,7 +33,7 @@ public class DocumentDto extends EntityDto {
3333
@Required
3434
private String name;
3535
@Required
36-
private String contentType;
36+
private String mimeType;
3737
@Required
3838
private long size;
3939
@Required
@@ -64,12 +64,12 @@ public void setName(String name) {
6464
this.name = name;
6565
}
6666

67-
public String getContentType() {
68-
return contentType;
67+
public String getMimeType() {
68+
return mimeType;
6969
}
7070

71-
public void setContentType(String contentType) {
72-
this.contentType = contentType;
71+
public void setMimeType(String mimeType) {
72+
this.mimeType = mimeType;
7373
}
7474

7575
public long getSize() {

sormas-api/src/main/java/de/symeda/sormas/api/document/DocumentRelatedEntityType.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@
1414
*/
1515
package de.symeda.sormas.api.document;
1616

17+
import de.symeda.sormas.api.i18n.I18nProperties;
18+
1719
public enum DocumentRelatedEntityType {
18-
EVENT
20+
21+
EVENT;
22+
23+
public String toString() {
24+
return I18nProperties.getEnumCaption(this);
25+
}
1926
}

sormas-api/src/main/resources/enum.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,8 @@ Disease.Short.ENTEROVIRUS = Enterovirus
371371
Disease.Short.M_PNEUMONIAE = M.pneumoniae
372372
Disease.Short.C_PNEUMONIAE = C.pneumoniae
373373

374-
# DocumentContext
375-
DocumentContext.EVENT = Event
374+
# DocumentRelatedEntityType
375+
DocumentRelatedEntityType.EVENT = Event
376376

377377
EducationType.NONE = No education
378378
EducationType.NURSERY = Nursery

sormas-api/src/main/resources/enum_fr-FR.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,8 @@ Disease.Short.ENTEROVIRUS = Enterovirus
371371
Disease.Short.M_PNEUMONIAE = M.pneumoniae
372372
Disease.Short.C_PNEUMONIAE = C.pneumoniae
373373

374-
# DocumentContext
375-
DocumentContext.EVENT = Événement
374+
# DocumentRelatedEntityType
375+
DocumentRelatedEntityType.EVENT = Événement
376376

377377
EducationType.NONE = Pas d'éducation
378378
EducationType.NURSERY = Maternelle

sormas-backend/src/main/java/de/symeda/sormas/backend/document/Document.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import de.symeda.sormas.api.document.DocumentRelatedEntityType;
2727
import de.symeda.sormas.backend.common.AbstractDomainObject;
28+
import de.symeda.sormas.backend.common.CronService;
2829
import de.symeda.sormas.backend.user.User;
2930

3031
@Entity(name = TABLE_NAME)
@@ -43,12 +44,32 @@ public class Document extends AbstractDomainObject {
4344
private boolean deleted;
4445
private User uploadingUser;
4546
private String name;
46-
private String contentType;
47+
private String mimeType;
4748
private long size;
4849
private String storageReference;
4950
private String relatedEntityUuid;
5051
private DocumentRelatedEntityType relatedEntityType;
5152

53+
/**
54+
* Indicates whether the document is marked for deletion.
55+
*
56+
* <p>
57+
* Documents aren't directly deleted. They're instead marked for deletion,
58+
* and a nightly process actually deletes both the files and the document
59+
* metadata (this entity).
60+
* <p>
61+
* This allows for non-atomic backups:
62+
* <ol>
63+
* <li>First backup the database, this will include "marked as deleted" documents;
64+
* <li>Then backup the file storage, it might include files for documents added
65+
* since the database backup, and files for documents marked as deleted might
66+
* be <i>missing</i> as they could have already been cleaned up.
67+
* </ol>
68+
* <p>
69+
* Once a document is marked for deletion, it will never be restored.
70+
*
71+
* @see CronService#cleanupDeletedDocuments()
72+
*/
5273
public boolean isDeleted() {
5374
return deleted;
5475
}
@@ -75,13 +96,13 @@ public void setName(String name) {
7596
this.name = name;
7697
}
7798

78-
@Column(name = "contenttype")
79-
public String getContentType() {
80-
return contentType;
99+
@Column(name = "mimetype")
100+
public String getMimeType() {
101+
return mimeType;
81102
}
82103

83-
public void setContentType(String contentType) {
84-
this.contentType = contentType;
104+
public void setMimeType(String mimeType) {
105+
this.mimeType = mimeType;
85106
}
86107

87108
public long getSize() {

sormas-backend/src/main/java/de/symeda/sormas/backend/document/DocumentFacadeEjb.java

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,35 @@
2222
import javax.ejb.EJB;
2323
import javax.ejb.LocalBean;
2424
import javax.ejb.Stateless;
25-
import javax.enterprise.event.Event;
26-
import javax.inject.Inject;
2725
import javax.persistence.EntityExistsException;
2826
import javax.persistence.EntityManager;
2927
import javax.persistence.PersistenceContext;
3028

3129
import de.symeda.sormas.api.document.DocumentDto;
3230
import de.symeda.sormas.api.document.DocumentFacade;
3331
import de.symeda.sormas.api.document.DocumentRelatedEntityType;
32+
import de.symeda.sormas.backend.event.Event;
33+
import de.symeda.sormas.backend.event.EventJurisdictionChecker;
34+
import de.symeda.sormas.backend.event.EventService;
3435
import de.symeda.sormas.backend.user.UserFacadeEjb;
3536
import de.symeda.sormas.backend.user.UserService;
3637
import de.symeda.sormas.backend.util.DtoHelper;
3738
import de.symeda.sormas.backend.util.ModelConstants;
38-
39+
import de.symeda.sormas.backend.util.Pseudonymizer;
40+
41+
/**
42+
* Manages documents and their storage.
43+
*
44+
* <p>
45+
* Storage of the document content itself is delegated to {@link DocumentStorageService},
46+
* which generates a <i>storage reference</i> that is stored alongside document metadata in the
47+
* database ({@link Document} entity).
48+
* <p>
49+
* Deletion is a two-phase process (see {@link Document#isDeleted()} for details).
50+
*
51+
* @see DocumentStorageService
52+
* @see Document#isDeleted()
53+
*/
3954
@Stateless(name = "DocumentFacade")
4055
public class DocumentFacadeEjb implements DocumentFacade {
4156

@@ -49,32 +64,41 @@ public class DocumentFacadeEjb implements DocumentFacade {
4964
@EJB
5065
private DocumentStorageService documentStorageService;
5166

52-
@Inject
53-
private Event<DocumentSaved> documentSavedEvent;
67+
@EJB
68+
private EventService eventService;
69+
@EJB
70+
private EventJurisdictionChecker eventJurisdictionChecker;
5471

5572
@Override
5673
public DocumentDto getDocumentByUuid(String uuid) {
57-
return toDto(documentService.getByUuid(uuid));
74+
return convertToDto(documentService.getByUuid(uuid), Pseudonymizer.getDefault(userService::hasRight));
5875
}
5976

6077
@Override
6178
public DocumentDto saveDocument(DocumentDto dto, byte[] content) throws IOException {
6279
Document existingDocument = dto.getUuid() == null ? null : documentService.getByUuid(dto.getUuid());
6380
if (existingDocument != null) {
64-
// TODO: add exception message
65-
throw new EntityExistsException();
81+
throw new EntityExistsException("Tried to save a document that already exists: " + dto.getUuid());
6682
}
6783

6884
Document document = fromDto(dto);
6985

70-
document.setStorageReference(documentStorageService.save(document, content));
71-
72-
documentService.persist(document);
73-
documentService.doFlush();
86+
String storageReference = documentStorageService.save(document, content);
87+
try {
88+
document.setStorageReference(storageReference);
7489

75-
documentSavedEvent.fire(new DocumentSaved(document));
90+
documentService.persist(document);
91+
documentService.doFlush();
7692

77-
return toDto(document);
93+
return convertToDto(document, Pseudonymizer.getDefault(userService::hasRight));
94+
} catch (Throwable t) {
95+
try {
96+
documentStorageService.delete(storageReference);
97+
} catch (Throwable t2) {
98+
t.addSuppressed(t2);
99+
}
100+
throw t;
101+
}
78102
}
79103

80104
@Override
@@ -85,7 +109,8 @@ public void deleteDocument(String uuid) {
85109

86110
@Override
87111
public List<DocumentDto> getDocumentsRelatedToEntity(DocumentRelatedEntityType type, String uuid) {
88-
return documentService.getRelatedToEntity(type, uuid).stream().map(DocumentFacadeEjb::toDto).collect(Collectors.toList());
112+
Pseudonymizer pseudonymizer = Pseudonymizer.getDefault(userService::hasRight);
113+
return documentService.getRelatedToEntity(type, uuid).stream().map(d -> convertToDto(d, pseudonymizer)).collect(Collectors.toList());
89114
}
90115

91116
@Override
@@ -101,7 +126,7 @@ public byte[] read(String uuid) throws IOException {
101126

102127
@Override
103128
public void cleanupDeletedDocuments() {
104-
List<Document> deleted = documentService.getDeletedDocuments();
129+
List<Document> deleted = documentService.getDocumentsMarkedForDeletion();
105130
for (Document document : deleted) {
106131
documentStorageService.delete(document.getStorageReference());
107132
documentService.delete(document);
@@ -121,15 +146,43 @@ public Document fromDto(DocumentDto source) {
121146

122147
target.setUploadingUser(userService.getByReferenceDto(source.getUploadingUser()));
123148
target.setName(source.getName());
124-
target.setContentType(source.getContentType());
149+
target.setMimeType(source.getMimeType());
125150
target.setSize(source.getSize());
126151
target.setRelatedEntityUuid(source.getRelatedEntityUuid());
127152
target.setRelatedEntityType(source.getRelatedEntityType());
128153

129154
return target;
130155
}
131156

132-
// XXX: pseudonimize uploadingUser? Under which conditions?
157+
public DocumentDto convertToDto(Document source, Pseudonymizer pseudonymizer) {
158+
DocumentDto documentDto = toDto(source);
159+
160+
pseudonymizeDto(source, documentDto, pseudonymizer);
161+
162+
return documentDto;
163+
}
164+
165+
private void pseudonymizeDto(Document document, DocumentDto dto, Pseudonymizer pseudonymizer) {
166+
if (dto != null) {
167+
switch (dto.getRelatedEntityType()) {
168+
case EVENT:
169+
pseudonimizeEventRelatedDto(document, dto, pseudonymizer);
170+
break;
171+
}
172+
}
173+
}
174+
175+
private void pseudonimizeEventRelatedDto(Document document, DocumentDto dto, Pseudonymizer pseudonymizer) {
176+
assert dto.getRelatedEntityType() == DocumentRelatedEntityType.EVENT;
177+
178+
Event event = eventService.getByUuid(dto.getRelatedEntityUuid());
179+
boolean inJurisdiction = eventJurisdictionChecker.isInJurisdictionOrOwned(event);
180+
181+
pseudonymizer.pseudonymizeDto(DocumentDto.class, dto, inJurisdiction, (e) -> {
182+
pseudonymizer.pseudonymizeUser(document.getUploadingUser(), userService.getCurrentUser(), dto::setUploadingUser);
183+
});
184+
}
185+
133186
public static DocumentDto toDto(Document source) {
134187
if (source == null) {
135188
return null;
@@ -139,7 +192,7 @@ public static DocumentDto toDto(Document source) {
139192

140193
target.setUploadingUser(UserFacadeEjb.toReferenceDto(source.getUploadingUser()));
141194
target.setName(source.getName());
142-
target.setContentType(source.getContentType());
195+
target.setMimeType(source.getMimeType());
143196
target.setSize(source.getSize());
144197
target.setRelatedEntityUuid(source.getRelatedEntityUuid());
145198
target.setRelatedEntityType(source.getRelatedEntityType());

sormas-backend/src/main/java/de/symeda/sormas/backend/document/DocumentSaved.java

Lines changed: 0 additions & 28 deletions
This file was deleted.

sormas-backend/src/main/java/de/symeda/sormas/backend/document/DocumentService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public String isExisting(DocumentRelatedEntityType type, String uuid, String nam
8888
}
8989
}
9090

91-
public List<Document> getDeletedDocuments() {
91+
public List<Document> getDocumentsMarkedForDeletion() {
9292
CriteriaBuilder cb = em.getCriteriaBuilder();
9393
CriteriaQuery<Document> cq = cb.createQuery(getElementClass());
9494
Root<Document> from = cq.from(getElementClass());

0 commit comments

Comments
 (0)