Skip to content

Commit

Permalink
feat: #3 integrate with CKAN
Browse files Browse the repository at this point in the history
  • Loading branch information
brunopacheco1 committed Apr 5, 2024
1 parent 02f043b commit cf69e13
Show file tree
Hide file tree
Showing 17 changed files with 2,609 additions and 24 deletions.
2 changes: 1 addition & 1 deletion _http/ckan.http
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: Apache-2.0

GET https://ckan-test.healthdata.nl/api/3/action/package_search?facet.field=["organization","theme","conforms_to","has_version","access_rights","language","publisher_name","res_format","provenance"]&rows=1&fq=id:e1b3eff9-13eb-48b0-b180-7ecb76b84454
GET https://ckan-test.healthdata.nl/api/3/action/package_search?fq=&sort=score+desc%2C+metadata_modified+desc&rows=10&start=0&facet.field=%5B%22organization%22%2C%22theme%22%2C%22conforms_to%22%2C%22has_version%22%2C%22access_rights%22%2C%22language%22%2C%22publisher_name%22%2C%22res_format%22%2C%22provenance%22%5D

###
GET https://ckan-test.healthdata.nl/api/3/action/package_show?id=e1b3eff9-13eb-48b0-b180-7ecb76b84454
26 changes: 26 additions & 0 deletions _http/discovery.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2024 PNED G.I.E.
#
# SPDX-License-Identifier: Apache-2.0

POST http://localhost:8080/api/v1/datasets/search
Content-Type: application/json

{
"query": "COVID",
"facets": [{
"facetGroup": "ckan",
"facet": "theme",
"value": "http://purl.bioontology.org/ontology/ICD10CM/U07.1"
}, {
"facetGroup": "ckan",
"facet": "theme",
"value": "http://purl.org/zonmw/covid19/10003"
}, {
"facetGroup": "ckan",
"facet": "tags",
"value": "COVID-19"
}]
}

###
GET http://localhost:8080/api/v1/datasets/dummy

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.github.genomicdatainfrastructure.discovery.api;

