Skip to content

Commit 85e9a19

Browse files
Gcolon021Luke Sikina
and
Luke Sikina
committed
[ALS-7760] Replicate Old Search in new Data-Dictionary (#53)
**[CHORE] GH Actions Fix** - Fixed GitHub Actions to resolve workflow issues. **Testing Enhancements** - Added `@ActiveProfiles("test")` to testing classes to remove spam logs from `DataSourceVerifier` during unit tests. **JSON Parsing Refactor** - Created `JsonBlobParser` for improved JSON parsing. - Refactored `ConceptResultSetUtil` to use `JsonBlobParser` for clearer separation of concerns. **Code Cleanup** - Removed unused imports from `ConceptShell` and `ContinuousConcept` classes to improve code readability. **Legacy Search Feature** - Implemented a new legacy search feature including service, controller, and related model classes. - Updated `ConceptRepository` to support legacy search queries. - Added test cases to ensure functionality. **Testing Improvements** - Added unit tests for `LegacySearchQueryMapper` to validate JSON parsing and string replacement. - Introduced integration tests for `LegacySearchController` to verify search response correctness using a PostgreSQL container. **Initial Configurations** - Added application properties for database configuration and dashboard settings. - Introduced Docker commands for local development with sample weights configuration. - Included `weights.csv` and `dictonaryRequest.http` as sample data for testing API requests. **Search Query Refactor** - Created `LegacySearchRepository` to handle legacy search functionalities. - Moved query logic (`ALLOW_FILTERING_Q`) to `QueryUtility`. - Removed legacy search code from `ConceptRepository` for better separation of concerns. **Filter Processing Refactor** - Introduced `FilterProcessor` to centralize filter processing logic. - Enhanced methods in `MetadataResultSetUtil` (`getDescription`, `getParentName`, `getParentDisplay`) for better validation using `StringUtils`. **Repository Enhancements** - Added `LegacySearchRepositoryTest` to verify legacy search functionalities. - Refactored legacy search logic from `ConceptService` into `LegacySearchRepository`. - Cleaned up unused imports and methods for better maintainability. --- Co-authored-by: Luke Sikina <lucas.skina@gmail.com>
1 parent b830920 commit 85e9a19

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+828
-101
lines changed

dictionaryweights/README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## Docker commands for local development
2+
### Docker build
3+
```bash
4+
docker build --no-cache --build-arg SPRING_PROFILE=bdc-dev -t weights:latest .
5+
```
6+
7+
### Docker run
8+
You will need a local weights.csv file.
9+
```bash
10+
docker run --rm -t --name dictionary-weights --network=host -v ./weights.csv:/weights.csv weights:latest
11+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
spring.application.name=dictionaryweights
2+
spring.main.web-application-type=none
3+
4+
spring.datasource.url=jdbc:postgresql://localhost:5432/dictionary_db?currentSchema=dict
5+
spring.datasource.username=username
6+
spring.datasource.password=password
7+
spring.datasource.driver-class-name=org.postgresql.Driver
8+
9+
weights.filename=/weights.csv

dictionaryweights/weights.csv

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
concept_node.DISPLAY,2
2+
concept_node.CONCEPT_PATH,2
3+
dataset.FULL_NAME,1
4+
dataset.DESCRIPTION,1
5+
parent.DISPLAY,1
6+
grandparent.DISPLAY,1
7+
concept_node_meta_str,1

dictonaryReqeust.http

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# curl 'https://dev.picsure.biodatacatalyst.nhlbi.nih.gov/picsure/proxy/dictionary-api/concepts?page_number=1&page_size=1'
2+
# -H 'origin: https://dev.picsure.biodatacatalyst.nhlbi.nih.gov'
3+
# -H 'referer: https://dev.picsure.biodatacatalyst.nhlbi.nih.gov/'
4+
# --data-raw '{"facets":[],"search":"","consents":[]}'
5+
POST http://localhost:80/concepts?page_number=0&page_size=100
6+
Content-Type: application/json
7+
8+
{"facets":[],"search":"lipid triglyceride"}
9+
10+
###
11+
12+
POST http://localhost:80/search
13+
Content-Type: application/json
14+
15+
{"@type":"GeneralQueryRequest","resourceCredentials":{},"query":{"searchTerm":"breast","includedTags":[],"excludedTags":[],"returnTags":"true","offset":0,"limit":10000000},"resourceUUID":null}

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptRepository.java

+6-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import edu.harvard.dbmi.avillach.dictionary.concept.model.Concept;
44
import edu.harvard.dbmi.avillach.dictionary.filter.Filter;
55
import edu.harvard.dbmi.avillach.dictionary.filter.QueryParamPair;
6+
import edu.harvard.dbmi.avillach.dictionary.legacysearch.SearchResultRowMapper;
67
import edu.harvard.dbmi.avillach.dictionary.util.MapExtractor;
78
import org.springframework.beans.factory.annotation.Autowired;
89
import org.springframework.beans.factory.annotation.Value;
@@ -15,32 +16,19 @@
1516
import java.util.Map;
1617
import java.util.Optional;
1718

19+
import static edu.harvard.dbmi.avillach.dictionary.util.QueryUtility.ALLOW_FILTERING_Q;
20+
21+
1822
@Repository
1923
public class ConceptRepository {
2024

21-
private static final String ALLOW_FILTERING_Q = """
22-
WITH allow_filtering AS (
23-
SELECT
24-
concept_node.concept_node_id AS concept_node_id,
25-
(string_agg(concept_node_meta.value, ' ') NOT LIKE '%' || 'true' || '%') AS allowFiltering
26-
FROM
27-
concept_node
28-
JOIN concept_node_meta ON
29-
concept_node.concept_node_id = concept_node_meta.concept_node_id
30-
AND concept_node_meta.KEY IN (:disallowed_meta_keys)
31-
GROUP BY
32-
concept_node.concept_node_id
33-
)
34-
""";
35-
3625
private final NamedParameterJdbcTemplate template;
3726
private final ConceptRowMapper mapper;
3827
private final ConceptFilterQueryGenerator filterGen;
3928
private final ConceptMetaExtractor conceptMetaExtractor;
4029
private final ConceptResultSetExtractor conceptResultSetExtractor;
4130
private final List<String> disallowedMetaFields;
4231

43-
4432
@Autowired
4533
public ConceptRepository(
4634
NamedParameterJdbcTemplate template, ConceptRowMapper mapper, ConceptFilterQueryGenerator filterGen,
@@ -240,4 +228,6 @@ WITH RECURSIVE nodes AS (
240228
return Optional.ofNullable(template.query(sql, params, conceptResultSetExtractor));
241229

242230
}
231+
232+
243233
}

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptResultSetUtil.java

+11-51
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,43 @@
22

33
import edu.harvard.dbmi.avillach.dictionary.concept.model.CategoricalConcept;
44
import edu.harvard.dbmi.avillach.dictionary.concept.model.ContinuousConcept;
5-
import org.json.JSONArray;
6-
import org.json.JSONException;
5+
import edu.harvard.dbmi.avillach.dictionary.util.JsonBlobParser;
76
import org.slf4j.Logger;
87
import org.slf4j.LoggerFactory;
8+
import org.springframework.beans.factory.annotation.Autowired;
99
import org.springframework.stereotype.Component;
1010

11-
import java.math.BigDecimal;
12-
import java.math.BigInteger;
1311
import java.sql.ResultSet;
1412
import java.sql.SQLException;
15-
import java.util.ArrayList;
1613
import java.util.List;
17-
import java.util.stream.Collectors;
1814

1915
@Component
2016
public class ConceptResultSetUtil {
2117

2218
private static final Logger log = LoggerFactory.getLogger(ConceptResultSetUtil.class);
19+
private final JsonBlobParser jsonBlobParser;
20+
21+
@Autowired
22+
public ConceptResultSetUtil(JsonBlobParser jsonBlobParser) {
23+
this.jsonBlobParser = jsonBlobParser;
24+
}
2325

2426
public CategoricalConcept mapCategorical(ResultSet rs) throws SQLException {
2527
return new CategoricalConcept(
2628
rs.getString("concept_path"), rs.getString("name"), rs.getString("display"), rs.getString("dataset"),
27-
rs.getString("description"), rs.getString("values") == null ? List.of() : parseValues(rs.getString("values")),
29+
rs.getString("description"), rs.getString("values") == null ? List.of() : jsonBlobParser.parseValues(rs.getString("values")),
2830
rs.getBoolean("allowFiltering"), rs.getString("studyAcronym"), null, null
2931
);
3032
}
3133

3234
public ContinuousConcept mapContinuous(ResultSet rs) throws SQLException {
3335
return new ContinuousConcept(
3436
rs.getString("concept_path"), rs.getString("name"), rs.getString("display"), rs.getString("dataset"),
35-
rs.getString("description"), rs.getBoolean("allowFiltering"), parseMin(rs.getString("values")),
36-
parseMax(rs.getString("values")), rs.getString("studyAcronym"), null
37+
rs.getString("description"), rs.getBoolean("allowFiltering"), jsonBlobParser.parseMin(rs.getString("values")),
38+
jsonBlobParser.parseMax(rs.getString("values")), rs.getString("studyAcronym"), null
3739
);
3840
}
3941

40-
public List<String> parseValues(String valuesArr) {
41-
try {
42-
ArrayList<String> vals = new ArrayList<>();
43-
JSONArray arr = new JSONArray(valuesArr);
44-
for (int i = 0; i < arr.length(); i++) {
45-
vals.add(arr.getString(i));
46-
}
47-
return vals;
48-
} catch (JSONException ex) {
49-
return List.of();
50-
}
51-
}
5242

53-
public Float parseMin(String valuesArr) {
54-
return parseFromIndex(valuesArr, 0);
55-
}
5643

57-
private Float parseFromIndex(String valuesArr, int index) {
58-
try {
59-
JSONArray arr = new JSONArray(valuesArr);
60-
if (arr.length() != 2) {
61-
return 0F;
62-
}
63-
Object raw = arr.get(index);
64-
return switch (raw) {
65-
case Double d -> d.floatValue();
66-
case Integer i -> i.floatValue();
67-
case String s -> Double.valueOf(s).floatValue();
68-
case BigDecimal d -> d.floatValue();
69-
case BigInteger i -> i.floatValue();
70-
default -> 0f;
71-
};
72-
} catch (JSONException ex) {
73-
log.warn("Invalid json array for values: ", ex);
74-
return 0F;
75-
} catch (NumberFormatException ex) {
76-
log.warn("Valid json array but invalid val within: ", ex);
77-
return 0F;
78-
}
79-
}
80-
81-
public Float parseMax(String valuesArr) {
82-
return parseFromIndex(valuesArr, 1);
83-
}
8444
}

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/ConceptService.java

+1
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,5 @@ public Optional<Concept> conceptTree(String dataset, String conceptPath, int dep
7070
public Optional<Concept> conceptDetailWithoutAncestors(String dataset, String conceptPath) {
7171
return getConcept(dataset, conceptPath, false);
7272
}
73+
7374
}

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ConceptShell.java

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package edu.harvard.dbmi.avillach.dictionary.concept.model;
22

33
import edu.harvard.dbmi.avillach.dictionary.dataset.Dataset;
4-
import jakarta.annotation.Nullable;
54

65
import java.util.List;
76
import java.util.Map;

src/main/java/edu/harvard/dbmi/avillach/dictionary/concept/model/ContinuousConcept.java

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import edu.harvard.dbmi.avillach.dictionary.dataset.Dataset;
55
import jakarta.annotation.Nullable;
66

7-
import java.util.ArrayList;
87
import java.util.List;
98
import java.util.Map;
109
import java.util.Objects;

src/main/java/edu/harvard/dbmi/avillach/dictionary/datasource/DataSourceVerifier.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
55
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.context.annotation.Profile;
68
import org.springframework.context.event.ContextRefreshedEvent;
79
import org.springframework.context.event.EventListener;
8-
import org.springframework.stereotype.Service;
910

1011
import javax.sql.DataSource;
1112
import java.sql.Connection;
1213
import java.sql.SQLException;
1314

14-
@Service
15+
@Profile("!test")
16+
@Configuration
1517
public class DataSourceVerifier {
1618

1719
private static final Logger LOG = LoggerFactory.getLogger(DataSourceVerifier.class);
@@ -28,11 +30,10 @@ public void verifyDataSourceConnection() {
2830
try (Connection connection = dataSource.getConnection()) {
2931
if (connection != null) {
3032
LOG.info("Datasource connection verified successfully.");
31-
} else {
32-
LOG.info("Failed to obtain a connection from the datasource.");
3333
}
3434
} catch (SQLException e) {
35-
LOG.info("Error verifying datasource connection: {}", e.getMessage());
35+
LOG.info("Failed to obtain a connection from the datasource.");
36+
LOG.debug("Error verifying datasource connection: {}", e.getMessage());
3637
}
3738
}
3839

src/main/java/edu/harvard/dbmi/avillach/dictionary/facet/FilterPreProcessor.java

+14-21
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
package edu.harvard.dbmi.avillach.dictionary.facet;
22

33
import edu.harvard.dbmi.avillach.dictionary.filter.Filter;
4+
import edu.harvard.dbmi.avillach.dictionary.filter.FilterProcessor;
5+
import org.springframework.beans.factory.annotation.Autowired;
46
import org.springframework.core.MethodParameter;
57
import org.springframework.http.HttpInputMessage;
68
import org.springframework.http.converter.HttpMessageConverter;
7-
import org.springframework.util.StringUtils;
89
import org.springframework.web.bind.annotation.ControllerAdvice;
910
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
1011

1112
import java.io.IOException;
1213
import java.lang.reflect.Type;
13-
import java.util.ArrayList;
14-
import java.util.Comparator;
15-
import java.util.List;
16-
import java.util.function.Function;
1714

1815
@ControllerAdvice
1916
public class FilterPreProcessor implements RequestBodyAdvice {
17+
18+
private final FilterProcessor filterProcessor;
19+
20+
@Autowired
21+
public FilterPreProcessor(FilterProcessor filterProcessor) {
22+
this.filterProcessor = filterProcessor;
23+
}
24+
25+
2026
@Override
2127
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
2228
return true;
@@ -35,26 +41,13 @@ public Object afterBodyRead(
3541
Class<? extends HttpMessageConverter<?>> converterType
3642
) {
3743
if (body instanceof Filter filter) {
38-
List<Facet> newFacets = filter.facets();
39-
List<String> newConsents = filter.consents();
40-
if (filter.facets() != null) {
41-
newFacets = new ArrayList<>(filter.facets());
42-
newFacets.sort(Comparator.comparing(Facet::name));
43-
}
44-
if (filter.consents() != null) {
45-
newConsents = new ArrayList<>(newConsents);
46-
newConsents.sort(Comparator.comparing(Function.identity()));
47-
}
48-
filter = new Filter(newFacets, filter.search(), newConsents);
49-
50-
if (StringUtils.hasLength(filter.search())) {
51-
filter = new Filter(filter.facets(), filter.search().replaceAll("_", "/"), filter.consents());
52-
}
53-
return filter;
44+
return filterProcessor.processsFilter(filter);
5445
}
5546
return body;
5647
}
5748

49+
50+
5851
@Override
5952
public Object handleEmptyBody(
6053
Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package edu.harvard.dbmi.avillach.dictionary.filter;
2+
3+
import edu.harvard.dbmi.avillach.dictionary.facet.Facet;
4+
import org.springframework.stereotype.Component;
5+
import org.springframework.util.StringUtils;
6+
7+
import java.util.ArrayList;
8+
import java.util.Comparator;
9+
import java.util.List;
10+
import java.util.function.Function;
11+
12+
@Component
13+
public class FilterProcessor {
14+
15+
public Filter processsFilter(Filter filter) {
16+
List<Facet> newFacets = filter.facets();
17+
List<String> newConsents = filter.consents();
18+
if (filter.facets() != null) {
19+
newFacets = new ArrayList<>(filter.facets());
20+
newFacets.sort(Comparator.comparing(Facet::name));
21+
}
22+
if (filter.consents() != null) {
23+
newConsents = new ArrayList<>(newConsents);
24+
newConsents.sort(Comparator.comparing(Function.identity()));
25+
}
26+
filter = new Filter(newFacets, filter.search(), newConsents);
27+
28+
if (StringUtils.hasLength(filter.search())) {
29+
filter = new Filter(filter.facets(), filter.search().replaceAll("_", "/"), filter.consents());
30+
}
31+
return filter;
32+
}
33+
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package edu.harvard.dbmi.avillach.dictionary.legacysearch;
2+
3+
import edu.harvard.dbmi.avillach.dictionary.legacysearch.model.LegacyResponse;
4+
import edu.harvard.dbmi.avillach.dictionary.legacysearch.model.LegacySearchQuery;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.http.ResponseEntity;
7+
import org.springframework.stereotype.Controller;
8+
import org.springframework.web.bind.annotation.RequestBody;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
11+
import java.io.IOException;
12+
13+
@Controller
14+
public class LegacySearchController {
15+
16+
private final LegacySearchService legacySearchService;
17+
private final LegacySearchQueryMapper legacySearchQueryMapper;
18+
19+
@Autowired
20+
public LegacySearchController(LegacySearchService legacySearchService, LegacySearchQueryMapper legacySearchQueryMapper) {
21+
this.legacySearchService = legacySearchService;
22+
this.legacySearchQueryMapper = legacySearchQueryMapper;
23+
}
24+
25+
@RequestMapping(path = "/search")
26+
public ResponseEntity<LegacyResponse> legacySearch(@RequestBody String jsonString) throws IOException {
27+
LegacySearchQuery legacySearchQuery = legacySearchQueryMapper.mapFromJson(jsonString);
28+
return ResponseEntity
29+
.ok(new LegacyResponse(legacySearchService.getSearchResults(legacySearchQuery.filter(), legacySearchQuery.pageable())));
30+
}
31+
32+
}

0 commit comments

Comments
 (0)