Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Records import #41

Merged
merged 5 commits into from
Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/main/java/cz/cvut/kbss/study/dto/RecordImportResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cz.cvut.kbss.study.dto;

import java.util.ArrayList;
import java.util.List;

/**
* Represents the result of importing records to this instance.
*/
public class RecordImportResult {

/**
* Total number of processed records.
*/
private int totalCount;

/**
* Number of successfully imported records.
*/
private int importedCount;

/**
* Errors that occurred during import.
*/
private List<String> errors;

public RecordImportResult() {
}

public RecordImportResult(int totalCount) {
this.totalCount = totalCount;
}

public int getTotalCount() {
return totalCount;
}

public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}

public int getImportedCount() {
return importedCount;
}

public void setImportedCount(int importedCount) {
this.importedCount = importedCount;
}

public void incrementImportedCount() {
this.importedCount++;
}

public List<String> getErrors() {
return errors;
}

public void setErrors(List<String> errors) {
this.errors = errors;
}

public void addError(String error) {
if (this.errors == null) {
this.errors = new ArrayList<>();
}
errors.add(error);
}

@Override
public String toString() {
return "RecordImportResult{" +
"totalCount=" + totalCount +
", importedCount=" + importedCount +
(errors != null ? ", errors=" + errors : "") +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package cz.cvut.kbss.study.exception;

/**
* Indicates that the application is attempting to import a record with a nonexistent author.
*/
public class RecordAuthorNotFoundException extends RecordManagerException {

public RecordAuthorNotFoundException(String message) {
super(message);
}
}
16 changes: 16 additions & 0 deletions src/main/java/cz/cvut/kbss/study/model/RecordPhase.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,20 @@ public enum RecordPhase {
public String getIri() {
return iri;
}

/**
* Returns {@link RecordPhase} with the specified IRI.
*
* @param iri record phase identifier
* @return matching {@code RecordPhase}
* @throws IllegalArgumentException When no matching phase is found
*/
public static RecordPhase fromString(String iri) {
for (RecordPhase p : values()) {
if (p.getIri().equals(iri)) {
return p;
}
}
throw new IllegalArgumentException("Unknown record phase identifier '" + iri + "'.");
}
}
20 changes: 19 additions & 1 deletion src/main/java/cz/cvut/kbss/study/model/User.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package cz.cvut.kbss.study.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import cz.cvut.kbss.jopa.model.annotations.*;
import cz.cvut.kbss.jopa.model.annotations.FetchType;
import cz.cvut.kbss.jopa.model.annotations.Id;
import cz.cvut.kbss.jopa.model.annotations.OWLClass;
import cz.cvut.kbss.jopa.model.annotations.OWLDataProperty;
import cz.cvut.kbss.jopa.model.annotations.OWLObjectProperty;
import cz.cvut.kbss.jopa.model.annotations.ParticipationConstraints;
import cz.cvut.kbss.jopa.model.annotations.Types;
import cz.cvut.kbss.study.model.util.HasDerivableUri;
import cz.cvut.kbss.study.util.Constants;
import cz.cvut.kbss.study.util.IdentificationUtils;
Expand Down Expand Up @@ -148,6 +154,18 @@ public void addType(String type) {
getTypes().add(type);
}

/**
* Returns true if this user is an admin.
* <p>
* That is, it has an admin type.
*
* @return {@code true} if this is admin, {@code false} otherwise
*/
public boolean isAdmin() {
assert types != null;
return getTypes().contains(Vocabulary.s_c_administrator);
}

public String getToken() {
return token;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ public PatientRecord findByKey(String key) {
@Override
public void persist(PatientRecord entity) {
Objects.requireNonNull(entity);
entity.setKey(IdentificationUtils.generateKey());
entity.setUri(generateRecordUriFromKey(entity.getKey()));
if (entity.getKey() == null) {
entity.setKey(IdentificationUtils.generateKey());
}
if (entity.getUri() == null) {
entity.setUri(generateRecordUriFromKey(entity.getKey()));
}
try {
final Descriptor descriptor = getDescriptor(entity.getUri());
em.persist(entity, descriptor);
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package cz.cvut.kbss.study.rest;

import cz.cvut.kbss.study.dto.PatientRecordDto;
import cz.cvut.kbss.study.dto.RecordImportResult;
import cz.cvut.kbss.study.exception.NotFoundException;
import cz.cvut.kbss.study.model.Institution;
import cz.cvut.kbss.study.model.PatientRecord;
import cz.cvut.kbss.study.model.RecordPhase;
import cz.cvut.kbss.study.rest.exception.BadRequestException;
import cz.cvut.kbss.study.rest.util.RecordFilterMapper;
import cz.cvut.kbss.study.rest.util.RestUtils;
Expand Down Expand Up @@ -94,6 +96,20 @@ public ResponseEntity<Void> createRecord(@RequestBody PatientRecord record) {
return new ResponseEntity<>(headers, HttpStatus.CREATED);
}

@PostMapping(value = "/import", consumes = MediaType.APPLICATION_JSON_VALUE)
public RecordImportResult importRecords(@RequestBody List<PatientRecord> records,
@RequestParam(name = "phase", required = false) String phaseIri) {
final RecordImportResult importResult;
if (phaseIri != null) {
final RecordPhase targetPhase = RecordPhase.fromString(phaseIri);
importResult = recordService.importRecords(records, targetPhase);
} else {
importResult = recordService.importRecords(records);
}
LOG.trace("Records imported with result: {}.", importResult);
return importResult;
}

@PutMapping(value = "/{key}", consumes = MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updateRecord(@PathVariable("key") String key, @RequestBody PatientRecord record) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package cz.cvut.kbss.study.rest.handler;

import cz.cvut.kbss.study.exception.*;
import cz.cvut.kbss.study.exception.EntityExistsException;
import cz.cvut.kbss.study.exception.NotFoundException;
import cz.cvut.kbss.study.exception.PersistenceException;
import cz.cvut.kbss.study.exception.RecordAuthorNotFoundException;
import cz.cvut.kbss.study.exception.ValidationException;
import cz.cvut.kbss.study.exception.WebServiceIntegrationException;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -83,13 +88,14 @@ public ResponseEntity<ErrorInfo> persistenceException(HttpServletRequest request
return new ResponseEntity<>(errorInfo(request, e.getCause()), HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(RecordAuthorNotFoundException.class)
public ResponseEntity<ErrorInfo> recordAuthorNotFoundException(HttpServletRequest request,
RecordAuthorNotFoundException e) {
logException(request, e);
return new ResponseEntity<>(errorInfo(request, e), HttpStatus.CONFLICT);
}

void logException(HttpServletRequest request, RuntimeException e) {
LOG.debug(
String.format(
"Request to '%s' failed due to error: %s",
request.getRequestURI(),
e.getMessage()
)
);
LOG.debug("Request to '{}' failed due to error: {}", request.getRequestURI(), e.getMessage());
}
}
40 changes: 40 additions & 0 deletions src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package cz.cvut.kbss.study.service;

import cz.cvut.kbss.study.dto.PatientRecordDto;
import cz.cvut.kbss.study.dto.RecordImportResult;
import cz.cvut.kbss.study.model.Institution;
import cz.cvut.kbss.study.model.PatientRecord;
import cz.cvut.kbss.study.model.RecordPhase;
import cz.cvut.kbss.study.model.User;
import cz.cvut.kbss.study.persistence.dao.util.RecordFilterParams;

Expand Down Expand Up @@ -51,4 +53,42 @@ public interface PatientRecordService extends BaseService<PatientRecord> {
* @see #findAllRecords()
*/
List<PatientRecord> findAllFull(RecordFilterParams filterParams);

/**
* Imports the specified records.
* <p>
* Only records whose identifiers do not already exist in the repository are imported. Existing records are skipped
* and the returned object contains a note that the record already exists.
* <p>
* This method, in contrast to {@link #importRecords(List, RecordPhase)}, preserves the phase of the imported
* records.
* <p>
* If the current user is an admin, the import procedure retains provenance data of the record. Otherwise, the
* current user is set as the record's author. Also, if the current user is not an admin, the phase of all
* the imported records is set to {@link RecordPhase#open}, for admin, the phase of the records is retained.
*
* @param records Records to import
* @return Instance representing the import result
* @throws cz.cvut.kbss.study.exception.RecordAuthorNotFoundException Thrown when importing a record whose author
* does not exist in this application instance's
* repository
*/
RecordImportResult importRecords(List<PatientRecord> records);

/**
* Imports the specified records and sets them all to the specified phase.
* <p>
* Only records whose identifiers do not already exist in the repository are imported. Existing records are skipped
* and the returned object contains a note that the record already exists.
* <p>
* If the current user is an admin, the import procedure retains provenance data of the record. Otherwise, the
* current user is set as the record's author.
*
* @param records Records to import
* @return Instance representing the import result
* @throws cz.cvut.kbss.study.exception.RecordAuthorNotFoundException Thrown when importing a record whose author
* does not exist in this application instance's
* repository
*/
RecordImportResult importRecords(List<PatientRecord> records, RecordPhase targetPhase);
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,46 @@
package cz.cvut.kbss.study.service.repository;

import cz.cvut.kbss.study.dto.PatientRecordDto;
import cz.cvut.kbss.study.dto.RecordImportResult;
import cz.cvut.kbss.study.exception.RecordAuthorNotFoundException;
import cz.cvut.kbss.study.model.Institution;
import cz.cvut.kbss.study.model.PatientRecord;
import cz.cvut.kbss.study.model.RecordPhase;
import cz.cvut.kbss.study.model.User;
import cz.cvut.kbss.study.persistence.dao.OwlKeySupportingDao;
import cz.cvut.kbss.study.persistence.dao.PatientRecordDao;
import cz.cvut.kbss.study.persistence.dao.util.RecordFilterParams;
import cz.cvut.kbss.study.service.PatientRecordService;
import cz.cvut.kbss.study.service.UserService;
import cz.cvut.kbss.study.service.security.SecurityUtils;
import cz.cvut.kbss.study.util.IdentificationUtils;
import cz.cvut.kbss.study.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Service
public class RepositoryPatientRecordService extends KeySupportingRepositoryService<PatientRecord>
implements PatientRecordService {

private static final Logger LOG = LoggerFactory.getLogger(RepositoryPatientRecordService.class);

private final PatientRecordDao recordDao;

private final SecurityUtils securityUtils;

public RepositoryPatientRecordService(PatientRecordDao recordDao,
SecurityUtils securityUtils) {
private final UserService userService;

public RepositoryPatientRecordService(PatientRecordDao recordDao, SecurityUtils securityUtils,
UserService userService) {
this.recordDao = recordDao;
this.securityUtils = securityUtils;
this.userService = userService;
}

@Override
Expand Down Expand Up @@ -65,7 +78,6 @@ protected void prePersist(PatientRecord instance) {
instance.setAuthor(author);
instance.setDateCreated(new Date());
instance.setInstitution(author.getInstitution());
instance.setKey(IdentificationUtils.generateKey());
recordDao.requireUniqueNonEmptyLocalName(instance);
}

Expand All @@ -75,4 +87,53 @@ protected void preUpdate(PatientRecord instance) {
instance.setLastModified(new Date());
recordDao.requireUniqueNonEmptyLocalName(instance);
}

@Transactional
@Override
public RecordImportResult importRecords(List<PatientRecord> records) {
Objects.requireNonNull(records);
LOG.debug("Importing records.");
return importRecordsImpl(records, Optional.empty());
}

private RecordImportResult importRecordsImpl(List<PatientRecord> records, Optional<RecordPhase> targetPhase) {
final User author = securityUtils.getCurrentUser();
final Date created = new Date();
final RecordImportResult result = new RecordImportResult(records.size());
records.forEach(r -> {
setImportedRecordProvenance(author, created, targetPhase, r);
if (recordDao.exists(r.getUri())) {
LOG.warn("Record {} already exists. Skipping it.", Utils.uriToString(r.getUri()));
result.addError("Record " + Utils.uriToString(r.getUri()) + " already exists.");
} else {
recordDao.persist(r);
result.incrementImportedCount();
}
});
return result;
}

private void setImportedRecordProvenance(User currentUser, Date now, Optional<RecordPhase> targetPhase,
PatientRecord record) {
if (!currentUser.isAdmin()) {
record.setAuthor(currentUser);
record.setInstitution(currentUser.getInstitution());
record.setDateCreated(now);
targetPhase.ifPresentOrElse(record::setPhase, () -> record.setPhase(RecordPhase.open));
} else {
targetPhase.ifPresent(record::setPhase);
if (!userService.exists(record.getAuthor().getUri())) {
throw new RecordAuthorNotFoundException("Author of record " + record + "not found during import.");
}
}
}


@Transactional
@Override
public RecordImportResult importRecords(List<PatientRecord> records, RecordPhase targetPhase) {
Objects.requireNonNull(records);
LOG.debug("Importing records to target phase '{}'.", targetPhase);
return importRecordsImpl(records, Optional.ofNullable(targetPhase));
}
}
Loading