Skip to content

Commit

Permalink
Merge pull request #41 from akaene/main
Browse files Browse the repository at this point in the history
Records import
  • Loading branch information
blcham authored Dec 27, 2023
2 parents 0bb6e50 + 69da5c1 commit 84ed465
Show file tree
Hide file tree
Showing 15 changed files with 544 additions and 16 deletions.
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

0 comments on commit 84ed465

Please sign in to comment.