Skip to content

Commit 2b9c35a

Browse files
oleg-odysseusalex-odysseus
authored andcommitted
[ATL-58] Fixed permissions issues for annotations feature, transformed search data JSON to a human friendly format in annotations tab
1 parent f40a666 commit 2b9c35a

File tree

6 files changed

+217
-26
lines changed

6 files changed

+217
-26
lines changed

src/main/java/org/ohdsi/webapi/security/model/ConceptSetAnnotationPermissionSchema.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,16 @@ public class ConceptSetAnnotationPermissionSchema extends EntityPermissionSchema
1010

1111
private static Map<String, String> writePermissions = new HashMap<String, String>() {
1212
{
13-
put("conceptset:*:annotation:put", "Create Concept Set Annotation");
14-
put("conceptset:update:*:annotation:put", "Update Concept Set Annotation");
1513
put("conceptset:annotation:%s:delete", "Delete Concept Set Annotation with ID %s");
1614
}
1715
};
1816

1917
private static Map<String, String> readPermissions = new HashMap<String, String>() {
2018
{
21-
put("conceptset:%s:annotation:get", "List Concept Set Annotation with id %s");
22-
put("conceptset:*:annotation:get", "View Concept Set Annotation expression");
2319
}
2420
};
2521

2622
public ConceptSetAnnotationPermissionSchema() {
27-
2823
super(EntityType.CONCEPT_SET_ANNOTATION, readPermissions, writePermissions);
2924
}
3025
}

src/main/java/org/ohdsi/webapi/service/ConceptSetService.java

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.fasterxml.jackson.core.JsonProcessingException;
3030
import com.fasterxml.jackson.databind.ObjectMapper;
3131

32+
import org.apache.commons.collections4.CollectionUtils;
3233
import org.apache.shiro.authz.UnauthorizedException;
3334
import org.ohdsi.circe.vocabulary.ConceptSetExpression;
3435
import org.ohdsi.vocabulary.Concept;
@@ -43,9 +44,10 @@
4344
import org.ohdsi.webapi.conceptset.annotation.ConceptSetAnnotation;
4445
import org.ohdsi.webapi.exception.ConceptNotExistException;
4546
import org.ohdsi.webapi.security.PermissionService;
47+
import org.ohdsi.webapi.service.annotations.SearchDataTransformer;
4648
import org.ohdsi.webapi.service.dto.AnnotationDetailsDTO;
4749
import org.ohdsi.webapi.service.dto.ConceptSetDTO;
48-
import org.ohdsi.webapi.service.dto.ConceptSetAnnotationDTO;
50+
import org.ohdsi.webapi.service.dto.SaveConceptSetAnnotationsRequest;
4951
import org.ohdsi.webapi.service.dto.AnnotationDTO;
5052
import org.ohdsi.webapi.service.dto.CopyAnnotationsRequest;
5153
import org.ohdsi.webapi.shiro.Entities.UserEntity;
@@ -114,6 +116,12 @@ public class ConceptSetService extends AbstractDaoService implements HasTags<Int
114116
@Autowired
115117
private VersionService<ConceptSetVersion> versionService;
116118

119+
@Autowired
120+
private SearchDataTransformer searchDataTransformer;
121+
122+
@Autowired
123+
private ObjectMapper mapper;
124+
117125

