Conversation
fixed case when file contains valid ObsUnitIds but no phenotypic data columns
| } | ||
| @Override | ||
| public List<ImportWorkflow> getWorkflows() { | ||
| public List<ImportWorkflow> getWorkflows() throws UnprocessableEntityException { |
There was a problem hiding this comment.
There is some code in this method that pulls an exception out of the workflow context and throws it (ImportWorkflowResult::getCaughtException). I think that was the intention for the append workflow and how exceptions are expected to be propagated. There are places in the processing code that use ImportWorkflowResult::setCaughtException
dmeidlin
left a comment
There was a problem hiding this comment.
Getting and setting caught exceptions in the ImportWorkflowResult is deliberate and necessary for implementing a SAGA design pattern for handling transactions atomically. Changing the error messages and status codes is fine, but do not change the error handling to simply halt execution and propagate exceptions up the layers to be handled. This short-circuits the SAGA pattern and prevents compensating actions from being performed.
I understand. I tried having |
|
After discussions with Nick, Dave, and Shawn, I've made several changes.
|
|
Caught exceptions are handled in the middleware instead of the lower layers, so just pass the exception through PendingObservationUnit#brapiRead and move the ValidationError handler up to the middleware. |
|
The first middleware step in the appendOverwrite workflow is AppendOverwriteIDValidation.java which currently checks for empty values or duplicated values in the ObsUnitID column, throwing an exception if any are found which appears as a banner message on the front end. Validating that every valid ObSUnitID value is also found in the BrAPI service should be done in this step. This involves simply moving this line from |
|
|
|
|
|
There's a bug in |
|
I spoke with @shawnyarnes about the error messaging for the case of missing or duplicated ObsUnitID values and the source of truth has been updated and should be used for the validation errors. |
|
There are a couple of problems with setting all summary statistics counts to zero in Each import row and each cell of observation data can be in one of many possible states that requires its own set of validation steps, pending object processing, and statistics calculation, and there is a Then in the import process method just before looping over any observation variables defined add a check for this edge case where there are no observation variables and set Then have processedData call the same methods to update statistics as is used when processing rows with observation variables. It would be good to place these lines in a helper method |
| public static final String UNMATCHED_COLUMN = "Ontology term(s) not found: "; | ||
|
|
||
|
|
||
| public static final String INVALID_OBS_UNIT_ID_ERROR = "Invalid ObsUnitID"; |
There was a problem hiding this comment.
The message for empty ids is the same but duplicate ids have a more specific error message. Also, constants specific to the append workflow are here.
| public static final String INVALID_OBS_UNIT_ID_ERROR = "Invalid ObsUnitID"; | |
| public enum ErrMessage { | |
| MULTIPLE_EXP_TITLES("File contains more than one Experiment Title"), | |
| MISSING_OBS_UNIT_ID("Invalid ObsUnitID"), | |
| PREEXISTING_EXPERIMENT_TITLE("Experiment Title already exists"), | |
| UNMATCHED_COLUMN("Ontology term(s) not found: "), | |
| OBS_UNIT_NOT_FOUND("Invalid ObsUnitID"), | |
| DUPLICATE_OBS_UNIT_ID("ObsUnitId is repeated"); |
| package org.breedinginsight.brapps.importer.services.processors.experiment; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
| import java.util.Set; | ||
|
|
||
| @Getter | ||
| public class MissingValuesException extends Exception { | ||
| private Set<String> missingIds; | ||
|
|
||
| public MissingValuesException(Set<String> missingIds) { this.missingIds = missingIds; } | ||
| } |
There was a problem hiding this comment.
| package org.breedinginsight.brapps.importer.services.processors.experiment; | |
| import lombok.Getter; | |
| import java.util.Set; | |
| @Getter | |
| public class MissingValuesException extends Exception { | |
| private Set<String> missingIds; | |
| public MissingValuesException(Set<String> missingIds) { this.missingIds = missingIds; } | |
| } | |
| package org.breedinginsight.brapps.importer.services.processors.experiment.model; | |
| import lombok.Getter; | |
| import java.util.Set; | |
| @Getter | |
| public class EntityNotFoundException extends Throwable { | |
| private Set<String> missingEntityIds; | |
| public EntityNotFoundException(Set<String> missingEntityIds) { this.missingEntityIds = missingEntityIds; } | |
| } |
| * @throws ApiException if an error occurs during the execution of the action. | ||
| */ | ||
| Optional<BrAPIState<T>> execute() throws ApiException, MissingRequiredInfoException, UnprocessableEntityException, DoesNotExistException; | ||
| Optional<BrAPIState<T>> execute() throws ApiException, MissingRequiredInfoException, UnprocessableEntityException, DoesNotExistException, ValidatorException; |
There was a problem hiding this comment.
| Optional<BrAPIState<T>> execute() throws ApiException, MissingRequiredInfoException, UnprocessableEntityException, DoesNotExistException, ValidatorException; | |
| Optional<BrAPIState<T>> execute() throws ApiException, MissingRequiredInfoException, UnprocessableEntityException, DoesNotExistException, EntityNotFoundException; |
| * @throws ApiException if an error occurs during execution | ||
| */ | ||
| public Optional<BrAPIState<T>> execute() throws ApiException { | ||
| public Optional<BrAPIState<T>> execute() throws ApiException, ValidatorException { |
There was a problem hiding this comment.
| public Optional<BrAPIState<T>> execute() throws ApiException, ValidatorException { | |
| public Optional<BrAPIState<T>> execute() throws ApiException, EntityNotFoundException { |
| * @throws ApiException if there is an issue with the API call | ||
| */ | ||
| public List<T> brapiRead() throws ApiException; | ||
| public List<T> brapiRead() throws ApiException, ValidatorException; |
There was a problem hiding this comment.
| public List<T> brapiRead() throws ApiException, ValidatorException; | |
| public List<T> brapiRead() throws ApiException, EntityNotFoundException; |
| try { | ||
| // For each id fetch the observation unit from the brapi data store | ||
| return observationUnitService.getObservationUnitsByDbId(new HashSet<>(obsUnitIds), importContext.getProgram()); | ||
| } | ||
| catch (MissingValuesException e) { | ||
| ValidationErrors validationErrors = new ValidationErrors(); | ||
|
|
||
| // Build a detailed tabular error. | ||
| for (int rowNum = 0; rowNum < importContext.getImportRows().size(); rowNum++) { | ||
| String rowObsUnitId = ((ExperimentObservation)importContext.getImportRows().get(rowNum)).getObsUnitID(); | ||
| if (e.getMissingIds().contains(rowObsUnitId)) { | ||
| ExperimentUtilities.addRowError(ExperimentObservation.Columns.OBS_UNIT_ID, ExperimentUtilities.INVALID_OBS_UNIT_ID_ERROR, validationErrors, rowNum); | ||
| } | ||
| } | ||
| throw new ValidatorException(validationErrors); | ||
| } |
There was a problem hiding this comment.
| try { | |
| // For each id fetch the observation unit from the brapi data store | |
| return observationUnitService.getObservationUnitsByDbId(new HashSet<>(obsUnitIds), importContext.getProgram()); | |
| } | |
| catch (MissingValuesException e) { | |
| ValidationErrors validationErrors = new ValidationErrors(); | |
| // Build a detailed tabular error. | |
| for (int rowNum = 0; rowNum < importContext.getImportRows().size(); rowNum++) { | |
| String rowObsUnitId = ((ExperimentObservation)importContext.getImportRows().get(rowNum)).getObsUnitID(); | |
| if (e.getMissingIds().contains(rowObsUnitId)) { | |
| ExperimentUtilities.addRowError(ExperimentObservation.Columns.OBS_UNIT_ID, ExperimentUtilities.INVALID_OBS_UNIT_ID_ERROR, validationErrors, rowNum); | |
| } | |
| } | |
| throw new ValidatorException(validationErrors); | |
| } | |
| @Override | |
| public List<BrAPIObservationUnit> brapiRead() throws ApiException, EntityNotFoundException { | |
| // Collect deltabreed-generated obs unit ids listed in the import | |
| Set<String> obsUnitIds = cache.getReferenceOUIds(); | |
| // For each id fetch the observation unit from the brapi data store | |
| return observationUnitService.getObservationUnitsByDbId(new HashSet<>(obsUnitIds), importContext.getProgram()); | |
| } |
| brAPIDatasetReadWorkflowInitialization.execute(); | ||
| brAPIGermplasmReadWorkflowInitialization.execute(); | ||
| } catch (ApiException e) { | ||
| } catch (ApiException | ValidatorException e) { |
There was a problem hiding this comment.
Remove the OU initialization from this middleware and add it to the prior validation middleware step.
| } catch (ApiException | ValidatorException e) { | |
| @Override | |
| public AppendOverwriteMiddlewareContext process(AppendOverwriteMiddlewareContext context) { | |
| brAPITrialReadWorkflowInitialization = brAPIReadFactory.trialWorkflowReadInitializationBean(context); | |
| brAPIStudyReadWorkflowInitialization = brAPIReadFactory.studyWorkflowReadInitializationBean(context); | |
| locationReadWorkflowInitialization = brAPIReadFactory.locationWorkflowReadInitializationBean(context); | |
| brAPIDatasetReadWorkflowInitialization = brAPIReadFactory.datasetWorkflowReadInitializationBean(context); | |
| brAPIGermplasmReadWorkflowInitialization = brAPIReadFactory.germplasmWorkflowReadInitializationBean(context); | |
| log.debug("reading required BrAPI data from BrAPI service"); | |
| try { | |
| brAPITrialReadWorkflowInitialization.execute(); | |
| brAPIStudyReadWorkflowInitialization.execute(); | |
| locationReadWorkflowInitialization.execute(); | |
| brAPIDatasetReadWorkflowInitialization.execute(); | |
| brAPIGermplasmReadWorkflowInitialization.execute(); | |
| } catch (ApiException e) { | |
| context.getAppendOverwriteWorkflowContext().setProcessError(new MiddlewareException(e)); | |
| return this.compensate(context); | |
| } catch (EntityNotFoundException e) { | |
| // TODO: handle edge cases of missing brapi entities as needed | |
| return this.compensate(context); | |
| } | |
| return processNext(context); | |
| } |
| // Make sure the workflow statistic is not null. | ||
| if (context.getAppendOverwriteWorkflowContext().getStatistic() == null) { | ||
| context.getAppendOverwriteWorkflowContext().setStatistic(statistic); | ||
| } | ||
|
|
There was a problem hiding this comment.
| // Make sure the workflow statistic is not null. | |
| if (context.getAppendOverwriteWorkflowContext().getStatistic() == null) { | |
| context.getAppendOverwriteWorkflowContext().setStatistic(statistic); | |
| } | |
| /** | |
| * Handle the edge case where a user imports with the append/overwrite workflow for an experiment | |
| * without a dataset defined (i.e. no observation variables headers) and the import does not | |
| * actually have new data to append | |
| */ | |
| if (phenotypeCols.isEmpty()) { | |
| processedData = processedDataFactory.undefinedDatasetBean(); | |
| updatePreviewStatistics(processedData, context, studyName, unitId); | |
| } | |
| // Assemble the pending observation data for all phenotypes | |
| for (Column<?> column : phenotypeCols) { | |
| String cellData = column.getString(rowNum); | |
| * @throws IllegalStateException if the retrieved observation units do not match the provided observation unit IDs | ||
| */ | ||
| public List<BrAPIObservationUnit> getObservationUnitsByDbId(Set<String> obsUnitIds, Program program) throws ApiException, IllegalStateException { | ||
| public List<BrAPIObservationUnit> getObservationUnitsByDbId(Set<String> obsUnitIds, Program program) throws ApiException, IllegalStateException, MissingValuesException { |
There was a problem hiding this comment.
The method name should be changed, the bug fixed, and EntityNotFoundException used as a throwable.
| public List<BrAPIObservationUnit> getObservationUnitsByDbId(Set<String> obsUnitIds, Program program) throws ApiException, IllegalStateException, MissingValuesException { | |
| public List<BrAPIObservationUnit> getObservationUnitsById(Set<String> obsUnitIds, Program program) throws ApiException, IllegalStateException, EntityNotFoundException { | |
| List<BrAPIObservationUnit> brapiUnits = null; | |
| // Retrieve reference Observation Units based on IDs | |
| brapiUnits = brAPIObservationUnitDAO.getObservationUnitsById(obsUnitIds, program); | |
| // If no BrAPI units are found, throw an EntityNotFoundException with an error message | |
| if (obsUnitIds.size() != brapiUnits.size()) { | |
| Set<String> missingIds = new HashSet<>(obsUnitIds); | |
| // Calculate missing IDs based on retrieved BrAPI units | |
| //missingIds.removeAll(brapiUnits.stream().map(BrAPIObservationUnit::getObservationUnitDbId).collect(Collectors.toSet())); | |
| missingIds.removeAll(brapiUnits.stream() | |
| .map(unit -> Utilities.getExternalReference(unit.getExternalReferences(), BRAPI_REFERENCE_SOURCE, ExternalReferenceSource.OBSERVATION_UNITS)) | |
| .filter(Optional::isPresent) | |
| .map(Optional::get) | |
| .map(BrAPIExternalReference::getReferenceId).collect(Collectors.toSet())); | |
| // Throw exception with missing IDs information | |
| throw new EntityNotFoundException(missingIds); | |
| } | |
| return brapiUnits; | |
| } |


Description
Story: https://breedinginsight.atlassian.net/browse/BI-2156 (These edge cases were discovered in QA while testing BI-2156, but they have nothing to do with BI-2156.)
Current Behavior
When using the "Append experimental dataset" workflow (a) with a file that contains non-existent ObsUnitIds, and (b) with a file that contains valid ObsUnitIds but no Ontology Terms or Observation Data, generic errors were being shown to the user.
Desired Behavior
Ideally (a) should result in an useful error message and (b) shouldn’t result in an error, but selecting commit shouldn’t change anything in the experiment.
Changes
For (a) the main change was creating a better error message,
INVALID_OBS_UNIT_ID_ERRORinExperimentUtilities.java, the other changes (acrossmanyseveral files) were to propagate the exception correctly so that a 4xx status is returned rather than a 5xx status.For (b) (commit e52481f) I handled a null case to prevent a NullPointerException in
AppendOverwritePhenotypesWorkflow.javaat this line.Testing
(a)
(b)
Checklist: