From 38ce37acb3243ed3d6054f07353f994c729f2f44 Mon Sep 17 00:00:00 2001 From: Thomas Schaffter Date: Thu, 1 Aug 2024 13:29:13 -0700 Subject: [PATCH] feat(openchallenges): fetch data in JSON-LD format from the challenge service (#2750) --- .../.openapi-generator/FILES | 1 + .../challenge/service/api/ChallengeApi.java | 15 +- .../service/api/ChallengeApiDelegate.java | 9 +- .../service/api/ChallengeApiDelegateImpl.java | 39 +- .../service/model/dto/ChallengeJsonLdDto.java | 754 ++++++++++++++++++ .../model/mapper/ChallengeJsonLdMapper.java | 62 ++ .../service/service/ChallengeService.java | 30 +- .../src/main/resources/openapi.yaml | 26 + .../src/lib/.openapi-generator/FILES | 2 + .../src/lib/api/challenge.service.ts | 10 +- .../src/lib/model/challengeJsonLd.ts | 87 ++ .../src/lib/model/challengeJsonLdAllOf.ts | 19 + .../src/lib/model/models.ts | 2 + .../build/challenge.openapi.yaml | 23 + .../api-description/build/openapi.yaml | 23 + .../components/schemas/ChallengeJsonLd.yaml | 19 + .../src/paths/challenges/@{challengeId}.yaml | 3 + .../challenge/src/lib/challenge.component.ts | 12 +- 18 files changed, 1111 insertions(+), 25 deletions(-) create mode 100644 apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengeJsonLdDto.java create mode 100644 apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/mapper/ChallengeJsonLdMapper.java create mode 100644 libs/openchallenges/api-client-angular/src/lib/model/challengeJsonLd.ts create mode 100644 libs/openchallenges/api-client-angular/src/lib/model/challengeJsonLdAllOf.ts create mode 100644 libs/openchallenges/api-description/src/components/schemas/ChallengeJsonLd.yaml diff --git a/apps/openchallenges/challenge-service/.openapi-generator/FILES b/apps/openchallenges/challenge-service/.openapi-generator/FILES index 0541d3df4a..27561ae32f 100644 --- a/apps/openchallenges/challenge-service/.openapi-generator/FILES +++ b/apps/openchallenges/challenge-service/.openapi-generator/FILES @@ -27,6 +27,7 @@ src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/Cha src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengeDirectionDto.java src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengeDto.java src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengeIncentiveDto.java +src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengeJsonLdDto.java src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengePlatformDirectionDto.java src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengePlatformDto.java src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengePlatformSearchQueryDto.java diff --git a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApi.java b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApi.java index dbabbc283c..4fcd4f9c85 100644 --- a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApi.java +++ b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApi.java @@ -15,6 +15,7 @@ import javax.validation.constraints.*; import org.sagebionetworks.openchallenges.challenge.service.model.dto.BasicErrorDto; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeDto; +import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeJsonLdDto; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeSearchQueryDto; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengesPageDto; import org.springframework.http.ResponseEntity; @@ -50,8 +51,8 @@ default ChallengeApiDelegate getDelegate() { mediaType = "application/json", schema = @Schema(implementation = ChallengeDto.class)), @Content( - mediaType = "application/problem+json", - schema = @Schema(implementation = ChallengeDto.class)) + mediaType = "application/ld+json", + schema = @Schema(implementation = ChallengeJsonLdDto.class)), }), @ApiResponse( responseCode = "404", @@ -60,6 +61,9 @@ default ChallengeApiDelegate getDelegate() { @Content( mediaType = "application/json", schema = @Schema(implementation = BasicErrorDto.class)), + @Content( + mediaType = "application/ld+json", + schema = @Schema(implementation = BasicErrorDto.class)), @Content( mediaType = "application/problem+json", schema = @Schema(implementation = BasicErrorDto.class)) @@ -71,6 +75,9 @@ default ChallengeApiDelegate getDelegate() { @Content( mediaType = "application/json", schema = @Schema(implementation = BasicErrorDto.class)), + @Content( + mediaType = "application/ld+json", + schema = @Schema(implementation = BasicErrorDto.class)), @Content( mediaType = "application/problem+json", schema = @Schema(implementation = BasicErrorDto.class)) @@ -79,8 +86,8 @@ default ChallengeApiDelegate getDelegate() { @RequestMapping( method = RequestMethod.GET, value = "/challenges/{challengeId}", - produces = {"application/json", "application/problem+json"}) - default ResponseEntity getChallenge( + produces = {"application/json", "application/ld+json", "application/problem+json"}) + default ResponseEntity getChallenge( @Parameter( name = "challengeId", description = "The unique identifier of the challenge.", diff --git a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApiDelegate.java b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApiDelegate.java index 8ec9197661..1d921c1ff8 100644 --- a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApiDelegate.java +++ b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApiDelegate.java @@ -2,7 +2,6 @@ import java.util.Optional; import javax.annotation.Generated; -import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeDto; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeSearchQueryDto; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengesPageDto; import org.springframework.http.HttpStatus; @@ -29,7 +28,7 @@ default Optional getRequest() { * or The request cannot be fulfilled due to an unexpected server error (status code 500) * @see ChallengeApi#getChallenge */ - default ResponseEntity getChallenge(Long challengeId) { + default ResponseEntity getChallenge(Long challengeId) { getRequest() .ifPresent( request -> { @@ -40,6 +39,12 @@ default ResponseEntity getChallenge(Long challengeId) { ApiUtil.setExampleResponse(request, "application/json", exampleString); break; } + if (mediaType.isCompatibleWith(MediaType.valueOf("application/ld+json"))) { + String exampleString = + "Custom MIME type example not yet supported: application/ld+json"; + ApiUtil.setExampleResponse(request, "application/ld+json", exampleString); + break; + } if (mediaType.isCompatibleWith(MediaType.valueOf("application/problem+json"))) { String exampleString = "Custom MIME type example not yet supported: application/problem+json"; diff --git a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApiDelegateImpl.java b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApiDelegateImpl.java index 5ca81659d5..77b615f0bb 100644 --- a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApiDelegateImpl.java +++ b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/api/ChallengeApiDelegateImpl.java @@ -1,23 +1,50 @@ package org.sagebionetworks.openchallenges.challenge.service.api; -import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeDto; +import java.util.List; +import java.util.Optional; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeSearchQueryDto; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengesPageDto; import org.sagebionetworks.openchallenges.challenge.service.service.ChallengeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.web.context.request.NativeWebRequest; @Component public class ChallengeApiDelegateImpl implements ChallengeApiDelegate { + private static final Logger LOGGER = LoggerFactory.getLogger(ChallengeApiDelegateImpl.class); + + private static final MediaType APPLICATION_LD_JSON = MediaType.valueOf("application/ld+json"); + private static final MediaType APPLICATION_JSON = MediaType.valueOf("application/json"); + private final ChallengeService challengeService; - public ChallengeApiDelegateImpl(ChallengeService challengeService) { + private final NativeWebRequest request; + + public ChallengeApiDelegateImpl(ChallengeService challengeService, NativeWebRequest request) { this.challengeService = challengeService; + this.request = request; + } + + @Override + public Optional getRequest() { + return Optional.ofNullable(request); } @Override - public ResponseEntity getChallenge(Long challengeId) { + public ResponseEntity getChallenge(Long challengeId) { + for (MediaType mediaType : getAcceptedMediaTypes(getRequest())) { + if (mediaType.isCompatibleWith(APPLICATION_LD_JSON)) { + return ResponseEntity.ok(challengeService.getChallengeJsonLd(challengeId)); + } + if (mediaType.isCompatibleWith(APPLICATION_JSON)) { + return ResponseEntity.ok(challengeService.getChallenge(challengeId)); + } + } + // TODO return an error object if this API does not support any of the accepted types return ResponseEntity.ok(challengeService.getChallenge(challengeId)); } @@ -25,4 +52,10 @@ public ResponseEntity getChallenge(Long challengeId) { public ResponseEntity listChallenges(ChallengeSearchQueryDto query) { return ResponseEntity.ok(challengeService.listChallenges(query)); } + + public List getAcceptedMediaTypes(Optional requestOpt) { + return requestOpt + .map(request -> MediaType.parseMediaTypes(request.getHeader("Accept"))) + .orElseGet(List::of); + } } diff --git a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengeJsonLdDto.java b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengeJsonLdDto.java new file mode 100644 index 0000000000..3e09e460ea --- /dev/null +++ b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/dto/ChallengeJsonLdDto.java @@ -0,0 +1,754 @@ +package org.sagebionetworks.openchallenges.challenge.service.model.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.annotation.Generated; +import javax.validation.Valid; +import javax.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; + +/** A challenge */ +@Schema(name = "ChallengeJsonLd", description = "A challenge") +@JsonTypeName("ChallengeJsonLd") +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen") +// TODO Add x-java-class-annotations +public class ChallengeJsonLdDto { + + @JsonProperty("id") + private Long id; + + @JsonProperty("slug") + private String slug; + + @JsonProperty("name") + private String name; + + @JsonProperty("headline") + private String headline = null; + + @JsonProperty("description") + private String description; + + @JsonProperty("doi") + private String doi = null; + + @JsonProperty("status") + private ChallengeStatusDto status; + + @JsonProperty("platform") + private SimpleChallengePlatformDto platform = null; + + @JsonProperty("websiteUrl") + private String websiteUrl = null; + + @JsonProperty("avatarUrl") + private String avatarUrl = null; + + @JsonProperty("incentives") + @Valid + private List incentives = new ArrayList<>(); + + @JsonProperty("submissionTypes") + @Valid + private List submissionTypes = new ArrayList<>(); + + @JsonProperty("inputDataTypes") + @Valid + private List inputDataTypes = null; + + @JsonProperty("categories") + @Valid + private List categories = new ArrayList<>(); + + @JsonProperty("startDate") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + private LocalDate startDate = null; + + @JsonProperty("endDate") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) + private LocalDate endDate = null; + + @JsonProperty("starredCount") + private Integer starredCount = 0; + + @JsonProperty("operation") + private EdamConceptDto operation = null; + + @JsonProperty("createdAt") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private OffsetDateTime createdAt; + + @JsonProperty("updatedAt") + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private OffsetDateTime updatedAt; + + @JsonProperty("@context") + private String atContext; + + @JsonProperty("@id") + private String atId; + + @JsonProperty("@type") + private String atType; + + public ChallengeJsonLdDto id(Long id) { + this.id = id; + return this; + } + + /** + * The unique identifier of the challenge. + * + * @return id + */ + @NotNull + @Schema( + name = "id", + example = "1", + description = "The unique identifier of the challenge.", + required = true) + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ChallengeJsonLdDto slug(String slug) { + this.slug = slug; + return this; + } + + /** + * The unique slug of the challenge. + * + * @return slug + */ + @NotNull + @Pattern(regexp = "^[a-z0-9]+(?:-[a-z0-9]+)*$") + @Size(min = 3, max = 255) + @Schema( + name = "slug", + example = "awesome-challenge", + description = "The unique slug of the challenge.", + required = true) + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } + + public ChallengeJsonLdDto name(String name) { + this.name = name; + return this; + } + + /** + * The name of the challenge. + * + * @return name + */ + @NotNull + @Size(min = 3, max = 255) + @Schema(name = "name", description = "The name of the challenge.", required = true) + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ChallengeJsonLdDto headline(String headline) { + this.headline = headline; + return this; + } + + /** + * The headline of the challenge. + * + * @return headline + */ + @Size(min = 0, max = 80) + @Schema( + name = "headline", + example = "Example challenge headline", + description = "The headline of the challenge.", + required = false) + public String getHeadline() { + return headline; + } + + public void setHeadline(String headline) { + this.headline = headline; + } + + public ChallengeJsonLdDto description(String description) { + this.description = description; + return this; + } + + /** + * The description of the challenge. + * + * @return description + */ + @NotNull + @Size(min = 0, max = 1000) + @Schema( + name = "description", + example = "This is an example description of the challenge.", + description = "The description of the challenge.", + required = true) + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public ChallengeJsonLdDto doi(String doi) { + this.doi = doi; + return this; + } + + /** + * The DOI of the challenge. + * + * @return doi + */ + @Size(max = 120) + @Schema( + name = "doi", + example = "https://doi.org/123/abc", + description = "The DOI of the challenge.", + required = false) + public String getDoi() { + return doi; + } + + public void setDoi(String doi) { + this.doi = doi; + } + + public ChallengeJsonLdDto status(ChallengeStatusDto status) { + this.status = status; + return this; + } + + /** + * Get status + * + * @return status + */ + @NotNull + @Valid + @Schema(name = "status", required = true) + public ChallengeStatusDto getStatus() { + return status; + } + + public void setStatus(ChallengeStatusDto status) { + this.status = status; + } + + public ChallengeJsonLdDto platform(SimpleChallengePlatformDto platform) { + this.platform = platform; + return this; + } + + /** + * Get platform + * + * @return platform + */ + @Valid + @Schema(name = "platform", required = false) + public SimpleChallengePlatformDto getPlatform() { + return platform; + } + + public void setPlatform(SimpleChallengePlatformDto platform) { + this.platform = platform; + } + + public ChallengeJsonLdDto websiteUrl(String websiteUrl) { + this.websiteUrl = websiteUrl; + return this; + } + + /** + * A URL to the website or image. + * + * @return websiteUrl + */ + @Size(max = 500) + @Schema( + name = "websiteUrl", + example = "https://openchallenges.io", + description = "A URL to the website or image.", + required = false) + public String getWebsiteUrl() { + return websiteUrl; + } + + public void setWebsiteUrl(String websiteUrl) { + this.websiteUrl = websiteUrl; + } + + public ChallengeJsonLdDto avatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + return this; + } + + /** + * A URL to the website or image. + * + * @return avatarUrl + */ + @Size(max = 500) + @Schema( + name = "avatarUrl", + example = "https://openchallenges.io", + description = "A URL to the website or image.", + required = false) + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public ChallengeJsonLdDto incentives(List incentives) { + this.incentives = incentives; + return this; + } + + public ChallengeJsonLdDto addIncentivesItem(ChallengeIncentiveDto incentivesItem) { + if (this.incentives == null) { + this.incentives = new ArrayList<>(); + } + this.incentives.add(incentivesItem); + return this; + } + + /** + * Get incentives + * + * @return incentives + */ + @NotNull + @Valid + @Schema(name = "incentives", required = true) + public List getIncentives() { + return incentives; + } + + public void setIncentives(List incentives) { + this.incentives = incentives; + } + + public ChallengeJsonLdDto submissionTypes(List submissionTypes) { + this.submissionTypes = submissionTypes; + return this; + } + + public ChallengeJsonLdDto addSubmissionTypesItem(ChallengeSubmissionTypeDto submissionTypesItem) { + if (this.submissionTypes == null) { + this.submissionTypes = new ArrayList<>(); + } + this.submissionTypes.add(submissionTypesItem); + return this; + } + + /** + * Get submissionTypes + * + * @return submissionTypes + */ + @NotNull + @Valid + @Schema(name = "submissionTypes", required = true) + public List getSubmissionTypes() { + return submissionTypes; + } + + public void setSubmissionTypes(List submissionTypes) { + this.submissionTypes = submissionTypes; + } + + public ChallengeJsonLdDto inputDataTypes(List inputDataTypes) { + this.inputDataTypes = inputDataTypes; + return this; + } + + public ChallengeJsonLdDto addInputDataTypesItem(EdamConceptDto inputDataTypesItem) { + if (this.inputDataTypes == null) { + this.inputDataTypes = new ArrayList<>(); + } + this.inputDataTypes.add(inputDataTypesItem); + return this; + } + + /** + * Get inputDataTypes + * + * @return inputDataTypes + */ + @Valid + @Schema(name = "inputDataTypes", required = false) + public List getInputDataTypes() { + return inputDataTypes; + } + + public void setInputDataTypes(List inputDataTypes) { + this.inputDataTypes = inputDataTypes; + } + + public ChallengeJsonLdDto categories(List categories) { + this.categories = categories; + return this; + } + + public ChallengeJsonLdDto addCategoriesItem(ChallengeCategoryDto categoriesItem) { + if (this.categories == null) { + this.categories = new ArrayList<>(); + } + this.categories.add(categoriesItem); + return this; + } + + /** + * Get categories + * + * @return categories + */ + @NotNull + @Valid + @Schema(name = "categories", required = true) + public List getCategories() { + return categories; + } + + public void setCategories(List categories) { + this.categories = categories; + } + + public ChallengeJsonLdDto startDate(LocalDate startDate) { + this.startDate = startDate; + return this; + } + + /** + * The start date of the challenge. + * + * @return startDate + */ + @Valid + @Schema( + name = "startDate", + example = "Fri Jul 21 00:00:00 UTC 2017", + description = "The start date of the challenge.", + required = false) + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public ChallengeJsonLdDto endDate(LocalDate endDate) { + this.endDate = endDate; + return this; + } + + /** + * The end date of the challenge. + * + * @return endDate + */ + @Valid + @Schema( + name = "endDate", + example = "Fri Jul 21 00:00:00 UTC 2017", + description = "The end date of the challenge.", + required = false) + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } + + public ChallengeJsonLdDto starredCount(Integer starredCount) { + this.starredCount = starredCount; + return this; + } + + /** + * The number of times the challenge has been starred by users. minimum: 0 + * + * @return starredCount + */ + @NotNull + @Min(0) + @Schema( + name = "starredCount", + example = "100", + description = "The number of times the challenge has been starred by users.", + required = true) + public Integer getStarredCount() { + return starredCount; + } + + public void setStarredCount(Integer starredCount) { + this.starredCount = starredCount; + } + + public ChallengeJsonLdDto operation(EdamConceptDto operation) { + this.operation = operation; + return this; + } + + /** + * Get operation + * + * @return operation + */ + @Valid + @Schema(name = "operation", required = false) + public EdamConceptDto getOperation() { + return operation; + } + + public void setOperation(EdamConceptDto operation) { + this.operation = operation; + } + + public ChallengeJsonLdDto createdAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + return this; + } + + /** + * Datetime when the object was added to the database. + * + * @return createdAt + */ + @NotNull + @Valid + @Schema( + name = "createdAt", + example = "2022-07-04T22:19:11Z", + description = "Datetime when the object was added to the database.", + required = true) + public OffsetDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(OffsetDateTime createdAt) { + this.createdAt = createdAt; + } + + public ChallengeJsonLdDto updatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + /** + * Datetime when the object was last modified in the database. + * + * @return updatedAt + */ + @NotNull + @Valid + @Schema( + name = "updatedAt", + example = "2022-07-04T22:19:11Z", + description = "Datetime when the object was last modified in the database.", + required = true) + public OffsetDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(OffsetDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public ChallengeJsonLdDto atContext(String atContext) { + this.atContext = atContext; + return this; + } + + /** + * Get atContext + * + * @return atContext + */ + @NotNull + @Schema(name = "@context", example = "https://schema.org", required = true) + public String getAtContext() { + return atContext; + } + + public void setAtContext(String atContext) { + this.atContext = atContext; + } + + public ChallengeJsonLdDto atId(String atId) { + this.atId = atId; + return this; + } + + /** + * Get atId + * + * @return atId + */ + @NotNull + @Schema(name = "@id", example = "https://openchallenges.io/api/v1/challenges/1", required = true) + public String getAtId() { + return atId; + } + + public void setAtId(String atId) { + this.atId = atId; + } + + public ChallengeJsonLdDto atType(String atType) { + this.atType = atType; + return this; + } + + /** + * Get atType + * + * @return atType + */ + @NotNull + @Schema(name = "@type", example = "Challenge", required = true) + public String getAtType() { + return atType; + } + + public void setAtType(String atType) { + this.atType = atType; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChallengeJsonLdDto challengeJsonLd = (ChallengeJsonLdDto) o; + return Objects.equals(this.id, challengeJsonLd.id) + && Objects.equals(this.slug, challengeJsonLd.slug) + && Objects.equals(this.name, challengeJsonLd.name) + && Objects.equals(this.headline, challengeJsonLd.headline) + && Objects.equals(this.description, challengeJsonLd.description) + && Objects.equals(this.doi, challengeJsonLd.doi) + && Objects.equals(this.status, challengeJsonLd.status) + && Objects.equals(this.platform, challengeJsonLd.platform) + && Objects.equals(this.websiteUrl, challengeJsonLd.websiteUrl) + && Objects.equals(this.avatarUrl, challengeJsonLd.avatarUrl) + && Objects.equals(this.incentives, challengeJsonLd.incentives) + && Objects.equals(this.submissionTypes, challengeJsonLd.submissionTypes) + && Objects.equals(this.inputDataTypes, challengeJsonLd.inputDataTypes) + && Objects.equals(this.categories, challengeJsonLd.categories) + && Objects.equals(this.startDate, challengeJsonLd.startDate) + && Objects.equals(this.endDate, challengeJsonLd.endDate) + && Objects.equals(this.starredCount, challengeJsonLd.starredCount) + && Objects.equals(this.operation, challengeJsonLd.operation) + && Objects.equals(this.createdAt, challengeJsonLd.createdAt) + && Objects.equals(this.updatedAt, challengeJsonLd.updatedAt) + && Objects.equals(this.atContext, challengeJsonLd.atContext) + && Objects.equals(this.atId, challengeJsonLd.atId) + && Objects.equals(this.atType, challengeJsonLd.atType); + } + + @Override + public int hashCode() { + return Objects.hash( + id, + slug, + name, + headline, + description, + doi, + status, + platform, + websiteUrl, + avatarUrl, + incentives, + submissionTypes, + inputDataTypes, + categories, + startDate, + endDate, + starredCount, + operation, + createdAt, + updatedAt, + atContext, + atId, + atType); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ChallengeJsonLdDto {\n"); + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" slug: ").append(toIndentedString(slug)).append("\n"); + sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" headline: ").append(toIndentedString(headline)).append("\n"); + sb.append(" description: ").append(toIndentedString(description)).append("\n"); + sb.append(" doi: ").append(toIndentedString(doi)).append("\n"); + sb.append(" status: ").append(toIndentedString(status)).append("\n"); + sb.append(" platform: ").append(toIndentedString(platform)).append("\n"); + sb.append(" websiteUrl: ").append(toIndentedString(websiteUrl)).append("\n"); + sb.append(" avatarUrl: ").append(toIndentedString(avatarUrl)).append("\n"); + sb.append(" incentives: ").append(toIndentedString(incentives)).append("\n"); + sb.append(" submissionTypes: ").append(toIndentedString(submissionTypes)).append("\n"); + sb.append(" inputDataTypes: ").append(toIndentedString(inputDataTypes)).append("\n"); + sb.append(" categories: ").append(toIndentedString(categories)).append("\n"); + sb.append(" startDate: ").append(toIndentedString(startDate)).append("\n"); + sb.append(" endDate: ").append(toIndentedString(endDate)).append("\n"); + sb.append(" starredCount: ").append(toIndentedString(starredCount)).append("\n"); + sb.append(" operation: ").append(toIndentedString(operation)).append("\n"); + sb.append(" createdAt: ").append(toIndentedString(createdAt)).append("\n"); + sb.append(" updatedAt: ").append(toIndentedString(updatedAt)).append("\n"); + sb.append(" atContext: ").append(toIndentedString(atContext)).append("\n"); + sb.append(" atId: ").append(toIndentedString(atId)).append("\n"); + sb.append(" atType: ").append(toIndentedString(atType)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/mapper/ChallengeJsonLdMapper.java b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/mapper/ChallengeJsonLdMapper.java new file mode 100644 index 0000000000..f770ced75c --- /dev/null +++ b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/model/mapper/ChallengeJsonLdMapper.java @@ -0,0 +1,62 @@ +package org.sagebionetworks.openchallenges.challenge.service.model.mapper; + +import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeCategoryDto; +import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeIncentiveDto; +import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeJsonLdDto; +import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeStatusDto; +import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeSubmissionTypeDto; +import org.sagebionetworks.openchallenges.challenge.service.model.entity.ChallengeEntity; +import org.sagebionetworks.util.model.mapper.BaseMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; + +public class ChallengeJsonLdMapper extends BaseMapper { + + private static final Logger LOG = LoggerFactory.getLogger(ChallengeJsonLdMapper.class); + + private SimpleChallengePlatformMapper platformMapper = new SimpleChallengePlatformMapper(); + private EdamConceptMapper edamConceptMapper = new EdamConceptMapper(); + + @Override + public ChallengeEntity convertToEntity(ChallengeJsonLdDto dto, Object... args) { + ChallengeEntity entity = new ChallengeEntity(); + if (dto != null) { + BeanUtils.copyProperties(dto, entity); + } + return entity; + } + + @Override + public ChallengeJsonLdDto convertToDto(ChallengeEntity entity, Object... args) { + ChallengeJsonLdDto dto = new ChallengeJsonLdDto(); + if (entity != null) { + BeanUtils.copyProperties(entity, dto, "stars", "inputDataTypes", "platform", "operation"); + dto.setStatus(ChallengeStatusDto.fromValue(entity.getStatus())); + dto.setPlatform(platformMapper.convertToDto(entity.getPlatform())); + if (entity.getOperation() != null) { + dto.setOperation(edamConceptMapper.convertToDto(entity.getOperation())); + } + dto.submissionTypes( + entity.getSubmissionTypes().stream() + .map(o -> ChallengeSubmissionTypeDto.fromValue(o.getName())) + .toList()); + dto.incentives( + entity.getIncentives().stream() + .map(o -> ChallengeIncentiveDto.fromValue(o.getName())) + .toList()); + dto.categories( + entity.getCategories().stream() + .map(o -> ChallengeCategoryDto.fromValue(o.getName())) + .toList()); + dto.inputDataTypes(edamConceptMapper.convertToDtoList(entity.getInputDataTypes())); + dto.starredCount(entity.getStars().size()); + // Add JSON-LD specific properties + dto.atContext( + "https://raw.githubusercontent.com/Sage-Bionetworks/core-models/de1000bf4f2ad7f19eb3ff0ff249ed4c046fd247/draft-data-models/challenges.jsonld"); + dto.atId("https://openchallenges.io/api/v1/challenges/" + entity.getId()); + dto.atType("Challenge"); + } + return dto; + } +} diff --git a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/service/ChallengeService.java b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/service/ChallengeService.java index f3de346686..b6163c2d63 100644 --- a/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/service/ChallengeService.java +++ b/apps/openchallenges/challenge-service/src/main/java/org/sagebionetworks/openchallenges/challenge/service/service/ChallengeService.java @@ -5,9 +5,11 @@ import lombok.extern.slf4j.Slf4j; import org.sagebionetworks.openchallenges.challenge.service.exception.ChallengeNotFoundException; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeDto; +import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeJsonLdDto; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengeSearchQueryDto; import org.sagebionetworks.openchallenges.challenge.service.model.dto.ChallengesPageDto; import org.sagebionetworks.openchallenges.challenge.service.model.entity.ChallengeEntity; +import org.sagebionetworks.openchallenges.challenge.service.model.mapper.ChallengeJsonLdMapper; import org.sagebionetworks.openchallenges.challenge.service.model.mapper.ChallengeMapper; import org.sagebionetworks.openchallenges.challenge.service.model.repository.ChallengeRepository; import org.springframework.data.domain.Page; @@ -29,6 +31,7 @@ public ChallengeService(ChallengeRepository challengeRepository) { // @Autowired private ProducerService producerService; private ChallengeMapper challengeMapper = new ChallengeMapper(); + private ChallengeJsonLdMapper challengeJsonLdMapper = new ChallengeJsonLdMapper(); private static final List SEARCHABLE_FIELDS = Arrays.asList( @@ -72,15 +75,22 @@ public ChallengesPageDto listChallenges(ChallengeSearchQueryDto query) { @Transactional(readOnly = true) public ChallengeDto getChallenge(Long challengeId) { - ChallengeEntity challengeEntity = - challengeRepository - .findById(challengeId) - .orElseThrow( - () -> - new ChallengeNotFoundException( - String.format("The challenge with ID %d does not exist.", challengeId))); - log.info("challenge entity platform: {}", challengeEntity.getPlatform()); - ChallengeDto challenge = challengeMapper.convertToDto(challengeEntity); - return challenge; + ChallengeEntity challengeEntity = getChallengeEntity(challengeId); + return challengeMapper.convertToDto(challengeEntity); + } + + @Transactional(readOnly = true) + public ChallengeJsonLdDto getChallengeJsonLd(Long challengeId) { + ChallengeEntity challengeEntity = getChallengeEntity(challengeId); + return challengeJsonLdMapper.convertToDto(challengeEntity); + } + + private ChallengeEntity getChallengeEntity(Long challengeId) throws ChallengeNotFoundException { + return challengeRepository + .findById(challengeId) + .orElseThrow( + () -> + new ChallengeNotFoundException( + String.format("The challenge with ID %d does not exist.", challengeId))); } } diff --git a/apps/openchallenges/challenge-service/src/main/resources/openapi.yaml b/apps/openchallenges/challenge-service/src/main/resources/openapi.yaml index 7491945958..255fca070e 100644 --- a/apps/openchallenges/challenge-service/src/main/resources/openapi.yaml +++ b/apps/openchallenges/challenge-service/src/main/resources/openapi.yaml @@ -80,6 +80,9 @@ paths: application/json: schema: $ref: '#/components/schemas/Challenge' + application/ld+json: + schema: + $ref: '#/components/schemas/ChallengeJsonLd' description: A challenge "404": content: @@ -862,6 +865,12 @@ components: x-java-class-annotations: - '@lombok.AllArgsConstructor' - '@lombok.Builder' + ChallengeJsonLd: + allOf: + - $ref: '#/components/schemas/Challenge' + - $ref: '#/components/schemas/ChallengeJsonLd_allOf' + description: A challenge + type: object ChallengeContributionRole: description: The nature of a challenge contribution. enum: @@ -1111,6 +1120,23 @@ components: - challenges type: object example: null + ChallengeJsonLd_allOf: + properties: + '@context': + example: https://schema.org + type: string + '@id': + example: https://openchallenges.io/api/v1/challenges/1 + type: string + '@type': + example: Challenge + type: string + required: + - '@context' + - '@id' + - '@type' + type: object + example: null ChallengeContributionsPage_allOf: properties: challengeContributions: diff --git a/libs/openchallenges/api-client-angular/src/lib/.openapi-generator/FILES b/libs/openchallenges/api-client-angular/src/lib/.openapi-generator/FILES index 7b9fc63566..ebf1381296 100644 --- a/libs/openchallenges/api-client-angular/src/lib/.openapi-generator/FILES +++ b/libs/openchallenges/api-client-angular/src/lib/.openapi-generator/FILES @@ -23,6 +23,8 @@ model/challengeContributionsPage.ts model/challengeContributionsPageAllOf.ts model/challengeDirection.ts model/challengeIncentive.ts +model/challengeJsonLd.ts +model/challengeJsonLdAllOf.ts model/challengePlatform.ts model/challengePlatformDirection.ts model/challengePlatformSearchQuery.ts diff --git a/libs/openchallenges/api-client-angular/src/lib/api/challenge.service.ts b/libs/openchallenges/api-client-angular/src/lib/api/challenge.service.ts index 0ccdbe95de..087cd15eb6 100644 --- a/libs/openchallenges/api-client-angular/src/lib/api/challenge.service.ts +++ b/libs/openchallenges/api-client-angular/src/lib/api/challenge.service.ts @@ -23,6 +23,8 @@ import { BasicError } from '../model/basicError'; // @ts-ignore import { Challenge } from '../model/challenge'; // @ts-ignore +import { ChallengeJsonLd } from '../model/challengeJsonLd'; +// @ts-ignore import { ChallengeSearchQuery } from '../model/challengeSearchQuery'; // @ts-ignore import { ChallengesPage } from '../model/challengesPage'; @@ -104,10 +106,11 @@ export class ChallengeService { * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ + public getChallenge(challengeId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/ld+json' | 'application/problem+json', context?: HttpContext}): Observable; public getChallenge(challengeId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json' | 'application/problem+json', context?: HttpContext}): Observable; - public getChallenge(challengeId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json' | 'application/problem+json', context?: HttpContext}): Observable>; - public getChallenge(challengeId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json' | 'application/problem+json', context?: HttpContext}): Observable>; - public getChallenge(challengeId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json' | 'application/problem+json', context?: HttpContext}): Observable { + public getChallenge(challengeId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json' | 'application/ld+json' | 'application/problem+json', context?: HttpContext}): Observable>; + public getChallenge(challengeId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json' | 'application/ld+json' | 'application/problem+json', context?: HttpContext}): Observable>; + public getChallenge(challengeId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json' | 'application/ld+json' | 'application/problem+json', context?: HttpContext}): Observable { if (challengeId === null || challengeId === undefined) { throw new Error('Required parameter challengeId was null or undefined when calling getChallenge.'); } @@ -119,6 +122,7 @@ export class ChallengeService { // to determine the Accept header const httpHeaderAccepts: string[] = [ 'application/json', + 'application/ld+json', 'application/problem+json' ]; localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); diff --git a/libs/openchallenges/api-client-angular/src/lib/model/challengeJsonLd.ts b/libs/openchallenges/api-client-angular/src/lib/model/challengeJsonLd.ts new file mode 100644 index 0000000000..89a4471476 --- /dev/null +++ b/libs/openchallenges/api-client-angular/src/lib/model/challengeJsonLd.ts @@ -0,0 +1,87 @@ +/** + * OpenChallenges REST API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { ChallengeCategory } from './challengeCategory'; +import { EdamConcept } from './edamConcept'; +import { SimpleChallengePlatform } from './simpleChallengePlatform'; +import { ChallengeStatus } from './challengeStatus'; +import { ChallengeIncentive } from './challengeIncentive'; +import { ChallengeSubmissionType } from './challengeSubmissionType'; + + +/** + * A challenge + */ +export interface ChallengeJsonLd { + /** + * The unique identifier of the challenge. + */ + id: number; + /** + * The unique slug of the challenge. + */ + slug: string; + /** + * The name of the challenge. + */ + name: string; + /** + * The headline of the challenge. + */ + headline?: string | null; + /** + * The description of the challenge. + */ + description: string; + /** + * The DOI of the challenge. + */ + doi?: string | null; + status: ChallengeStatus; + platform?: SimpleChallengePlatform | null; + /** + * A URL to the website or image. + */ + websiteUrl?: string | null; + /** + * A URL to the website or image. + */ + avatarUrl?: string | null; + incentives: Array; + submissionTypes: Array; + inputDataTypes?: Array; + categories: Array; + /** + * The start date of the challenge. + */ + startDate?: string | null; + /** + * The end date of the challenge. + */ + endDate?: string | null; + /** + * The number of times the challenge has been starred by users. + */ + starredCount: number; + operation?: EdamConcept | null; + /** + * Datetime when the object was added to the database. + */ + createdAt: string; + /** + * Datetime when the object was last modified in the database. + */ + updatedAt: string; + '@context': string; + '@id': string; + '@type': string; +} + diff --git a/libs/openchallenges/api-client-angular/src/lib/model/challengeJsonLdAllOf.ts b/libs/openchallenges/api-client-angular/src/lib/model/challengeJsonLdAllOf.ts new file mode 100644 index 0000000000..fb3a0bd2b3 --- /dev/null +++ b/libs/openchallenges/api-client-angular/src/lib/model/challengeJsonLdAllOf.ts @@ -0,0 +1,19 @@ +/** + * OpenChallenges REST API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ChallengeJsonLdAllOf { + context: string; + id: string; + type: string; +} + diff --git a/libs/openchallenges/api-client-angular/src/lib/model/models.ts b/libs/openchallenges/api-client-angular/src/lib/model/models.ts index d70a0b36d3..06874f77a6 100644 --- a/libs/openchallenges/api-client-angular/src/lib/model/models.ts +++ b/libs/openchallenges/api-client-angular/src/lib/model/models.ts @@ -7,6 +7,8 @@ export * from './challengeContributionsPage'; export * from './challengeContributionsPageAllOf'; export * from './challengeDirection'; export * from './challengeIncentive'; +export * from './challengeJsonLd'; +export * from './challengeJsonLdAllOf'; export * from './challengePlatform'; export * from './challengePlatformDirection'; export * from './challengePlatformSearchQuery'; diff --git a/libs/openchallenges/api-description/build/challenge.openapi.yaml b/libs/openchallenges/api-description/build/challenge.openapi.yaml index 3d1fcdbc5f..5220ca026b 100644 --- a/libs/openchallenges/api-description/build/challenge.openapi.yaml +++ b/libs/openchallenges/api-description/build/challenge.openapi.yaml @@ -58,6 +58,9 @@ paths: application/json: schema: $ref: '#/components/schemas/Challenge' + application/ld+json: + schema: + $ref: '#/components/schemas/ChallengeJsonLd' '404': $ref: '#/components/responses/NotFound' '500': @@ -572,6 +575,26 @@ components: x-java-class-annotations: - '@lombok.AllArgsConstructor' - '@lombok.Builder' + ChallengeJsonLd: + type: object + description: A challenge + allOf: + - $ref: '#/components/schemas/Challenge' + - type: object + properties: + '@context': + type: string + example: https://schema.org + '@id': + type: string + example: https://openchallenges.io/api/v1/challenges/1 + '@type': + type: string + example: Challenge + required: + - '@context' + - '@id' + - '@type' ChallengeContributionRole: description: The nature of a challenge contribution. type: string diff --git a/libs/openchallenges/api-description/build/openapi.yaml b/libs/openchallenges/api-description/build/openapi.yaml index 0cfcdebab4..2be8569bf2 100644 --- a/libs/openchallenges/api-description/build/openapi.yaml +++ b/libs/openchallenges/api-description/build/openapi.yaml @@ -64,6 +64,9 @@ paths: application/json: schema: $ref: '#/components/schemas/Challenge' + application/ld+json: + schema: + $ref: '#/components/schemas/ChallengeJsonLd' '404': $ref: '#/components/responses/NotFound' '500': @@ -724,6 +727,26 @@ components: x-java-class-annotations: - '@lombok.AllArgsConstructor' - '@lombok.Builder' + ChallengeJsonLd: + type: object + description: A challenge + allOf: + - $ref: '#/components/schemas/Challenge' + - type: object + properties: + '@context': + type: string + example: 'https://schema.org' + '@id': + type: string + example: 'https://openchallenges.io/api/v1/challenges/1' + '@type': + type: string + example: Challenge + required: + - '@context' + - '@id' + - '@type' ChallengeContributionRole: description: The nature of a challenge contribution. type: string diff --git a/libs/openchallenges/api-description/src/components/schemas/ChallengeJsonLd.yaml b/libs/openchallenges/api-description/src/components/schemas/ChallengeJsonLd.yaml new file mode 100644 index 0000000000..ef4d2b5e57 --- /dev/null +++ b/libs/openchallenges/api-description/src/components/schemas/ChallengeJsonLd.yaml @@ -0,0 +1,19 @@ +type: object +description: A challenge +allOf: + - $ref: Challenge.yaml + - type: object + properties: + '@context': + type: string + example: 'https://schema.org' + '@id': + type: string + example: 'https://openchallenges.io/api/v1/challenges/1' + '@type': + type: string + example: 'Challenge' + required: + - '@context' + - '@id' + - '@type' diff --git a/libs/openchallenges/api-description/src/paths/challenges/@{challengeId}.yaml b/libs/openchallenges/api-description/src/paths/challenges/@{challengeId}.yaml index 3f933b17bd..78e433ab0e 100644 --- a/libs/openchallenges/api-description/src/paths/challenges/@{challengeId}.yaml +++ b/libs/openchallenges/api-description/src/paths/challenges/@{challengeId}.yaml @@ -13,6 +13,9 @@ get: application/json: schema: $ref: ../../components/schemas/Challenge.yaml + application/ld+json: + schema: + $ref: ../../components/schemas/ChallengeJsonLd.yaml '404': $ref: ../../components/responses/NotFound.yaml '500': diff --git a/libs/openchallenges/challenge/src/lib/challenge.component.ts b/libs/openchallenges/challenge/src/lib/challenge.component.ts index 14d4626946..9aca5562ad 100644 --- a/libs/openchallenges/challenge/src/lib/challenge.component.ts +++ b/libs/openchallenges/challenge/src/lib/challenge.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit, Renderer2 } from '@angular/core'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { - Challenge, + ChallengeJsonLd, ChallengeService, } from '@sagebionetworks/openchallenges/api-client-angular'; import { @@ -65,7 +65,7 @@ export class ChallengeComponent implements OnInit, OnDestroy { public termsOfUseUrl: string; public apiDocsUrl: string; - challenge$!: Observable; + challenge$!: Observable; loggedIn = false; challengeAvatar!: Avatar; tabs = CHALLENGE_TABS; @@ -91,7 +91,12 @@ export class ChallengeComponent implements OnInit, OnDestroy { ngOnInit(): void { this.challenge$ = this.activatedRoute.params.pipe( switchMap((params) => - this.challengeService.getChallenge(params['challengeId']), + this.challengeService.getChallenge( + params['challengeId'], + undefined, + undefined, + { httpHeaderAccept: 'application/ld+json' }, + ), ), catchError((err) => { const error = handleHttpError(err, this.router, { @@ -112,6 +117,7 @@ export class ChallengeComponent implements OnInit, OnDestroy { }; this.seoService.setData(getSeoData(challenge), this.renderer2); + this.seoService.setJsonLds([challenge], this.renderer2); }); const activeTabKey$: Observable =