118126
@Value("${security.defaultGlobalReadPermissions}")
119127
private boolean defaultGlobalReadPermissions;
@@ -875,28 +883,21 @@ private ConceptSetVersion saveVersion(int id) {
875883
* The body has two parts: 1) the elements new concept which added to the
876884
* concept set. 2) the elements concept which remove from concept set.
877885
*
878-
* @param id The concept set ID
879-
* @param dto An object of 2 Array new annotation and remove annotation
886+
* @param conceptSetId The concept set ID
887+
* @param request An object of 2 Array new annotation and remove annotation
880888
* @return Boolean: true if the save is successful
881889
* @summary Create new or delete concept set annotation items
882890
*/
883891
@PUT
884892
@Path("/{id}/annotation")
885893
@Produces(MediaType.APPLICATION_JSON)
886894
@Transactional
887-
public boolean saveConceptSetAnnotation(@PathParam("id") final int id, ConceptSetAnnotationDTO dto) {
888-
if (dto.getRemoveAnnotation() != null && !dto.getRemoveAnnotation().isEmpty()) {
889-
for (AnnotationDTO annotationDTO : dto.getRemoveAnnotation()) {
890-
this.getConceptSetAnnotationRepository().deleteAnnotationByConceptSetIdAndConceptId(id, annotationDTO.getConceptId());
891-
}
892-
// getConceptSetAnnotationRepository().deleteAnnotationByConceptSetIdAndInConceptId(id,
893-
// dto.getRemoveAnnotation().stream().map(AnnotationDTO::getConceptId).collect(Collectors.toList()));
894-
}
895-
ObjectMapper mapper = new ObjectMapper();
896-
if (dto.getNewAnnotation() != null && !dto.getNewAnnotation().isEmpty()) {
897-
List<ConceptSetAnnotation> annotationList = dto.getNewAnnotation().stream().map(m -> {
895+
public boolean saveConceptSetAnnotation(@PathParam("id") final int conceptSetId, SaveConceptSetAnnotationsRequest request) {
896+
removeAnnotations(conceptSetId, request);
897+
if (CollectionUtils.isNotEmpty(request.getNewAnnotation())) {
898+
List<ConceptSetAnnotation> annotationList = request.getNewAnnotation().stream().map(m -> {
898899
ConceptSetAnnotation conceptSetAnnotation = new ConceptSetAnnotation();
899-
conceptSetAnnotation.setConceptSetId(id);
900+
conceptSetAnnotation.setConceptSetId(conceptSetId);
900901
try {
901902
AnnotationDetailsDTO annotationDetailsDTO = mapper.readValue(mapper.writeValueAsString(m), AnnotationDetailsDTO.class);
902903
conceptSetAnnotation.setAnnotationDetails(mapper.writeValueAsString(annotationDetailsDTO));
@@ -916,7 +917,13 @@ public boolean saveConceptSetAnnotation(@PathParam("id") final int id, ConceptSe
916917

917918
return true;
918919
}
919-
920+
private void removeAnnotations(int id, SaveConceptSetAnnotationsRequest request){
921+
if (CollectionUtils.isNotEmpty(request.getRemoveAnnotation())) {
922+
for (AnnotationDTO annotationDTO : request.getRemoveAnnotation()) {
923+
this.getConceptSetAnnotationRepository().deleteAnnotationByConceptSetIdAndConceptId(id, annotationDTO.getConceptId());
924+
}
925+
}
926+
}
920927
private ConceptSetAnnotation copyAnnotation(ConceptSetAnnotation sourceConceptSetAnnotation, int targetConceptSetId){
921928
ConceptSetAnnotation targetConceptSetAnnotation = new ConceptSetAnnotation();
922929
targetConceptSetAnnotation.setConceptSetId(targetConceptSetId);
@@ -947,19 +954,22 @@ public void copyAnnotations(CopyAnnotationsRequest copyAnnotationsRequest ) {
947954
@Path("/{id}/annotation")
948955
@Produces(MediaType.APPLICATION_JSON)
949956
public List<AnnotationDTO> getConceptSetAnnotation(@PathParam("id") final int id) {
950-
ObjectMapper mapper = new ObjectMapper();
951957
List<ConceptSetAnnotation> annotationList = getConceptSetAnnotationRepository().findByConceptSetId(id);
952958
List<AnnotationDTO> annotationDTOList = new ArrayList<>();
953959
for (ConceptSetAnnotation conceptSetAnnotation : annotationList) {
954-
AnnotationDTO annotationDTO = null;
960+
AnnotationDTO annotationDTO;
955961
try {
956962
annotationDTO = mapper.readValue(conceptSetAnnotation.getAnnotationDetails(), AnnotationDTO.class);
957963
annotationDTO.setId(conceptSetAnnotation.getId());
958964
annotationDTO.setVocabularyVersion(conceptSetAnnotation.getVocabularyVersion());
959965
annotationDTO.setConceptSetVersion(conceptSetAnnotation.getConceptSetVersion());
960966
annotationDTO.setCreatedBy(conceptSetAnnotation.getCreatedBy() != null ? conceptSetAnnotation.getCreatedBy().getName() : null);
961967
annotationDTO.setCreatedDate(conceptSetAnnotation.getCreatedDate() != null ? conceptSetAnnotation.getCreatedDate().toString() : null);
962-
annotationDTOList.add(annotationDTO);
968+
969+
String searchDataJSON = annotationDTO.getSearchData();
970+
String humanReadableData = searchDataTransformer.convertJsonToReadableFormat(searchDataJSON);
971+
annotationDTO.setSearchData(humanReadableData);
972+
annotationDTOList.add(annotationDTO);
963973
} catch (IOException e) {
964974
throw new RuntimeException(e);
965975
}
@@ -982,7 +992,6 @@ public Response deleteConceptSetAnnotation(@PathParam("id") final int id) {
982992
@Path("/update/{id}/annotation")
983993
@Produces(MediaType.APPLICATION_JSON)
984994
public AnnotationDTO updateConceptSetAnnotation(@PathParam("id") final int id, AnnotationDTO annotationDTO) throws IOException {
985-
ObjectMapper mapper = new ObjectMapper();
986995
ConceptSetAnnotation conceptSetAnnotation = getConceptSetAnnotationRepository()
987996
.findConceptSetAnnotationByConceptIdAndConceptId(id, annotationDTO.getConceptId())
988997
.orElseThrow(() -> new RuntimeException("Concept set annotation not found"));
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package org.ohdsi.webapi.service.annotations;
2+
3+
import org.apache.commons.lang3.StringUtils;
4+
import org.json.JSONArray;
5+
import org.json.JSONObject;
6+
import org.springframework.stereotype.Service;
7+
8+
import java.util.stream.Collectors;
9+
import java.util.stream.IntStream;
10+
import java.util.stream.Stream;
11+
12+
@Service
13+
public class SearchDataTransformer {
14+
15+
private static final String FILTER_DATA = "filterData";
16+
private static final String TITLE = "title";
17+
private static final String VALUE = "value";
18+
private static final String FILTER_SOURCE = "filterSource";
19+
private static final String FILTER_SOURCE_LABEL = "Filter Source";
20+
private static final String SEARCH_TEXT = "searchText";
21+
private static final String SEARCH_TEXT_LABEL = "Search Text";
22+
private static final String FILTER_COLUMNS = "filterColumns";
23+
private static final String QUOTE = "\"";
24+
25+
private static final String DELIMITER = ", ";
26+
27+
private static final String ENTRY_FORMAT = "%s: \"%s\"";
28+
29+
public String convertJsonToReadableFormat(String jsonInput) {
30+
JSONObject searchObject = new JSONObject(jsonInput);
31+
StringBuilder result = new StringBuilder();
32+
33+
String filterDataResult = extractFilterData(searchObject);
34+
appendCommaSeparated(result, filterDataResult);
35+
36+
String filterSourceResult = processFilterSource(searchObject);
37+
appendCommaSeparated(result, filterSourceResult);
38+
39+
return result.toString();
40+
}
41+
42+
private String extractFilterData(JSONObject jsonObject) {
43+
JSONObject filterData = jsonObject.optJSONObject(FILTER_DATA);
44+
if (filterData != null) {
45+
String searchText = extractSearchText(filterData);
46+
String filterColumns = extractFilterColumns(filterData);
47+
return Stream.of(searchText, filterColumns)
48+
.filter(StringUtils::isNotEmpty)
49+
.collect(Collectors.joining(DELIMITER));
50+
}
51+
JSONArray filterDataArray = jsonObject.optJSONArray(FILTER_DATA);
52+
if (filterDataArray != null) {
53+
return formatKeyValuePairs(filterDataArray);
54+
}
55+
return "";
56+
}
57+
58+
private String extractFilterColumns(JSONObject filterData) {
59+
JSONArray filterColumns = filterData.optJSONArray(FILTER_COLUMNS);
60+
if (filterColumns != null) {
61+
return formatKeyValuePairs(filterColumns);
62+
}
63+
return "";
64+
}
65+
66+
private String processFilterSource(JSONObject jsonObject) {
67+
String filterSource = jsonObject.optString(FILTER_SOURCE, "");
68+
if (!filterSource.isEmpty()) {
69+
return String.format(ENTRY_FORMAT, FILTER_SOURCE_LABEL, filterSource);
70+
}
71+
return "";
72+
}
73+
74+
private String extractSearchText(JSONObject jsonObject) {
75+
String searchText = jsonObject.optString(SEARCH_TEXT, "");
76+
if (!searchText.isEmpty()) {
77+
return String.format(ENTRY_FORMAT, SEARCH_TEXT_LABEL, searchText);
78+
}
79+
return "";
80+
}
81+
82+
private String formatKeyValuePairs(JSONArray filterColumns) {
83+
return IntStream.range(0, filterColumns.length())
84+
.mapToObj(index -> {
85+
JSONObject item = filterColumns.getJSONObject(index);
86+
String title = optString(item, TITLE);
87+
String value = StringUtils.unwrap(optString(item, VALUE), QUOTE);
88+
return String.format(ENTRY_FORMAT, title, value);
89+
})
90+
.collect(Collectors.joining(DELIMITER));
91+
}
92+
93+
private void appendCommaSeparated(StringBuilder resultBuilder, String part) {
94+
if (!part.isEmpty()) {
95+
if (resultBuilder.length() > 0) {
96+
resultBuilder.append(DELIMITER);
97+
}
98+
resultBuilder.append(part);
99+
}
100+
}
101+
102+
private String optString(JSONObject item, String key) {
103+
return item.optString(key, "");
104+
}
105+
}

src/main/java/org/ohdsi/webapi/service/dto/ConceptSetAnnotationDTO.java renamed to src/main/java/org/ohdsi/webapi/service/dto/SaveConceptSetAnnotationsRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.util.List;
44

5-
public class ConceptSetAnnotationDTO {
5+
public class SaveConceptSetAnnotationsRequest {
66

77
private List<AnnotationDTO> newAnnotation;
88

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
UPDATE ${ohdsiSchema}.sec_permission
2+
SET value = 'conceptset:annotation:*:delete', description = 'Delete Concept Set Annotation'
3+
WHERE value = 'conceptset:annotation:%s:delete';
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.ohdsi.webapi.service.annotations;
2+
3+
import org.json.JSONObject;
4+
import org.junit.Before;
5+
import org.junit.Test;
6+
7+
import static org.hamcrest.MatcherAssert.assertThat;
8+
import static org.hamcrest.Matchers.containsString;
9+
import static org.hamcrest.Matchers.is;
10+
import static org.hamcrest.Matchers.isEmptyString;
11+
import static org.hamcrest.Matchers.not;
12+
13+
public class SearchDataTransformerTest {
14+
15+
private SearchDataTransformer sut;
16+
17+
@Before
18+
public void setUp() {
19+
sut = new SearchDataTransformer();
20+
}
21+
22+
@Test
23+
public void shouldReturnEmptyStringWhenInputIsEmpty() {
24+
JSONObject emptyJson = new JSONObject();
25+
String transformed = sut.convertJsonToReadableFormat(emptyJson.toString());
26+
assertThat(transformed, isEmptyString());
27+
}
28+
29+
@Test
30+
public void shouldHandleSearchText() {
31+
String input = "{\"filterData\":{\"searchText\":\"testSearch\"}}";
32+
String result = sut.convertJsonToReadableFormat(input);
33+
assertThat(result, is("Search Text: \"testSearch\""));
34+
}
35+
36+
@Test
37+
public void shouldHandleFilterSource() {
38+
String input = "{\"filterSource\":\"Search\"}";
39+
String result = sut.convertJsonToReadableFormat(input);
40+
assertThat(result, is("Filter Source: \"Search\""));
41+
}
42+
43+
@Test
44+
public void shouldHandleFilterColumns() {
45+
String input = "{\"filterData\":{\"filterColumns\":[{\"title\":\"Domain\",\"value\":\"Drug\"}]} }";
46+
String result = sut.convertJsonToReadableFormat(input);
47+
assertThat(result, is("Domain: \"Drug\""));
48+
}
49+
50+
@Test
51+
public void shouldCombineFilterDataAndFilterSource() {
52+
String input = "{\"filterData\":{\"searchText\":\"testSearch\",\"filterColumns\":[{\"title\":\"Domain\",\"value\":\"Drug\"}]},\"filterSource\":\"Search\"}";
53+
String result = sut.convertJsonToReadableFormat(input);
54+
String expected = "Search Text: \"testSearch\", Domain: \"Drug\", Filter Source: \"Search\"";
55+
assertThat(result, is(expected));
56+
}
57+
58+
@Test
59+
public void shouldHandleMultipleFilterColumns() {
60+
String input = "{\"filterData\":{\"filterColumns\":[{\"title\":\"Domain\",\"value\":\"Drug\"},{\"title\":\"Class\",\"value\":\"Medication\"}]}}";
61+
String result = sut.convertJsonToReadableFormat(input);
62+
String expected = "Domain: \"Drug\", Class: \"Medication\"";
63+
assertThat(result, is(expected));
64+
}
65+
66+
@Test
67+
public void shouldIgnoreEmptyFilterColumnsAndSearchText() {
68+
String input = "{\"filterData\":{\"searchText\":\"\",\"filterColumns\":[]}, \"filterSource\":\"\"}";
69+
String result = sut.convertJsonToReadableFormat(input);
70+
assertThat(result, isEmptyString());
71+
}
72+
73+
@Test
74+
public void shouldHandleNullValuesGracefully() {
75+
String input = "{\"filterData\":{\"filterColumns\":[{\"title\":null,\"value\":null}], \"searchText\":null}, \"filterSource\":null}";
76+
String result = sut.convertJsonToReadableFormat(input);
77+
assertThat(result, not(containsString("null")));
78+
}
79+
}

0 commit comments

Comments
 (0)