import io.github.genomicdatainfrastructure.discovery.model.DatasetSearchQuery;
import io.github.genomicdatainfrastructure.discovery.model.DatasetsSearchResponse;
import io.github.genomicdatainfrastructure.discovery.model.RetrievedDataset;
import io.github.genomicdatainfrastructure.discovery.services.DatasetsSearchService;
import io.quarkus.oidc.runtime.OidcJwtCallerPrincipal;
import io.quarkus.security.identity.SecurityIdentity;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class DatasetQueryApiImpl implements DatasetQueryApi {

private final SecurityIdentity identity;
private final DatasetsSearchService datasetsSearchService;

@Override
public DatasetsSearchResponse datasetSearch(DatasetSearchQuery datasetSearchQuery) {
return datasetsSearchService.search(accessToken(), datasetSearchQuery);
}

@Override
public RetrievedDataset retrieveDataset(String id) {
return RetrievedDataset.builder()
.build();
}

private String accessToken() {
if (identity.isAnonymous()) {
return null;
}
var principal = (OidcJwtCallerPrincipal) identity.getPrincipal();
return principal.getRawToken();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2024 PNED G.I.E.
//
// SPDX-License-Identifier: Apache-2.0

package io.github.genomicdatainfrastructure.discovery.api;

import static jakarta.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;

import io.github.genomicdatainfrastructure.discovery.model.ErrorResponse;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import lombok.extern.java.Log;
import java.util.logging.Level;

@Log
@Provider
public class GlobalExceptionMapper implements ExceptionMapper<Exception> {

@Override
public Response toResponse(Exception exception) {
log.log(Level.SEVERE, exception, exception::getMessage);

var errorResponse = ErrorResponse.builder()
.title("Not expected exception")
.status(INTERNAL_SERVER_ERROR.getStatusCode())
.detail(exception.getMessage())
.build();

return Response
.status(INTERNAL_SERVER_ERROR)
.entity(errorResponse)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.github.genomicdatainfrastructure.discovery.services;

import java.util.List;
import java.util.Objects;

import io.github.genomicdatainfrastructure.discovery.model.DatasetSearchQueryFacet;

import static io.quarkus.runtime.util.StringUtil.isNullOrEmpty;
import static java.util.Optional.ofNullable;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;

public class CkanFacetsQueryBuilder {

private static final String CKAN_FACET_GROUP = "ckan";
private static final String QUOTED_VALUE = "\"%s\"";
private static final String FACET_PATTERN = "%s:(%s)";
private static final String AND = " AND ";

private CkanFacetsQueryBuilder() {
// utility class
}

public static String buildFacetQuery(List<DatasetSearchQueryFacet> facets) {
var nonNullFacets = ofNullable(facets)
.orElseGet(List::of)
.stream()
.filter(CkanFacetsQueryBuilder::isCkanGroupAndValueIsNotBlank)
.collect(groupingBy(DatasetSearchQueryFacet::getFacet));

return nonNullFacets.entrySet().stream()
.map(entry -> getFacetQuery(entry.getKey(), entry.getValue()))
.collect(joining(AND));
}

private static Boolean isCkanGroupAndValueIsNotBlank(DatasetSearchQueryFacet facet) {
return Objects.equals(CKAN_FACET_GROUP, facet.getFacetGroup()) &&
!isNullOrEmpty(facet.getValue());
}

private static String getFacetQuery(String key, List<DatasetSearchQueryFacet> facets) {
var values = facets.stream()
.map(facet -> QUOTED_VALUE.formatted(facet.getValue()))
.collect(joining(AND));

return FACET_PATTERN.formatted(key, values);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.github.genomicdatainfrastructure.discovery.services;

import org.eclipse.microprofile.rest.client.inject.RestClient;

import io.github.genomicdatainfrastructure.discovery.model.DatasetSearchQuery;
import io.github.genomicdatainfrastructure.discovery.model.DatasetsSearchResponse;
import io.github.genomicdatainfrastructure.discovery.remote.ckan.api.CkanQueryApi;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class DatasetsSearchService {

private static final String FACETS_QUERY = "[\"access_rights\",\"theme\",\"tags\",\"spatial_uri\",\"organization\",\"publisher_name\",\"res_format\"]";

private final CkanQueryApi ckanQueryApi;

@Inject
public DatasetsSearchService(
@RestClient CkanQueryApi ckanQueryApi
) {
this.ckanQueryApi = ckanQueryApi;
}

public DatasetsSearchResponse search(String accessToken, DatasetSearchQuery query) {
var response = ckanQueryApi.packageSearch(
query.getQuery(),
CkanFacetsQueryBuilder.buildFacetQuery(query.getFacets()),
query.getSort(),
query.getRows(),
query.getStart(),
FACETS_QUERY,
accessToken
);
return PackagesSearchResponseMapper.from(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package io.github.genomicdatainfrastructure.discovery.services;

import io.github.genomicdatainfrastructure.discovery.model.DatasetsSearchResponse;
import io.github.genomicdatainfrastructure.discovery.model.Facet;
import io.github.genomicdatainfrastructure.discovery.model.FacetGroup;
import io.github.genomicdatainfrastructure.discovery.model.SearchedDataset;
import io.github.genomicdatainfrastructure.discovery.model.ValueLabel;
import io.github.genomicdatainfrastructure.discovery.remote.ckan.model.CkanFacet;
import io.github.genomicdatainfrastructure.discovery.remote.ckan.model.CkanOrganization;
import io.github.genomicdatainfrastructure.discovery.remote.ckan.model.CkanPackage;
import io.github.genomicdatainfrastructure.discovery.remote.ckan.model.PackagesSearchResponse;
import io.github.genomicdatainfrastructure.discovery.remote.ckan.model.PackagesSearchResult;
import java.util.List;
import java.util.Map;
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;

import static java.util.Optional.ofNullable;

public class PackagesSearchResponseMapper {

private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(
"yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
);

private PackagesSearchResponseMapper() {
// Utility class
}

public static DatasetsSearchResponse from(PackagesSearchResponse response) {
return DatasetsSearchResponse.builder()
.count(count(response.getResult()))
.facetGroups(facetGroups(response.getResult()))
.results(results(response.getResult()))
.build();
}

private static Integer count(PackagesSearchResult result) {
return ofNullable(result)
.map(PackagesSearchResult::getCount)
.orElse(null);
}

private static List<FacetGroup> facetGroups(PackagesSearchResult result) {
return ofNullable(result)
.map(PackagesSearchResult::getSearchFacets)
.map(PackagesSearchResponseMapper::facetGroup)
.map(List::of)
.orElseGet(List::of);
}

private static FacetGroup facetGroup(Map<String, CkanFacet> facets) {
return FacetGroup.builder()
.key("ckan")
.label("Metadata")
.facets(facets.entrySet().stream()
.map(PackagesSearchResponseMapper::facet)
.toList())
.build();
}

private static Facet facet(Map.Entry<String, CkanFacet> entry) {
var key = entry.getKey();
var facet = entry.getValue();
var values = ofNullable(facet.getItems())
.orElseGet(List::of)
.stream().map(value -> ValueLabel.builder()
.value(value.getName())
.label(value.getDisplayName())
.build())
.toList();

return Facet.builder()
.key(key)
.label(facet.getTitle())
.values(values)
.build();
}

private static List<SearchedDataset> results(PackagesSearchResult result) {
var nonNullDatasets = ofNullable(result)
.map(PackagesSearchResult::getResults)
.orElseGet(List::of);

return nonNullDatasets.stream()
.map(PackagesSearchResponseMapper::result)
.toList();
}

private static SearchedDataset result(CkanPackage dataset) {
var catalogue = ofNullable(dataset.getOrganization())
.map(CkanOrganization::getTitle)
.orElse(null);

return SearchedDataset.builder()
.id(dataset.getId())
.identifier(dataset.getIdentifier())
.title(dataset.getName())
.description(dataset.getNotes())
.themes(values(dataset.getTheme()))
.catalogue(catalogue)
.modifiedAt(parse(dataset.getMetadataModified()))
.build();
}

private static LocalDateTime parse(String date) {
return ofNullable(date)
.map(it -> LocalDateTime.parse(it, DATE_FORMATTER))
.orElse(null);
}

private static List<ValueLabel> values(List<String> values) {
return ofNullable(values)
.orElseGet(List::of)
.stream()
.map(it -> ValueLabel.builder()
.value(it)
.label(it)
.build())
.toList();
}
}
15 changes: 9 additions & 6 deletions src/main/openapi/ckan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,19 @@ paths:
components:
schemas:
PackagesSearchResponse:
type: object
properties:
help:
type: string
success:
type: boolean
result:
$ref: "#/components/schemas/PackagesSearchResult"
PackagesSearchResult:
type: object
properties:
count:
type: integer
description: The number of results found
results:
type: array
items:
Expand All @@ -115,7 +123,6 @@ components:
type: object
additionalProperties:
$ref: "#/components/schemas/CkanFacet"
description: Aggregated information about facet
CkanFacet:
type: object
properties:
Expand Down Expand Up @@ -159,10 +166,8 @@ components:
$ref: "#/components/schemas/CkanOrganization"
metadata_created:
type: string
format: date-time
metadata_modified:
type: string
format: date-time
url:
type: string
language:
Expand Down Expand Up @@ -220,10 +225,8 @@ components:
type: string
created:
type: string
format: date-time
last_modified:
type: string
format: date-time
required:
- id
- name
Expand Down
Loading

0 comments on commit cf69e13

Please sign in to comment.