diff --git a/src/main/java/org/ohdsi/webapi/conceptset/ConceptSet.java b/src/main/java/org/ohdsi/webapi/conceptset/ConceptSet.java index ee49b11226..e19f97ebca 100644 --- a/src/main/java/org/ohdsi/webapi/conceptset/ConceptSet.java +++ b/src/main/java/org/ohdsi/webapi/conceptset/ConceptSet.java @@ -26,6 +26,7 @@ import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; +import javax.persistence.OneToOne; import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.Parameter; import org.ohdsi.webapi.model.CommonEntity; diff --git a/src/main/java/org/ohdsi/webapi/conceptset/annotation/ConceptSetAnnotation.java b/src/main/java/org/ohdsi/webapi/conceptset/annotation/ConceptSetAnnotation.java new file mode 100644 index 0000000000..f13fd19db7 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/conceptset/annotation/ConceptSetAnnotation.java @@ -0,0 +1,108 @@ +package org.ohdsi.webapi.conceptset.annotation; + +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Parameter; +import org.ohdsi.webapi.model.CommonEntity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Table; +import java.io.Serializable; + +@Entity(name = "ConceptSetAnnotation") +@Table(name = "concept_set_annotation") +public class ConceptSetAnnotation implements Serializable { + /** + * + */ + private static final long serialVersionUID = 1L; + + @Id + @GenericGenerator( + name = "concept_set_annotation_generator", + strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator", + parameters = { + @Parameter(name = "sequence_name", value = "concept_set_annotation_sequence"), + @Parameter(name = "increment_size", value = "1") + } + ) + @GeneratedValue(generator = "concept_set_annotation_generator") + @Column(name = "concept_set_annotation_id") + private Integer id; + + @Column(name = "concept_set_id", nullable = false) + private Integer conceptSetId; + + @Column(name = "concept_id") + private Integer conceptId; + + @Column(name = "annotation_details") + private String annotationDetails; + + @Column(name = "vocabulary_version") + private String vocabularyVersion; + + @Column(name = "concept_set_version") + private Integer conceptSetVersion; + + @Column(name = "copied_from_concept_set_ids") + private String copiedFromConceptSetIds; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getConceptSetId() { + return conceptSetId; + } + + public void setConceptSetId(Integer conceptSetId) { + this.conceptSetId = conceptSetId; + } + + public Integer getConceptId() { + return conceptId; + } + + public void setConceptId(Integer conceptId) { + this.conceptId = conceptId; + } + + public String getAnnotationDetails() { + return annotationDetails; + } + + public void setAnnotationDetails(String annotationDetails) { + this.annotationDetails = annotationDetails; + } + + public String getVocabularyVersion() { + return vocabularyVersion; + } + + public void setVocabularyVersion(String vocabularyVersion) { + this.vocabularyVersion = vocabularyVersion; + } + + public Integer getConceptSetVersion() { + return conceptSetVersion; + } + + public void setConceptSetVersion(Integer conceptSetVersion) { + this.conceptSetVersion = conceptSetVersion; + } + + public String getCopiedFromConceptSetIds() { + return copiedFromConceptSetIds; + } + + public void setCopiedFromConceptSetIds(String copiedFromConceptSetIds) { + this.copiedFromConceptSetIds = copiedFromConceptSetIds; + } +} diff --git a/src/main/java/org/ohdsi/webapi/conceptset/annotation/ConceptSetAnnotationRepository.java b/src/main/java/org/ohdsi/webapi/conceptset/annotation/ConceptSetAnnotationRepository.java new file mode 100644 index 0000000000..f44bd01c72 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/conceptset/annotation/ConceptSetAnnotationRepository.java @@ -0,0 +1,20 @@ +package org.ohdsi.webapi.conceptset.annotation; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ConceptSetAnnotationRepository extends JpaRepository { + + @Query("DELETE FROM ConceptSetAnnotation cc WHERE cc.conceptSetId = :conceptSetId and cc.conceptId in :conceptId") + void deleteAnnotationByConceptSetIdAndInConceptId(int conceptSetId, List conceptId); + + void deleteAnnotationByConceptSetIdAndConceptId(int conceptSetId, int conceptId); + + List findByConceptSetId(int conceptSetId); + ConceptSetAnnotation findById(int id); + void deleteById(int id); + Optional findConceptSetAnnotationByConceptIdAndConceptId(int conceptSetId, int conceptId); +} diff --git a/src/main/java/org/ohdsi/webapi/security/model/ConceptSetPermissionSchema.java b/src/main/java/org/ohdsi/webapi/security/model/ConceptSetPermissionSchema.java index 66b4b1a4b2..e5d9c72cdc 100644 --- a/src/main/java/org/ohdsi/webapi/security/model/ConceptSetPermissionSchema.java +++ b/src/main/java/org/ohdsi/webapi/security/model/ConceptSetPermissionSchema.java @@ -11,17 +11,21 @@ public class ConceptSetPermissionSchema extends EntityPermissionSchema { private static Map writePermissions = new HashMap() {{ put("conceptset:%s:put", "Update Concept Set with ID = %s"); put("conceptset:%s:items:put", "Update Items of Concept Set with ID = %s"); + put("conceptset:*:annotation:put", "Create Concept Set Annotation"); + put("conceptset:%s:annotation:*:delete", "Delete Annotations of Concept Set with ID = %s"); + put("conceptset:*:annotation:*:delete", "Delete Annotations of any Concept Set"); put("conceptset:%s:delete", "Delete Concept Set with ID = %s"); }}; private static Map readPermissions = new HashMap() {{ put("conceptset:%s:get", "view conceptset definition with id %s"); put("conceptset:%s:expression:get", "Resolve concept set %s expression"); + put("conceptset:%s:annotation:get", "Resolve concept set annotations"); + put("conceptset:*:annotation:get", "Resolve concept set annotations"); put("conceptset:%s:version:*:expression:get", "Get expression for concept set %s items for default source"); }}; public ConceptSetPermissionSchema() { - super(EntityType.CONCEPT_SET, readPermissions, writePermissions); } } diff --git a/src/main/java/org/ohdsi/webapi/security/model/EntityType.java b/src/main/java/org/ohdsi/webapi/security/model/EntityType.java index d5a9d63acf..c294cdb17a 100644 --- a/src/main/java/org/ohdsi/webapi/security/model/EntityType.java +++ b/src/main/java/org/ohdsi/webapi/security/model/EntityType.java @@ -27,7 +27,6 @@ public enum EntityType { COHORT_SAMPLE(CohortSample.class), TAG(Tag.class), REUSABLE(Reusable.class); - private final Class entityClass; EntityType(Class entityClass) { diff --git a/src/main/java/org/ohdsi/webapi/service/AbstractDaoService.java b/src/main/java/org/ohdsi/webapi/service/AbstractDaoService.java index 95277cbe68..b84ef71890 100644 --- a/src/main/java/org/ohdsi/webapi/service/AbstractDaoService.java +++ b/src/main/java/org/ohdsi/webapi/service/AbstractDaoService.java @@ -20,6 +20,7 @@ import org.ohdsi.webapi.conceptset.ConceptSetComparison; import org.ohdsi.webapi.conceptset.ConceptSetItemRepository; import org.ohdsi.webapi.conceptset.ConceptSetRepository; +import org.ohdsi.webapi.conceptset.annotation.ConceptSetAnnotationRepository; import org.ohdsi.webapi.exception.BadRequestAtlasException; import org.ohdsi.webapi.ircalc.IncidenceRateAnalysis; import org.ohdsi.webapi.model.CommonEntity; @@ -106,6 +107,9 @@ public abstract class AbstractDaoService extends AbstractAdminService { @Autowired private ConceptSetItemRepository conceptSetItemRepository; + @Autowired + private ConceptSetAnnotationRepository conceptSetAnnotationRepository; + @Autowired protected Security security; @@ -120,6 +124,9 @@ public abstract class AbstractDaoService extends AbstractAdminService { public ConceptSetItemRepository getConceptSetItemRepository() { return conceptSetItemRepository; } + public ConceptSetAnnotationRepository getConceptSetAnnotationRepository() { + return conceptSetAnnotationRepository; + } @Autowired private ConceptSetRepository conceptSetRepository; diff --git a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java index 2f80ef991e..84bc8e7787 100644 --- a/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java +++ b/src/main/java/org/ohdsi/webapi/service/ConceptSetService.java @@ -25,6 +25,9 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + import org.apache.shiro.authz.UnauthorizedException; import org.ohdsi.circe.vocabulary.ConceptSetExpression; import org.ohdsi.vocabulary.Concept; @@ -36,9 +39,15 @@ import org.ohdsi.webapi.conceptset.ConceptSetGenerationInfoRepository; import org.ohdsi.webapi.conceptset.ConceptSetItem; import org.ohdsi.webapi.conceptset.dto.ConceptSetVersionFullDTO; +import org.ohdsi.webapi.conceptset.annotation.ConceptSetAnnotation; import org.ohdsi.webapi.exception.ConceptNotExistException; import org.ohdsi.webapi.security.PermissionService; +import org.ohdsi.webapi.service.annotations.SearchDataTransformer; +import org.ohdsi.webapi.service.dto.AnnotationDetailsDTO; import org.ohdsi.webapi.service.dto.ConceptSetDTO; +import org.ohdsi.webapi.service.dto.SaveConceptSetAnnotationsRequest; +import org.ohdsi.webapi.service.dto.AnnotationDTO; +import org.ohdsi.webapi.service.dto.CopyAnnotationsRequest; import org.ohdsi.webapi.shiro.Entities.UserEntity; import org.ohdsi.webapi.shiro.Entities.UserRepository; import org.ohdsi.webapi.shiro.management.Security; @@ -64,12 +73,12 @@ import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Component; - /** - * Provides REST services for working with - * concept sets. - * - * @summary Concept Set - */ +/** + * Provides REST services for working with + * concept sets. + * + * @summary Concept Set + */ @Component @Transactional @Path("/conceptset/") @@ -105,14 +114,21 @@ public class ConceptSetService extends AbstractDaoService implements HasTags versionService; + @Autowired + private SearchDataTransformer searchDataTransformer; + + @Autowired + private ObjectMapper mapper; + + @Value("${security.defaultGlobalReadPermissions}") private boolean defaultGlobalReadPermissions; - + public static final String COPY_NAME = "copyName"; /** * Get the concept set based in the identifier - * + * * @summary Get concept set by ID * @param id The concept set ID * @return The concept set definition @@ -128,7 +144,7 @@ public ConceptSetDTO getConceptSet(@PathParam("id") final int id) { /** * Get the full list of concept sets in the WebAPI database - * + * * @summary Get all concept sets * @return A list of all concept sets in the WebAPI database */ @@ -151,7 +167,7 @@ public Collection getConceptSets() { /** * Get the concept set items for a selected concept set ID. - * + * * @summary Get the concept set items * @param id The concept set identifier * @return A list of concept set items @@ -165,7 +181,7 @@ public Iterable getConceptSetItems(@PathParam("id") final int id /** * Get the concept set expression for a selected version of the expression - * + * * @summary Get concept set expression by version * @param id The concept set ID * @param version The version identifier @@ -188,7 +204,7 @@ public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int i * source key. NOTE: This method requires the specification * of a source key but it does not appear to be used by the underlying * code. - * + * * @summary Get concept set expression by version and source. * @param id The concept set identifier * @param version The version of the concept set @@ -210,7 +226,7 @@ public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int i /** * Get the concept set expression by identifier - * + * * @summary Get concept set by ID * @param id The concept set identifier * @return The concept set expression @@ -228,7 +244,7 @@ public ConceptSetExpression getConceptSetExpression(@PathParam("id") final int i /** * Get the concept set expression by identifier and source key - * + * * @summary Get concept set by ID and source * @param id The concept set ID * @param sourceKey The source key @@ -288,26 +304,26 @@ private ConceptSetExpression getConceptSetExpression(int id, Integer version, So throw new ConceptNotExistException("Current data source does not contain required concepts " + ids); } for(Concept concept : concepts) { - map.put(concept.conceptId, concept); // associate the concept object to the conceptID in the map + map.put(concept.conceptId, concept); // associate the concept object to the conceptID in the map } - // put the concept information into the expression along with the concept set item information + // put the concept information into the expression along with the concept set item information for (ConceptSetItem repositoryItem : repositoryItems) { - ConceptSetExpression.ConceptSetItem currentItem = new ConceptSetExpression.ConceptSetItem(); - currentItem.concept = map.get(repositoryItem.getConceptId()); - currentItem.includeDescendants = (repositoryItem.getIncludeDescendants() == 1); - currentItem.includeMapped = (repositoryItem.getIncludeMapped() == 1); - currentItem.isExcluded = (repositoryItem.getIsExcluded() == 1); - expressionItems.add(currentItem); + ConceptSetExpression.ConceptSetItem currentItem = new ConceptSetExpression.ConceptSetItem(); + currentItem.concept = map.get(repositoryItem.getConceptId()); + currentItem.includeDescendants = (repositoryItem.getIncludeDescendants() == 1); + currentItem.includeMapped = (repositoryItem.getIncludeMapped() == 1); + currentItem.isExcluded = (repositoryItem.getIsExcluded() == 1); + expressionItems.add(currentItem); } expression.items = expressionItems.toArray(new ConceptSetExpression.ConceptSetItem[0]); // this will return a new array - + return expression; } /** * Check if the concept set name exists (DEPRECATED) - * + * * @summary DO NOT USE * @deprecated * @param id The concept set ID @@ -328,11 +344,11 @@ public Response getConceptSetExistsDeprecated(@PathParam("id") final int id, @Pa * Check if a concept set with the same name exists in the WebAPI * database. The name is checked against the selected concept set ID * to ensure that only the selected concept set ID has the name specified. - * + * * @summary Concept set with same name exists * @param id The concept set ID * @param name The name of the concept set - * @return The count of concept sets with the name, excluding the + * @return The count of concept sets with the name, excluding the * specified concept set ID. */ @GET @@ -345,11 +361,11 @@ public int getCountCSetWithSameName(@PathParam("id") @DefaultValue("0") final in /** * Update the concept set items for the selected concept set ID in the * WebAPI database. - * + * * The concept set has two parts: 1) the elements of the ConceptSetDTO that - * consist of the identifier, name, etc. 2) the concept set items which + * consist of the identifier, name, etc. 2) the concept set items which * contain the concepts and their mapping (i.e. include descendants). - * + * * @summary Update concept set items * @param id The concept set ID * @param items An array of ConceptSetItems @@ -376,12 +392,12 @@ public boolean saveConceptSetItems(@PathParam("id") final int id, ConceptSetItem * Exports a list of concept sets, based on the conceptSetList argument, * to one or more comma separated value (CSV) file(s), compresses the files * into a ZIP file and sends the ZIP file to the client. - * + * * @summary Export concept set list to CSV files * @param conceptSetList A list of concept set identifiers in the format * conceptset=++ * @return - * @throws Exception + * @throws Exception */ @GET @Path("/exportlist") @@ -411,7 +427,7 @@ public Response exportConceptSetList(@QueryParam("conceptsets") final String con // Get the concept set information cs.add(getConceptSetForExport(conceptSetIds.get(i), new SourceInfo(source))); } - // Write Concept Set Expression to a CSV + // Write Concept Set Expression to a CSV baos = ExportUtil.writeConceptSetExportToCSVAndZip(cs); response = Response @@ -427,12 +443,12 @@ public Response exportConceptSetList(@QueryParam("conceptsets") final String con } /** - * Exports a single concept set to a comma separated value (CSV) + * Exports a single concept set to a comma separated value (CSV) * file, compresses to a ZIP file and sends to the client. * @param id The concept set ID * @return A zip file containing the exported concept set - * @throws Exception + * @throws Exception */ @GET @Path("{id}/export") @@ -444,7 +460,7 @@ public Response exportConceptSetToCSV(@PathParam("id") final String id) throws E /** * Save a new concept set to the WebAPI database - * + * * @summary Create a new concept set * @param conceptSetDTO The concept set to save * @return The concept set saved with the concept set identifier @@ -470,7 +486,7 @@ public ConceptSetDTO createConceptSet(ConceptSetDTO conceptSetDTO) { * that is used when generating a copy of an existing concept set. This * function is generally used in conjunction with the copy endpoint to * create a unique name and then save a copy of an existing concept set. - * + * * @sumamry Get concept set name suggestion for copying * @param id The concept set ID * @return A map of the new concept set name and the existing concept set @@ -492,17 +508,17 @@ public List getNamesLike(String copyName) { /** * Updates the concept set for the selected concept set. - * + * * The concept set has two parts: 1) the elements of the ConceptSetDTO that - * consist of the identifier, name, etc. 2) the concept set items which + * consist of the identifier, name, etc. 2) the concept set items which * contain the concepts and their mapping (i.e. include descendants). - * + * * @summary Update concept set * @param id The concept set identifier * @param conceptSetDTO The concept set header * @return The - * @throws Exception - */ + * @throws Exception + */ @Path("/{id}") @PUT @Consumes(MediaType.APPLICATION_JSON) @@ -512,7 +528,7 @@ public ConceptSetDTO updateConceptSet(@PathParam("id") final int id, ConceptSetD ConceptSet updated = getConceptSetRepository().findById(id); if (updated == null) { - throw new Exception("Concept Set does not exist."); + throw new Exception("Concept Set does not exist."); } saveVersion(id); @@ -528,11 +544,11 @@ private ConceptSet updateConceptSet(ConceptSet dst, ConceptSet src) { dst.setDescription(src.getDescription()); dst.setModifiedDate(new Date()); dst.setModifiedBy(user); - + dst = this.getConceptSetRepository().save(dst); return dst; } - + private ConceptSetExport getConceptSetForExport(int conceptSetId, SourceInfo vocabSource) { ConceptSetExport cs = new ConceptSetExport(); @@ -556,63 +572,63 @@ private ConceptSetExport getConceptSetForExport(int conceptSetId, SourceInfo voc * Get the concept set generation information for the selected concept * set ID. This function only works with the configuration of the CEM * data source. - * + * * @link https://github.com/OHDSI/CommonEvidenceModel/wiki - * + * * @summary Get concept set generation info * @param id The concept set identifier. * @return A collection of concept set generation info objects */ - @GET - @Path("{id}/generationinfo") - @Produces(MediaType.APPLICATION_JSON) - public Collection getConceptSetGenerationInfo(@PathParam("id") final int id) { - return this.conceptSetGenerationInfoRepository.findAllByConceptSetId(id); - } - - /** - * Delete the selected concept set by concept set identifier - * - * @summary Delete concept set - * @param id The concept set ID - */ - @DELETE - @Transactional(rollbackOn = Exception.class, dontRollbackOn = EmptyResultDataAccessException.class) - @Path("{id}") - public void deleteConceptSet(@PathParam("id") final int id) { - // Remove any generation info - try { - this.conceptSetGenerationInfoRepository.deleteByConceptSetId(id); - } catch (EmptyResultDataAccessException e) { - // Ignore - there may be no data - log.warn("Failed to delete Generation Info by ConceptSet with ID = {}, {}", id, e); - } - catch (Exception e) { - throw e; - } - - // Remove the concept set items - try { - getConceptSetItemRepository().deleteByConceptSetId(id); - } catch (EmptyResultDataAccessException e) { - // Ignore - there may be no data - log.warn("Failed to delete ConceptSet items with ID = {}, {}", id, e); - } - catch (Exception e) { - throw e; - } - - // Remove the concept set - try { - getConceptSetRepository().delete(id); - } catch (EmptyResultDataAccessException e) { - // Ignore - there may be no data - log.warn("Failed to delete ConceptSet with ID = {}, {}", id, e); - } - catch (Exception e) { - throw e; - } - } + @GET + @Path("{id}/generationinfo") + @Produces(MediaType.APPLICATION_JSON) + public Collection getConceptSetGenerationInfo(@PathParam("id") final int id) { + return this.conceptSetGenerationInfoRepository.findAllByConceptSetId(id); + } + + /** + * Delete the selected concept set by concept set identifier + * + * @summary Delete concept set + * @param id The concept set ID + */ + @DELETE + @Transactional(rollbackOn = Exception.class, dontRollbackOn = EmptyResultDataAccessException.class) + @Path("{id}") + public void deleteConceptSet(@PathParam("id") final int id) { + // Remove any generation info + try { + this.conceptSetGenerationInfoRepository.deleteByConceptSetId(id); + } catch (EmptyResultDataAccessException e) { + // Ignore - there may be no data + log.warn("Failed to delete Generation Info by ConceptSet with ID = {}, {}", id, e); + } + catch (Exception e) { + throw e; + } + + // Remove the concept set items + try { + getConceptSetItemRepository().deleteByConceptSetId(id); + } catch (EmptyResultDataAccessException e) { + // Ignore - there may be no data + log.warn("Failed to delete ConceptSet items with ID = {}, {}", id, e); + } + catch (Exception e) { + throw e; + } + + // Remove the concept set + try { + getConceptSetRepository().delete(id); + } catch (EmptyResultDataAccessException e) { + // Ignore - there may be no data + log.warn("Failed to delete ConceptSet with ID = {}, {}", id, e); + } + catch (Exception e) { + throw e; + } + } /** * Assign tag to Concept Set @@ -683,10 +699,10 @@ public void unassignPermissionProtectedTag(@PathParam("id") final int id, @PathP } /** - * Checks a concept set for diagnostic problems. At this time, + * Checks a concept set for diagnostic problems. At this time, * this appears to be an endpoint used to check to see which tags * are applied to a concept set. - * + * * @summary Concept set tag check * @since v2.10.0 * @param conceptSetDTO The concept set @@ -857,4 +873,130 @@ private ConceptSetVersion saveVersion(int id) { version.setCreatedDate(versionDate); return versionService.create(VersionType.CONCEPT_SET, version); } -} + + /** + * Update the concept set annotation for each concept in concept set ID in the + * WebAPI database. + *

+ * The body has two parts: 1) the elements new concept which added to the + * concept set. 2) the elements concept which remove from concept set. + * + * @param conceptSetId The concept set ID + * @param request An object of 2 Array new annotation and remove annotation + * @return Boolean: true if the save is successful + * @summary Create new or delete concept set annotation items + */ + @PUT + @Path("/{id}/annotation") + @Produces(MediaType.APPLICATION_JSON) + @Transactional + public boolean saveConceptSetAnnotation(@PathParam("id") final int conceptSetId, SaveConceptSetAnnotationsRequest request) { + removeAnnotations(conceptSetId, request); + if (request.getNewAnnotation() != null && !request.getNewAnnotation().isEmpty()) { + List annotationList = request.getNewAnnotation() + .stream() + .map(newAnnotationData -> { + ConceptSetAnnotation conceptSetAnnotation = new ConceptSetAnnotation(); + conceptSetAnnotation.setConceptSetId(conceptSetId); + try { + AnnotationDetailsDTO annotationDetailsDTO = new AnnotationDetailsDTO(); + annotationDetailsDTO.setId(newAnnotationData.getId()); + annotationDetailsDTO.setConceptId(newAnnotationData.getConceptId()); + annotationDetailsDTO.setSearchData(newAnnotationData.getSearchData()); + conceptSetAnnotation.setAnnotationDetails(mapper.writeValueAsString(annotationDetailsDTO)); + } catch (JsonProcessingException e) { + log.error("Could not serialize Concept Set AnnotationDetailsDTO", e); + throw new RuntimeException(e); + } + conceptSetAnnotation.setVocabularyVersion(newAnnotationData.getVocabularyVersion()); + conceptSetAnnotation.setConceptSetVersion(newAnnotationData.getConceptSetVersion()); + conceptSetAnnotation.setConceptId(newAnnotationData.getConceptId()); + return conceptSetAnnotation; + }).collect(Collectors.toList()); + + this.getConceptSetAnnotationRepository().save(annotationList); + } + + return true; + } + private void removeAnnotations(int id, SaveConceptSetAnnotationsRequest request){ + if (request.getRemoveAnnotation() != null && !request.getRemoveAnnotation().isEmpty()) { + for (AnnotationDTO annotationDTO : request.getRemoveAnnotation()) { + this.getConceptSetAnnotationRepository().deleteAnnotationByConceptSetIdAndConceptId(id, annotationDTO.getConceptId()); + } + } + } + @POST + @Path("/copy-annotations") + @Produces(MediaType.APPLICATION_JSON) + @Transactional + public void copyAnnotations(CopyAnnotationsRequest copyAnnotationsRequest ) { + List sourceAnnotations = getConceptSetAnnotationRepository().findByConceptSetId(copyAnnotationsRequest.getSourceConceptSetId()); + List copiedAnnotations= sourceAnnotations.stream() + .map(sourceAnnotation -> copyAnnotation(sourceAnnotation, copyAnnotationsRequest.getSourceConceptSetId(), copyAnnotationsRequest.getTargetConceptSetId())) + .collect(Collectors.toList()); + getConceptSetAnnotationRepository().save(copiedAnnotations); + } + private ConceptSetAnnotation copyAnnotation(ConceptSetAnnotation sourceConceptSetAnnotation, int sourceConceptSetId, int targetConceptSetId){ + ConceptSetAnnotation targetConceptSetAnnotation = new ConceptSetAnnotation(); + targetConceptSetAnnotation.setConceptSetId(targetConceptSetId); + targetConceptSetAnnotation.setConceptSetVersion(sourceConceptSetAnnotation.getConceptSetVersion()); + targetConceptSetAnnotation.setAnnotationDetails(sourceConceptSetAnnotation.getAnnotationDetails()); + targetConceptSetAnnotation.setConceptId(sourceConceptSetAnnotation.getConceptId()); + targetConceptSetAnnotation.setVocabularyVersion(sourceConceptSetAnnotation.getVocabularyVersion()); + targetConceptSetAnnotation.setCopiedFromConceptSetIds(appendCopiedFromConceptSetId(sourceConceptSetAnnotation.getCopiedFromConceptSetIds(), sourceConceptSetId)); + return targetConceptSetAnnotation; + } + private String appendCopiedFromConceptSetId(String copiedFromConceptSetIds, int sourceConceptSetId) { + if(copiedFromConceptSetIds == null || copiedFromConceptSetIds.isEmpty()){ + return Integer.toString(sourceConceptSetId); + } + return copiedFromConceptSetIds.concat(",").concat(Integer.toString(sourceConceptSetId)); + } + + @GET + @Path("/{id}/annotation") + @Produces(MediaType.APPLICATION_JSON) + public List getConceptSetAnnotation(@PathParam("id") final int id) { + List annotationList = getConceptSetAnnotationRepository().findByConceptSetId(id); + return annotationList.stream() + .map(this::convertAnnotationEntityToDTO) + .collect(Collectors.toList()); + } + + + private AnnotationDTO convertAnnotationEntityToDTO(ConceptSetAnnotation conceptSetAnnotation) { + AnnotationDetailsDTO annotationDetails; + try { + annotationDetails = mapper.readValue(conceptSetAnnotation.getAnnotationDetails(), AnnotationDetailsDTO.class); + } catch (JsonProcessingException e) { + log.error("Could not deserialize Concept Set AnnotationDetailsDTO", e); + throw new RuntimeException(e); + } + + AnnotationDTO annotationDTO = new AnnotationDTO(); + + annotationDTO.setId(conceptSetAnnotation.getId()); + annotationDTO.setConceptId(conceptSetAnnotation.getConceptId()); + + String searchDataJSON = annotationDetails.getSearchData(); + String humanReadableData = searchDataTransformer.convertJsonToReadableFormat(searchDataJSON); + annotationDTO.setSearchData(humanReadableData); + + annotationDTO.setVocabularyVersion(conceptSetAnnotation.getVocabularyVersion()); + annotationDTO.setConceptSetVersion(conceptSetAnnotation.getConceptSetVersion()); + annotationDTO.setCopiedFromConceptSetIds(conceptSetAnnotation.getCopiedFromConceptSetIds()); + return annotationDTO; + } + + @DELETE + @Path("/{conceptSetId}/annotation/{annotationId}") + @Produces(MediaType.APPLICATION_JSON) + public Response deleteConceptSetAnnotation(@PathParam("conceptSetId") final int conceptSetId, @PathParam("annotationId") final int annotationId) { + ConceptSetAnnotation conceptSetAnnotation = getConceptSetAnnotationRepository().findById(annotationId); + if (conceptSetAnnotation != null) { + getConceptSetAnnotationRepository().deleteById(annotationId); + return Response.ok().build(); + } else throw new NotFoundException("Concept set annotation not found"); + } +} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/service/annotations/SearchDataTransformer.java b/src/main/java/org/ohdsi/webapi/service/annotations/SearchDataTransformer.java new file mode 100644 index 0000000000..e013adfb0f --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/service/annotations/SearchDataTransformer.java @@ -0,0 +1,100 @@ +package org.ohdsi.webapi.service.annotations; + +import org.apache.commons.lang3.StringUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Service +public class SearchDataTransformer { + + private static final String FILTER_DATA = "filterData"; + private static final String TITLE = "title"; + private static final String VALUE = "value"; + private static final String KEY = "key"; + private static final String FILTER_SOURCE = "filterSource"; + private static final String FILTER_SOURCE_LABEL = "Filtered By"; + private static final String SEARCH_TEXT = "searchText"; + private static final String DEFAULT_FILTER_SOURCE = "Search"; + private static final String DELIMITER = ", "; + private static final String ENTRY_FORMAT = "%s: \"%s\""; + + public String convertJsonToReadableFormat(String jsonInput) { + JSONObject searchObject = new JSONObject(Optional.ofNullable(jsonInput).orElse("{}")); + + if (searchObject.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(); + + String filterSource = processFilterSource(searchObject); + append(result, getDefaultOrActual(filterSource, DEFAULT_FILTER_SOURCE)); + + JSONObject filterDataObject = searchObject.optJSONObject(FILTER_DATA); + JSONArray filterDataArray = searchObject.optJSONArray(FILTER_DATA); + + if (filterDataObject != null) { + Optional.ofNullable(filterDataObject).map(this::processSearchText).ifPresent(searchText -> appendCommaSeparated(result, formatQuoted(searchText))); + Optional.ofNullable(filterDataObject.optJSONArray("filterColumns")).map(this::formatKeyValuePairs).ifPresent( + fdResult -> appendCommaSeparated(result, FILTER_SOURCE_LABEL + ": \"" + fdResult + "\"") + ); + } else if (filterDataArray != null) { + String extractedData = formatKeyValuePairs(filterDataArray); + if (!extractedData.isEmpty()) { + appendCommaSeparated(result, FILTER_SOURCE_LABEL + ": \"" + extractedData + "\""); + } + } + + return result.toString().trim(); + } + + private String processFilterSource(JSONObject jsonObject) { + return jsonObject.optString(FILTER_SOURCE, ""); + } + + private String processSearchText(JSONObject filterData) { + return filterData.optString(SEARCH_TEXT, ""); + } + + private String formatKeyValuePairs(JSONArray filterDataArray) { + return IntStream.range(0, filterDataArray.length()) + .mapToObj(index -> formatEntry(filterDataArray.getJSONObject(index))) + .collect(Collectors.joining(DELIMITER)); + } + + private String formatEntry(JSONObject item) { + String title = optString(item, TITLE); + String key = StringUtils.unwrap(optString(item, KEY), '"'); + return String.format(ENTRY_FORMAT, title, key); + } + + private void appendCommaSeparated(StringBuilder builder, String part) { + if (!part.isEmpty()) { + append(builder, part); + } + } + + private void append(StringBuilder builder, String part) { + if (builder.length() > 0) { + builder.append(DELIMITER); + } + builder.append(part); + } + + private String optString(JSONObject item, String key) { + return item.optString(key, ""); + } + + private String getDefaultOrActual(String actual, String defaultVal) { + return actual.isEmpty() ? defaultVal : actual; + } + + private String formatQuoted(String text) { + return String.format("\"%s\"", text); + } +} \ No newline at end of file diff --git a/src/main/java/org/ohdsi/webapi/service/dto/AnnotationDTO.java b/src/main/java/org/ohdsi/webapi/service/dto/AnnotationDTO.java new file mode 100644 index 0000000000..0ed0808034 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/service/dto/AnnotationDTO.java @@ -0,0 +1,80 @@ +package org.ohdsi.webapi.service.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class AnnotationDTO { + + private Integer id; + private String createdBy; + private String createdDate; + private String vocabularyVersion; + private Integer conceptSetVersion; + private String searchData; + private String copiedFromConceptSetIds; + private Integer conceptId; + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public String getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(String createdDate) { + this.createdDate = createdDate; + } + + public String getVocabularyVersion() { + return vocabularyVersion; + } + + public void setVocabularyVersion(String vocabularyVersion) { + this.vocabularyVersion = vocabularyVersion; + } + + public Integer getConceptSetVersion() { + return conceptSetVersion; + } + + public void setConceptSetVersion(Integer conceptSetVersion) { + this.conceptSetVersion = conceptSetVersion; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getSearchData() { + return searchData; + } + + public void setSearchData(String searchData) { + this.searchData = searchData; + } + + public Integer getConceptId() { + return conceptId; + } + + public void setConceptId(Integer conceptId) { + this.conceptId = conceptId; + } + + public String getCopiedFromConceptSetIds() { + return copiedFromConceptSetIds; + } + + public void setCopiedFromConceptSetIds(String copiedFromConceptSetIds) { + this.copiedFromConceptSetIds = copiedFromConceptSetIds; + } +} diff --git a/src/main/java/org/ohdsi/webapi/service/dto/AnnotationDetailsDTO.java b/src/main/java/org/ohdsi/webapi/service/dto/AnnotationDetailsDTO.java new file mode 100644 index 0000000000..d62acb2493 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/service/dto/AnnotationDetailsDTO.java @@ -0,0 +1,37 @@ +package org.ohdsi.webapi.service.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class AnnotationDetailsDTO { + private Integer id; + + private String searchData; + + private Integer conceptId; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getSearchData() { + return searchData; + } + + public void setSearchData(String searchData) { + this.searchData = searchData; + } + + public Integer getConceptId() { + return conceptId; + } + + public void setConceptId(Integer conceptId) { + this.conceptId = conceptId; + } + +} diff --git a/src/main/java/org/ohdsi/webapi/service/dto/ConceptSetDTO.java b/src/main/java/org/ohdsi/webapi/service/dto/ConceptSetDTO.java index e91993332d..1323d338ed 100644 --- a/src/main/java/org/ohdsi/webapi/service/dto/ConceptSetDTO.java +++ b/src/main/java/org/ohdsi/webapi/service/dto/ConceptSetDTO.java @@ -29,4 +29,5 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + } diff --git a/src/main/java/org/ohdsi/webapi/service/dto/CopyAnnotationsRequest.java b/src/main/java/org/ohdsi/webapi/service/dto/CopyAnnotationsRequest.java new file mode 100644 index 0000000000..1d9313b5e9 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/service/dto/CopyAnnotationsRequest.java @@ -0,0 +1,23 @@ +package org.ohdsi.webapi.service.dto; + +public class CopyAnnotationsRequest { + + private int sourceConceptSetId; + private int targetConceptSetId; + + public int getSourceConceptSetId() { + return sourceConceptSetId; + } + + public int getTargetConceptSetId() { + return targetConceptSetId; + } + + public void setSourceConceptSetId(int sourceConceptSetId) { + this.sourceConceptSetId = sourceConceptSetId; + } + + public void setTargetConceptSetId(int targetConceptSetId) { + this.targetConceptSetId = targetConceptSetId; + } +} diff --git a/src/main/java/org/ohdsi/webapi/service/dto/SaveConceptSetAnnotationsRequest.java b/src/main/java/org/ohdsi/webapi/service/dto/SaveConceptSetAnnotationsRequest.java new file mode 100644 index 0000000000..3d8469ab99 --- /dev/null +++ b/src/main/java/org/ohdsi/webapi/service/dto/SaveConceptSetAnnotationsRequest.java @@ -0,0 +1,27 @@ +package org.ohdsi.webapi.service.dto; + +import java.util.List; + +public class SaveConceptSetAnnotationsRequest { + + private List newAnnotation; + + private List removeAnnotation; + + public List getNewAnnotation() { + return newAnnotation; + } + + public void setNewAnnotation(List newAnnotation) { + this.newAnnotation = newAnnotation; + } + + public List getRemoveAnnotation() { + return removeAnnotation; + } + + public void setRemoveAnnotation(List removeAnnotation) { + this.removeAnnotation = removeAnnotation; + } + +} diff --git a/src/main/resources/db/migration/postgresql/V2.15.0.20240716100000__conceptset_annotations.sql b/src/main/resources/db/migration/postgresql/V2.15.0.20240716100000__conceptset_annotations.sql new file mode 100644 index 0000000000..04da4e9153 --- /dev/null +++ b/src/main/resources/db/migration/postgresql/V2.15.0.20240716100000__conceptset_annotations.sql @@ -0,0 +1,67 @@ +CREATE SEQUENCE ${ohdsiSchema}.concept_set_annotation_sequence; + +CREATE TABLE ${ohdsiSchema}.concept_set_annotation +( + concept_set_annotation_id int4 NOT NULL DEFAULT nextval('${ohdsiSchema}.concept_set_annotation_sequence'), + concept_set_id integer NOT NULL, + concept_id integer, + annotation_details VARCHAR, + vocabulary_version VARCHAR, + created_by_id INTEGER, + created_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now()), + modified_by_id INTEGER, + modified_date TIMESTAMP WITH TIME ZONE, + concept_set_version VARCHAR(100), + copied_from_concept_set_ids VARCHAR(1000), + CONSTRAINT pk_concept_set_annotation_id PRIMARY KEY (concept_set_annotation_id), + CONSTRAINT fk_concept_set FOREIGN KEY (concept_set_id) + REFERENCES ${ohdsiSchema}.concept_set (concept_set_id) + ON DELETE CASCADE +); + +DELETE FROM ${ohdsiSchema}.sec_role_permission +WHERE permission_id IN ( + SELECT id FROM ${ohdsiSchema}.sec_permission + WHERE value like '%:annotation:%' +); + +DELETE FROM ${ohdsiSchema}.sec_permission +WHERE value like '%:annotation:%'; + +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'conceptset:*:annotation:put', 'Create Concept Set Annotation'); + +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'conceptset:%s:annotation:get', 'List Concept Set Annotations'); + +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'conceptset:*:annotation:get', 'View Concept Set Annotation'); + +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'conceptset:%s:annotation:*:delete', 'Delete Owner`s Concept Set Annotations'); + +INSERT INTO ${ohdsiSchema}.sec_permission(id, value, description) VALUES + (nextval('${ohdsiSchema}.sec_permission_id_seq'), 'conceptset:*:annotation:*:delete', 'Delete Any Concept Set Annotation'); + +INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id) +SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id +FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr +WHERE sp.value IN ( + 'conceptset:*:annotation:put', + 'conceptset:*:annotation:*:delete', + 'conceptset:%s:annotation:*:delete', + 'conceptset:%s:annotation:get', + 'conceptset:*:annotation:get' + ) AND sr.name IN ('admin'); + +INSERT INTO ${ohdsiSchema}.sec_role_permission(id, role_id, permission_id) +SELECT nextval('${ohdsiSchema}.sec_role_permission_sequence'), sr.id, sp.id +FROM ${ohdsiSchema}.sec_permission SP, ${ohdsiSchema}.sec_role sr +WHERE sp.value IN ( + 'conceptset:*:annotation:put', + 'conceptset:%s:annotation:*:delete', + 'conceptset:%s:annotation:get', + 'conceptset:*:annotation:get' + ) AND sr.name IN ('Atlas users'); + +ALTER TABLE ${ohdsiSchema}.concept_set_annotation ALTER COLUMN concept_set_version TYPE INTEGER USING (concept_set_version::integer); \ No newline at end of file diff --git a/src/main/resources/i18n/messages_en.json b/src/main/resources/i18n/messages_en.json index 5a247d4bb4..e4ea6edf60 100644 --- a/src/main/resources/i18n/messages_en.json +++ b/src/main/resources/i18n/messages_en.json @@ -1408,6 +1408,7 @@ "notPrevalent": "Not Prevalent", "optimizedOut": "Optimized Out", "options": "Options", + "originConceptSets": "Origin Concept Sets", "outcomeCohortName": "Outcome Cohort Name", "outcomeId": "Outcome Id", "outcomeModel": "Outcome Model", @@ -1521,7 +1522,11 @@ "rcTooltip": "Record Count", "drcTooltip": "Descendant Record Count", "pcTooltip": "Person Count", - "dpcTooltip": "Descendant Person Count" + "dpcTooltip": "Descendant Person Count", + "conceptID": "Concept Id", + "searchData": "Search Data", + "createdBy": "Created By", + "createdDate": "Created Date" }, "dataSources": { "const": { @@ -2446,7 +2451,8 @@ "alerts": { "clearLocalCache": "Local Storage has been cleared. Please refresh the page to reload configuration information.", "clearServerCache": "Server cache has been cleared.", - "failUpdatePrioritySourceDaimon": "Failed to update priority source daimon" + "failUpdatePrioritySourceDaimon": "Failed to update priority source daimon", + "failUpdateCurrentVocabVersion": "Failed to update current vocabulary version" }, "buttons": { "check": "check", diff --git a/src/main/resources/i18n/messages_ko.json b/src/main/resources/i18n/messages_ko.json index 63fe140849..9ec9712441 100644 --- a/src/main/resources/i18n/messages_ko.json +++ b/src/main/resources/i18n/messages_ko.json @@ -1408,6 +1408,7 @@ "notPrevalent": "만연하지 않음(Not Prevalent)", "optimizedOut": "최적화", "options": "옵션", + "originConceptSets": "오리진 컨셉트 세트", "outcomeCohortName": "아웃컴(outcome) 코호트 이름", "outcomeId": "아웃컴(outcome) ID", "outcomeModel": "결과 모델", @@ -1521,7 +1522,11 @@ "rcTooltip": "Record Count", "drcTooltip": "Descendant Record Count", "pcTooltip": "Person Count", - "dpcTooltip": "Descendant Person Count" + "dpcTooltip": "Descendant Person Count", + "conceptID": "개념 ID", + "searchData": "데이터 검색", + "createdBy": "만든 사람", + "createdDate": "생성 날짜" }, "dataSources": { "const": { diff --git a/src/main/resources/i18n/messages_ru.json b/src/main/resources/i18n/messages_ru.json index 6b576d00e3..aae125875e 100644 --- a/src/main/resources/i18n/messages_ru.json +++ b/src/main/resources/i18n/messages_ru.json @@ -1338,6 +1338,7 @@ "binary": "Двоичный", "model": "Модель", "options": "Опции", + "originConceptSets": "Исходные наборы концепций", "firstExposureOnly": "Только первое знакомство", "washoutPeriod": "Период вымывания", "includeAllOutcomes": "Включить все результаты", @@ -1521,7 +1522,11 @@ "pcTooltip": "Количество пациентов", "dpcTooltip": "Количество пациентов-потомков", "validStartDate": "Действительная дата начала", - "validEndDate": "Действительная дата конца" + "validEndDate": "Действительная дата конца", + "conceptID": "Идентификатор концепции", + "searchData": "Данные поиска", + "createdBy": "Автор", + "createdDate": "Дата создания" }, "dataSources": { "headingTitle": "Источники данных", diff --git a/src/main/resources/i18n/messages_zh.json b/src/main/resources/i18n/messages_zh.json index bc0688ba71..207cfcd706 100644 --- a/src/main/resources/i18n/messages_zh.json +++ b/src/main/resources/i18n/messages_zh.json @@ -1408,6 +1408,7 @@ "notPrevalent": "不普遍", "optimizedOut": "优化了", "options": "选择", + "originConceptSets": "起源概念集", "outcomeCohortName": "结果队列名称", "outcomeId": "结果编号", "outcomeModel": "结果模型", @@ -1521,7 +1522,11 @@ "rcTooltip": "Record Count", "drcTooltip": "Descendant Record Count", "pcTooltip": "Person Count", - "dpcTooltip": "Descendant Person Count" + "dpcTooltip": "Descendant Person Count", + "conceptID": "概念 ID", + "searchData": "搜索数据", + "createdBy": "由...制作", + "createdDate": "创建日期" }, "dataSources": { "const": { diff --git a/src/test/java/org/ohdsi/webapi/service/annotations/SearchDataTransformerTest.java b/src/test/java/org/ohdsi/webapi/service/annotations/SearchDataTransformerTest.java new file mode 100644 index 0000000000..ea24b5ac76 --- /dev/null +++ b/src/test/java/org/ohdsi/webapi/service/annotations/SearchDataTransformerTest.java @@ -0,0 +1,70 @@ +package org.ohdsi.webapi.service.annotations; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.isEmptyString; + +public class SearchDataTransformerTest { + + private SearchDataTransformer sut; + + @Before + public void setUp() { + sut = new SearchDataTransformer(); + } + + @Test + public void shouldReturnEmptyStringWhenInputIsEmpty() { + JSONObject emptyJson = new JSONObject(); + String transformed = sut.convertJsonToReadableFormat(emptyJson.toString()); + assertThat(transformed, isEmptyString()); + } + + @Test + public void shouldHandleSearchText() { + String input = "{\"filterData\":{\"searchText\":\"testSearch\"}}"; + String result = sut.convertJsonToReadableFormat(input); + assertThat(result, is("Search, \"testSearch\"")); + } + + @Test + public void shouldHandleFilterSource() { + String input = "{\"filterSource\":\"Search\"}"; + String result = sut.convertJsonToReadableFormat(input); + assertThat(result, is("Search")); + } + + @Test + public void shouldHandleFilterColumns() { + String input = "{\"filterData\":{\"filterColumns\":[{\"title\":\"Domain\",\"key\":\"Drug\"}]} }"; + String result = sut.convertJsonToReadableFormat(input); + assertThat(result, is("Search, \"\", Filtered By: \"Domain: \"Drug\"\"")); + } + + @Test + public void shouldCombineFilterDataAndFilterSource() { + String input = "{\"filterData\":{\"searchText\":\"testSearch\",\"filterColumns\":[{\"title\":\"Domain\",\"key\":\"Drug\"}]},\"filterSource\":\"Search\"}"; + String result = sut.convertJsonToReadableFormat(input); + String expected = "Search, \"testSearch\", Filtered By: \"Domain: \"Drug\"\""; + assertThat(result, is(expected)); + } + + @Test + public void shouldHandleMultipleFilterColumns() { + String input = "{\"filterData\":{\"filterColumns\":[{\"title\":\"Domain\",\"key\":\"Drug\"},{\"title\":\"Class\",\"key\":\"Medication\"}]}}"; + String result = sut.convertJsonToReadableFormat(input); + String expected = "Search, \"\", Filtered By: \"Domain: \"Drug\"" + ", Class: \"Medication\"\""; + assertThat(result, is(expected)); + } + + @Test + public void shouldHandleNullValuesGracefully() { + String input = "{\"filterData\":{\"filterColumns\":[{\"title\":null,\"key\":null}], \"searchText\":null}, \"filterSource\":null}"; + String result = sut.convertJsonToReadableFormat(input); + assertThat(result, is("Search, \"\", Filtered By: \": \"\"\"")); + } +} \ No newline at end of file