diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/Resource.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/Resource.java index 3faf9ddc..b2cb4367 100644 --- a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/Resource.java +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/Resource.java @@ -1,12 +1,12 @@ package edu.harvard.dbmi.avillach.data.entity; import java.io.StringReader; +import java.util.Optional; import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonReader; -import javax.persistence.Column; -import javax.persistence.Entity; +import javax.persistence.*; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -18,6 +18,9 @@ public class Resource extends BaseEntity{ @Column(length = 8192) private String description; private String targetURL; + + + @Convert(converter = ResourcePathConverter.class) private String resourceRSPath; @Column(length = 8192) @@ -102,4 +105,31 @@ public String toString() { .add("metadata", metadataObj) .build().toString(); } + + /** + * This resource path converter allows resource paths to contain a reference to a specific stack that is + * imputed at runtime based an an environment parameter. This allows multiple stacks to share the same database. + * + * The ___target_stack___ token in any resource path value will be replaced with the TARGET_STACK environment variable + */ + @Converter + public static class ResourcePathConverter implements AttributeConverter { + + public ResourcePathConverter() { + } + + private static final Optional targetStack = Optional.ofNullable(System.getProperty("TARGET_STACK", null)); + + @Override + public String convertToDatabaseColumn(String attribute) { + return attribute; + } + + @Override + public String convertToEntityAttribute(String dbData) { + return targetStack + .map(stack -> dbData.replace("___target_stack___", stack)) + .orElse(dbData); + } + } } diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/QueryRepository.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/QueryRepository.java index 12c59bef..0c867ed0 100644 --- a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/QueryRepository.java +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/QueryRepository.java @@ -3,12 +3,25 @@ import edu.harvard.dbmi.avillach.data.entity.Query; import javax.enterprise.context.ApplicationScoped; +import javax.persistence.PersistenceException; import javax.transaction.Transactional; import java.util.UUID; @Transactional @ApplicationScoped -public class QueryRepository extends BaseRepository{ +public class QueryRepository extends BaseRepository { - protected QueryRepository() {super(Query.class);} + protected QueryRepository() { + super(Query.class); + } + + public Query getQueryUUIDFromCommonAreaUUID(UUID caID) { + String caIDRegex = "%commonAreaUUID\":\"" + caID + "\"%"; + String query = "SELECT * FROM query WHERE CONVERT(metadata USING utf8) LIKE ?"; + try { + return (Query) em().createNativeQuery(query, Query.class).setParameter(1, caIDRegex).getSingleResult(); + } catch (PersistenceException ignored) { + return null; + } + } } diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/ResourceRepository.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/ResourceRepository.java index 8bb400f4..f377c9d6 100644 --- a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/ResourceRepository.java +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/ResourceRepository.java @@ -15,5 +15,5 @@ protected ResourceRepository() { super(Resource.class); } - + } diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java index 95c07223..43356e1b 100644 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java @@ -25,7 +25,10 @@ public class PicSureWarInit { @Resource(mappedName = "java:global/token_introspection_token") private String token_introspection_token; - //to be able to pre modified + @Resource(mappedName = "java:global/defaultApplicationUUID") + private String default_application_uuid; + + // to be able to pre modified public static final ObjectMapper objectMapper = new ObjectMapper(); // check the example from Apache HttpClient official website: @@ -39,14 +42,9 @@ public class PicSureWarInit { static { HTTP_CLIENT_CONNECTION_MANAGER = new PoolingHttpClientConnectionManager(); HTTP_CLIENT_CONNECTION_MANAGER.setMaxTotal(100); - CLOSEABLE_HTTP_CLIENT = HttpClients - .custom() - .setConnectionManager(HTTP_CLIENT_CONNECTION_MANAGER) - .useSystemProperties() - .build(); + CLOSEABLE_HTTP_CLIENT = HttpClients.custom().setConnectionManager(HTTP_CLIENT_CONNECTION_MANAGER).useSystemProperties().build(); } - public String getToken_introspection_url() { return token_introspection_url; } @@ -54,4 +52,13 @@ public String getToken_introspection_url() { public String getToken_introspection_token() { return token_introspection_token; } + + /** + * This method is used to get the default application UUID. This value is either the open or auth hpds resource UUID. + * + * @return the default application UUID + */ + public String getDefaultApplicationUUID() { + return this.default_application_uuid; + } } diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureInfoService.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureInfoService.java index 922746f8..a64cf005 100644 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureInfoService.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureInfoService.java @@ -21,55 +21,55 @@ public class PicsureInfoService { - private final Logger logger = LoggerFactory.getLogger(PicsureQueryService.class); + private final Logger logger = LoggerFactory.getLogger(PicsureQueryService.class); - private final static ObjectMapper mapper = new ObjectMapper(); + private final static ObjectMapper mapper = new ObjectMapper(); - @Inject - ResourceRepository resourceRepo; + @Inject + ResourceRepository resourceRepo; - @Inject - ResourceWebClient resourceWebClient; + @Inject + ResourceWebClient resourceWebClient; - /** - * Retrieve resource info for a specific resource. - * - * @param resourceId - Resource UUID - * @param credentialsQueryRequest - Contains resource specific credentials map - * @return a {@link edu.harvard.dbmi.avillach.domain.ResourceInfo ResourceInfo} - */ - public ResourceInfo info(UUID resourceId, QueryRequest credentialsQueryRequest, HttpHeaders headers) { - Resource resource = resourceRepo.getById(resourceId); - if (resource == null){ - throw new ProtocolException(ProtocolException.RESOURCE_NOT_FOUND + resourceId.toString()); - } - if (resource.getResourceRSPath() == null){ - throw new ApplicationException(ApplicationException.MISSING_RESOURCE_PATH); - } - if (credentialsQueryRequest == null){ - credentialsQueryRequest = new GeneralQueryRequest(); - } - if (credentialsQueryRequest.getResourceCredentials() == null){ - credentialsQueryRequest.setResourceCredentials(new HashMap()); - } + /** + * Retrieve resource info for a specific resource. + * + * @param resourceId - Resource UUID + * @param credentialsQueryRequest - Contains resource specific credentials map + * @return a {@link edu.harvard.dbmi.avillach.domain.ResourceInfo ResourceInfo} + */ + public ResourceInfo info(UUID resourceId, QueryRequest credentialsQueryRequest, HttpHeaders headers) { + Resource resource = resourceRepo.getById(resourceId); + if (resource == null) { + throw new ProtocolException(ProtocolException.RESOURCE_NOT_FOUND + resourceId.toString()); + } + if (resource.getResourceRSPath() == null) { + throw new ApplicationException(ApplicationException.MISSING_RESOURCE_PATH); + } + if (credentialsQueryRequest == null) { + credentialsQueryRequest = new GeneralQueryRequest(); + } + if (credentialsQueryRequest.getResourceCredentials() == null) { + credentialsQueryRequest.setResourceCredentials(new HashMap()); + } - logger.info("path=/info/{resourceId}, resourceId={}, requestSource={}, credentialsQueryRequest={}", - resourceId, - Utilities.getRequestSourceFromHeader(headers), - Utilities.convertQueryRequestToString(mapper, credentialsQueryRequest) - ); + logger.info( + "path=/info/{resourceId}, resourceId={}, requestSource={}, credentialsQueryRequest={}", resourceId, + Utilities.getRequestSourceFromHeader(headers), Utilities.convertQueryRequestToString(mapper, credentialsQueryRequest) + ); - credentialsQueryRequest.getResourceCredentials().put(ResourceWebClient.BEARER_TOKEN_KEY, resource.getToken()); - return resourceWebClient.info(resource.getResourceRSPath(), credentialsQueryRequest); - } + credentialsQueryRequest.getResourceCredentials().put(ResourceWebClient.BEARER_TOKEN_KEY, resource.getToken()); + return resourceWebClient.info(resource.getResourceRSPath(), credentialsQueryRequest); + } - /** - * Retrieve a list of all available resources. - * - * @return List containing limited metadata about all available resources and ids. - */ - public Map resources(HttpHeaders headers) { - logger.info("path=/info/resources, requestSource={}", Utilities.getRequestSourceFromHeader(headers)); - return resourceRepo.list().stream().collect(Collectors.toMap(Resource::getUuid, Resource::getName)); - } + /** + * Retrieve a list of all available resources. + * + * @return List containing limited metadata about all available resources and ids. + */ + public Map resources(HttpHeaders headers) { + logger.info("path=/info/resources, requestSource={}", Utilities.getRequestSourceFromHeader(headers)); + return resourceRepo.list().stream().filter(resource -> !resource.getHidden()) + .collect(Collectors.toMap(Resource::getUuid, Resource::getName)); + } } diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureQueryService.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureQueryService.java index b0c851bc..ca93d6d8 100644 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureQueryService.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureQueryService.java @@ -241,6 +241,7 @@ public Response querySync(QueryRequest queryRequest, HttpHeaders headers) { */ public QueryStatus queryMetadata(UUID queryId, HttpHeaders headers) { Query query = queryRepo.getById(queryId); + query = query == null ? queryRepo.getQueryUUIDFromCommonAreaUUID(queryId) : query; if (query == null) { throw new ProtocolException(ProtocolException.QUERY_NOT_FOUND + queryId.toString()); } diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/SystemService.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/SystemService.java index 2d6328d9..a9c8fcb2 100755 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/SystemService.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/SystemService.java @@ -1,137 +1,159 @@ -package edu.harvard.dbmi.avillach.service; -import static edu.harvard.dbmi.avillach.util.Utilities.buildHttpClientContext; - -import java.io.IOException; -import java.util.List; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; - -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.util.EntityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import edu.harvard.dbmi.avillach.PicSureWarInit; -import edu.harvard.dbmi.avillach.data.entity.Resource; -import edu.harvard.dbmi.avillach.data.repository.ResourceRepository; -import edu.harvard.dbmi.avillach.domain.GeneralQueryRequest; -import edu.harvard.dbmi.avillach.domain.ResourceInfo; -import edu.harvard.dbmi.avillach.util.exception.ApplicationException; - -@Path("/system") -public class SystemService { - static int max_test_frequency = 60000; - - static final String RUNNING = "RUNNING"; - - static final String ONE_OR_MORE_COMPONENTS_DEGRADED = "ONE OR MORE COMPONENTS DEGRADED"; - - Logger logger = LoggerFactory.getLogger(SystemService.class); - - @Inject - PicSureWarInit picSureWarInit; - - String lastStatus = "UNTESTED"; - long lastStatusCheck = 0l; - - @Inject - ResourceRepository resourceRepo; - - String token_introspection_url; - String token_introspection_token; - - @PostConstruct - public void init() { - token_introspection_url = picSureWarInit.getToken_introspection_url(); - token_introspection_token = picSureWarInit.getToken_introspection_token(); - if(token_introspection_url == null || token_introspection_token == null) { - throw new RuntimeException( - "token_introspection_url and token_introspection_token not configured"); - } - } - - @GET - @Path("/status") - @Produces("text/plain") - public String status() { - // Because there is no auth on this service we limit actually performing the checking to 1 per minute to avoid DOS scenarios. - long timeOfRequest = System.currentTimeMillis(); - if(timeOfRequest-lastStatusCheck < max_test_frequency) { - return lastStatus; - }else { - lastStatusCheck = timeOfRequest; - try{ - List resourcesToTest = resourceRepo.list(); - if( resourcesToTest != null && // This proves the MySQL database is serving queries - !resourcesToTest.isEmpty() && // This proves at least one resources is configured - testPSAMAResponds() && // This proves we can perform token introspection - testResourcesRespond(resourcesToTest) ){ // This proves all resources are at least serving info requests. - lastStatus = RUNNING; - return lastStatus; - }else { - lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; - } - }catch(Exception e) { - e.printStackTrace(); - lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; - } - return lastStatus; - } - } - - private boolean testPSAMAResponds() throws UnsupportedOperationException, IOException { - CloseableHttpClient client = PicSureWarInit.CLOSEABLE_HTTP_CLIENT; - ObjectMapper json = PicSureWarInit.objectMapper; - - HttpPost post = new HttpPost(token_introspection_url); - post.setEntity(new StringEntity("{}")); - post.setHeader("Content-Type", "application/json"); - //Authorize into the token introspection endpoint - post.setHeader("Authorization", "Bearer " + token_introspection_token); - CloseableHttpResponse response = null; - try { - response = client.execute(post, buildHttpClientContext()); - if (response.getStatusLine().getStatusCode() != 200){ - logger.error("callTokenIntroEndpoint() error back from token intro host server [" - + token_introspection_url + "]: " + EntityUtils.toString(response.getEntity())); - throw new ApplicationException("Token Introspection host server return " + response.getStatusLine().getStatusCode() + - ". Please see the log"); - } - JsonNode responseContent = json.readTree(response.getEntity().getContent()); - if (!responseContent.get("active").asBoolean()){ - // This is actually the expected response as we did not send a token in the token_introspection_request. - return true; - } - - return true; - } finally { - try { - if (response != null) - response.close(); - } catch (IOException ex) { - logger.error("callTokenIntroEndpoint() IOExcpetion when closing http response: " + ex.getMessage()); - } - } - } - - private boolean testResourcesRespond(List resourcesToTest) { - for(Resource resource : resourcesToTest) { - ResourceInfo info = new ResourceWebClient().info(resource.getResourceRSPath(), new GeneralQueryRequest()); - if(info==null) { - return false; - } - } - return true; - } -} - +package edu.harvard.dbmi.avillach.service; + +import static edu.harvard.dbmi.avillach.util.Utilities.buildHttpClientContext; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import edu.harvard.dbmi.avillach.PicSureWarInit; +import edu.harvard.dbmi.avillach.data.entity.Resource; +import edu.harvard.dbmi.avillach.data.repository.ResourceRepository; +import edu.harvard.dbmi.avillach.domain.GeneralQueryRequest; +import edu.harvard.dbmi.avillach.domain.ResourceInfo; +import edu.harvard.dbmi.avillach.util.exception.ApplicationException; + +@Path("/system") +public class SystemService { + static int max_test_frequency = 60000; + + static final String RUNNING = "RUNNING"; + + static final String ONE_OR_MORE_COMPONENTS_DEGRADED = "ONE OR MORE COMPONENTS DEGRADED"; + + Logger logger = LoggerFactory.getLogger(SystemService.class); + + @Inject + PicSureWarInit picSureWarInit; + + String lastStatus = "UNTESTED"; + long lastStatusCheck = 0l; + + @Inject + ResourceRepository resourceRepo; + + String token_introspection_url; + String token_introspection_token; + + String defaultApplicationUUID; + + @PostConstruct + public void init() { + token_introspection_url = picSureWarInit.getToken_introspection_url(); + token_introspection_token = picSureWarInit.getToken_introspection_token(); + defaultApplicationUUID = picSureWarInit.getDefaultApplicationUUID(); + if (token_introspection_url == null || token_introspection_token == null) { + throw new RuntimeException("token_introspection_url and token_introspection_token not configured"); + } + } + + @GET + @Path("/status") + @Produces("text/plain") + public String status() { + // Because there is no auth on this service we limit actually performing the checking to 1 per minute to avoid DOS scenarios. + long timeOfRequest = System.currentTimeMillis(); + if (timeOfRequest - lastStatusCheck < max_test_frequency) { + return lastStatus; + } else { + lastStatusCheck = timeOfRequest; + try { + List resourcesToTest = resourceRepo.list(); + if (resourcesToTest == null || resourcesToTest.isEmpty()) { + lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; + return lastStatus; + } + + // convert the default application uuid to an uuid object + UUID defaultApplicationUUID = UUID.fromString(this.defaultApplicationUUID); + + // We need to remove open or auth HPDS from the resource list depending on the environment deployed. + // This because both are included in the database, but only one is actually deployed. + // if the name contains hpds and is not the default application uuid, remove it. + resourcesToTest.removeIf( + resource -> resource.getName().toLowerCase().contains("hpds") && !resource.getUuid().equals(defaultApplicationUUID) + ); + + // This proves the MySQL database is serving queries + // This proves at least one resources is configured + // This proves we can perform token introspection + if (testPSAMAResponds() && testResourcesRespond(resourcesToTest)) { // This proves all resources are at least serving info + // requests. + lastStatus = RUNNING; + return lastStatus; + } else { + lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; + } + } catch (Exception e) { + e.printStackTrace(); + lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; + } + return lastStatus; + } + } + + private boolean testPSAMAResponds() throws UnsupportedOperationException, IOException { + CloseableHttpClient client = PicSureWarInit.CLOSEABLE_HTTP_CLIENT; + ObjectMapper json = PicSureWarInit.objectMapper; + + HttpPost post = new HttpPost(token_introspection_url); + post.setEntity(new StringEntity("{}")); + post.setHeader("Content-Type", "application/json"); + // Authorize into the token introspection endpoint + post.setHeader("Authorization", "Bearer " + token_introspection_token); + CloseableHttpResponse response = null; + try { + response = client.execute(post, buildHttpClientContext()); + if (response.getStatusLine().getStatusCode() != 200) { + logger.error( + "callTokenIntroEndpoint() error back from token intro host server [" + token_introspection_url + "]: " + + EntityUtils.toString(response.getEntity()) + ); + throw new ApplicationException( + "Token Introspection host server return " + response.getStatusLine().getStatusCode() + ". Please see the log" + ); + } + JsonNode responseContent = json.readTree(response.getEntity().getContent()); + if (!responseContent.get("active").asBoolean()) { + // This is actually the expected response as we did not send a token in the token_introspection_request. + return true; + } + + return true; + } finally { + try { + if (response != null) response.close(); + } catch (IOException ex) { + logger.error("callTokenIntroEndpoint() IOExcpetion when closing http response: " + ex.getMessage()); + } + } + } + + private boolean testResourcesRespond(List resourcesToTest) { + for (Resource resource : resourcesToTest) { + ResourceInfo info = new ResourceWebClient().info(resource.getResourceRSPath(), new GeneralQueryRequest()); + if (info == null) { + return false; + } + } + return true; + } +} + diff --git a/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/DataProcessingService.java b/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/DataProcessingService.java index 1988aab4..845909e4 100644 --- a/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/DataProcessingService.java +++ b/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/DataProcessingService.java @@ -158,8 +158,8 @@ private static Map bucketData(Map originalMap) int numBins = calcNumBins(data); double min = data.keySet().stream().min(Double::compareTo).orElse(0.0); double max = data.keySet().stream().max(Double::compareTo).orElse(0.0); - - if ((min == 0.0 && max == 0.0) || numBins == 0) return new HashMap<>(); + // The min and max can both be 0, but we could still have a numBins of 1 if there are values in the data. + if (min == 0.0 && max == 0.0 && numBins == 0) return new HashMap<>(); int binSize = (int) Math.ceil((max - min) / numBins); diff --git a/pic-sure-resources/pic-sure-visualization-resource/src/test/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationUtilTests.java b/pic-sure-resources/pic-sure-visualization-resource/src/test/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationUtilTests.java new file mode 100644 index 00000000..78583922 --- /dev/null +++ b/pic-sure-resources/pic-sure-visualization-resource/src/test/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationUtilTests.java @@ -0,0 +1,94 @@ +package edu.harvard.hms.dbmi.avillach.resource.visualization.service; + +import edu.harvard.dbmi.avillach.util.VisualizationUtil; +import org.junit.Test; +import org.junit.jupiter.api.DisplayName; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class VisualizationUtilTests { + + @Test + @DisplayName("Test limitKeySize") + public void testLimitKeySizeUniqueness() { + Map axisMap = new HashMap<>(Map.of( + "Disease-Specific (Asthma, Allergy and Inflammation, PUB)", 1, + "Disease-Specific (Asthma, Allergy and Inflammation, PUB, NPU)", 1, + "Disease-Specific (Asthma, Allergy and Inflammation, NPU)", 1, + "Disease-Specific (Asthma, Allergy and Inflammation)", 1 + )); + + Map actual = VisualizationUtil.limitKeySize(axisMap); + + Map expected = Map.of( + "Disease-Specific (Asthma, Allergy an..., PUB)", 1, + "Disease-Specific (Asthma, Allergy an...ation)", 1, + "Disease-Specific (Asthma, Allergy an..., NPU)", 1, + "Disease-Specific (Asthma, Allergy a...B, NPU)", 1 + ); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Empty Map limitKeySize") + public void testEmptyMapLimitKeySize() { + Map axisMap = new HashMap<>(); + Map actual = VisualizationUtil.limitKeySize(axisMap); + Map expected = new HashMap<>(); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test null Map limitKeySize") + public void testNullMapLimitKeySize() { + Map axisMap = null; + // this should throw a NullPointerException + try { + VisualizationUtil.limitKeySize(axisMap); + } catch (IllegalArgumentException e) { + assertEquals("axisMap cannot be null", e.getMessage()); + } + } + + @Test + @DisplayName("Test with no long keys limitKeySize") + public void testNoLongKeysLimitKeySize() { + // Test with no long keys + Map axisMap = new HashMap<>(); + for (int i = 0; i < 10; i++) { + axisMap.put("key" + i, 1); + } + Map actual = VisualizationUtil.limitKeySize(axisMap); + Map expected = new HashMap<>(axisMap); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test with keys of greater than 45 characters and uniqueness is near middle limitKeySize") + public void testKeysOfGreaterLengthAndUniquenessNearMiddleLimitKeySize() { + Map axisMap = new HashMap<>(); + axisMap.put("Hello, this is a long key that is STRING1 greater than 45 characters and is unique", 1); + axisMap.put("Hello, this is a long key that is STRING2 greater than 45 characters and is unique", 1); + axisMap.put("Hello, this is a long key that is STRING3 greater than 45 characters and is unique", 1); + + Map actual = VisualizationUtil.limitKeySize(axisMap); + + // loop through the keys and check if they are less than 45 characters + for (String key : actual.keySet()) { + assertEquals(45, key.length()); + } + + Map expected = Map.of( + "Hello, this is a long key that is ST...unique", 1, + "Hello, this is a long key that is S... unique", 1, + "Hello, this is a long key that is ...s unique", 1 + ); + + assertEquals(expected, actual); + } + + +} diff --git a/pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java b/pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java index 8a7d99a7..ed32a921 100644 --- a/pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java +++ b/pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java @@ -69,25 +69,50 @@ public static Map doProcessResults(Map axisMap } /** - * Replaces long column names with shorter version. + * This method is used to limit the size of the keys in the axisMap to a maximum of 45 characters. If the key is longer + * than 45 characters, it will be shortened to 45 characters and the last 3 characters will be replaced with "...". + * If the shortened key is not unique, we will create a unique one + *

* - * @param axisMap - * @return + * @param axisMap - Map of the categories and their counts + * @return Map - Map of the categories and their counts with the keys limited to 45 characters */ - private static Map limitKeySize(Map axisMap) { - List toRemove = new ArrayList<>(); - Map toAdd = new HashMap<>(); - axisMap.keySet().forEach(key -> { - if (key.length() > MAX_X_LABEL_LINE_LENGTH) { - toRemove.add(key); - toAdd.put( - key.substring(0, MAX_X_LABEL_LINE_LENGTH - 3) + "...", - axisMap.get(key)); - } + public static Map limitKeySize(Map axisMap) { + if (axisMap == null) { + throw new IllegalArgumentException("axisMap cannot be null"); + } + + Map newAxisMap = new HashMap<>(); + HashSet keys = new HashSet<>(); + axisMap.forEach((key, value) -> { + String adjustedKey = key.length() < MAX_X_LABEL_LINE_LENGTH ? key : createAdjustedKey(axisMap, keys, key); + newAxisMap.put(adjustedKey, value); + keys.add(adjustedKey); }); - toRemove.forEach(key -> axisMap.remove(key)); - axisMap.putAll(toAdd); - return axisMap; + return newAxisMap; + } + + private static String createAdjustedKey(Map axisMap, HashSet keys, String key) { + String keyPrefix = key.substring(0, MAX_X_LABEL_LINE_LENGTH); + return isKeyPrefixInAxisMap(axisMap, keyPrefix) ? generateUniqueKey(keys, key) : appendEllipsis(keyPrefix); + } + + private static boolean isKeyPrefixInAxisMap(Map axisMap, String keyPrefix) { + return axisMap.keySet().stream().anyMatch(k -> k.startsWith(keyPrefix)); + } + + private static String generateUniqueKey(HashSet keys, String key) { + int countFromEnd = 6; + String proposedKey; + do { + proposedKey = String.format("%s...%s", key.substring(0, MAX_X_LABEL_LINE_LENGTH - 3 - countFromEnd), key.substring(key.length() - countFromEnd)); + countFromEnd++; + } while (keys.contains(proposedKey)); + return proposedKey; + } + + private static String appendEllipsis(String keyPrefixAdjusted) { + return String.format("%s...", keyPrefixAdjusted); } }