diff --git a/fda-identifier-service/pom.xml b/fda-identifier-service/pom.xml index afd2774486cbe822f7baecebd3dee575913310de..afbe31fee99b3a94a5772b97f06a3d89b433984d 100644 --- a/fda-identifier-service/pom.xml +++ b/fda-identifier-service/pom.xml @@ -57,6 +57,10 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-thymeleaf</artifactId> + </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> @@ -182,6 +186,7 @@ <filtering>true</filtering> <includes> <include>**/application*.yml</include> + <include>**/templates/*.xml</include> </includes> </resource> </resources> diff --git a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java index 51d8cc4d5a993ad33fefddac1d5907c2298e7906..b4e9f30e7d07c3f647af8380e4a2a718016c9f81 100644 --- a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java +++ b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/IdentifierEndpoint.java @@ -1,6 +1,5 @@ package at.tuwien.endpoints; -import at.tuwien.ExportResource; import at.tuwien.api.identifier.IdentifierCreateDto; import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.api.identifier.IdentifierTypeDto; @@ -13,8 +12,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.InputStreamResource; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -58,22 +55,6 @@ public class IdentifierEndpoint { return ResponseEntity.ok(dto); } - @GetMapping("/{id}") - @Deprecated - @Transactional(readOnly = true) - @Timed(value = "identifier.export", description = "Time needed to export an identifier") - @Operation(summary = "Export some identifier metadata") - public ResponseEntity<InputStreamResource> export(@NotNull @PathVariable("id") Long id) - throws IdentifierNotFoundException { - log.debug("endpoint export identifier, id={}", id); - final HttpHeaders headers = new HttpHeaders(); - final ExportResource resource = identifierService.exportMetadata(id); - headers.add("Content-Disposition", "attachment; filename=\"" + resource.getFilename() + "\""); - return ResponseEntity.ok() - .headers(headers) - .body(resource.getResource()); - } - @PostMapping @Transactional @Timed(value = "identifier.create", description = "Time needed to create an identifier") diff --git a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java index 6150c9313bb7d77a415346ea0d60a833526cf9ff..b761546b6680d66e1d02a307fee5ee7180317eaa 100644 --- a/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java +++ b/fda-identifier-service/rest-service/src/main/java/at/tuwien/endpoints/PersistenceEndpoint.java @@ -1,6 +1,6 @@ package at.tuwien.endpoints; -import at.tuwien.ExportResource; +import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.config.EndpointConfig; import at.tuwien.entities.identifier.Identifier; import at.tuwien.exception.IdentifierNotFoundException; @@ -13,6 +13,7 @@ import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.Operation; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -45,30 +46,41 @@ public class PersistenceEndpoint { public ResponseEntity<?> find(@Valid @PathVariable("pid") Long pid, @RequestHeader(HttpHeaders.ACCEPT) String accept) throws IdentifierNotFoundException, QueryNotFoundException, RemoteUnavailableException { - log.debug("find identifier endpoint, pid={}, accept={}", pid, accept); + log.debug("endpoint find identifier, pid={}, accept={}", pid, accept); final Identifier identifier = identifierService.find(pid); log.info("Found persistent identifier with id {}", identifier.getId()); log.trace("found persistent identifier {}", identifier); if (accept != null) { log.trace("accept header present: {}", accept); - if (accept.equals("application/json")) { - log.trace("accept header matches json"); - return ResponseEntity.ok(identifierMapper.identifierToIdentifierDto(identifier)); - } else if (accept.equals("text/csv")) { - log.trace("accept header matches csv"); - return ResponseEntity.ok(identifierService.exportResource(pid)); - } else if (accept.equals("text/xml")) { - final HttpHeaders headers = new HttpHeaders(); - final ExportResource resource = identifierService.exportMetadata(pid); - headers.add("Content-Disposition", "attachment; filename=\"" + resource.getFilename() + "\""); - return ResponseEntity.ok() - .headers(headers) - .body(resource.getResource()); + switch (accept) { + case "application/json": + log.trace("accept header matches json"); + final IdentifierDto resource1 = identifierMapper.identifierToIdentifierDto(identifier); + log.debug("find identifier resulted in identifier {}", resource1); + return ResponseEntity.ok(resource1); + case "text/csv": + log.trace("accept header matches csv"); + final InputStreamResource resource2; + try { + resource2 = identifierService.exportResource(pid); + log.debug("find identifier resulted in resource {}", resource2); + return ResponseEntity.ok(resource2); + } catch (IdentifierRequestException e) { + /* ignore */ + } + case "text/xml": + log.trace("accept header matches xml"); + final InputStreamResource resource3 = identifierService.exportMetadata(pid); + log.debug("find identifier resulted in resource {}", resource3); + return ResponseEntity.ok(resource3); } + } else { + log.trace("no accept header present"); } - log.trace("no accept header present, serving http redirect"); final HttpHeaders headers = new HttpHeaders(); - headers.add("Location", identifierMapper.identifierToLocationUrl(endpointConfig.getWebsiteUrl(), identifier)); + final String url = identifierMapper.identifierToLocationUrl(endpointConfig.getWebsiteUrl(), identifier); + headers.add("Location", url); + log.debug("find identifier resulted in http redirect, headers={}, url={}", headers, url); return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY) .headers(headers) .build(); diff --git a/fda-identifier-service/rest-service/src/main/resources/templates/doi.xml b/fda-identifier-service/rest-service/src/main/resources/templates/doi.xml new file mode 100644 index 0000000000000000000000000000000000000000..81c7fccc9bf1ce052835b787bc4fb06e810cc3b9 --- /dev/null +++ b/fda-identifier-service/rest-service/src/main/resources/templates/doi.xml @@ -0,0 +1,49 @@ +<resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" + xsi:schemaLocation="http://datacite.org/schema/kernel-4 https://schema.datacite.org/meta/kernel-4.4/metadata.xsd"> + <identifier identifierType="PID">[[${doi}]]</identifier> + <creators th:if="${not #lists.isEmpty(creators)}"> + <creator th:each="creator: ${creators}"> + <creatorName nameType="Personal"> + [[${creator.name}]] + </creatorName> + <nameIdentifier th:if="${creator.orcid != null}" schemeURI="https://orcid.org" nameIdentifierScheme="ORCID"> + [[${creator.orcid}]] + </nameIdentifier> + <affiliation th:if="${creator.affiliation != null}"> + [[${creator.affiliation}]] + </affiliation> + </creator> + </creators> + <titles> + <title xml:lang="en"> + [[${title}]] + </title> + </titles> + <publisher xml:lang="en"> + [[${publisher}]] + </publisher> + <publicationYear> + [[${publicationYear}]] + </publicationYear> + <dates> + <date dateType="Issued"> + [[${created}]] + </date> + <date dateType="Available"> + [[${created}]] + </date> + </dates> + <resourceType resourceTypeGeneral="Dataset">Dataset</resourceType> + <relatedIdentifiers th:if="${not #lists.isEmpty(relatedIdentifiers)}"> + <relatedIdentifier th:each="relatedIdentifier: ${relatedIdentifiers}" + th:attr="relatedIdentifierType=${relatedIdentifier.type},relationType=${relatedIdentifier.relation}"> + [[${relatedIdentifier.value}]] + </relatedIdentifier> + </relatedIdentifiers> + <descriptions th:if="${description != null}"> + <description descriptionType="Abstract"> + [[${description}]] + </description> + </descriptions> + <version>1.0</version> +</resource> \ No newline at end of file diff --git a/fda-identifier-service/rest-service/src/main/java/at/tuwien/config/EndpointConfig.java b/fda-identifier-service/services/src/main/java/at/tuwien/config/EndpointConfig.java similarity index 100% rename from fda-identifier-service/rest-service/src/main/java/at/tuwien/config/EndpointConfig.java rename to fda-identifier-service/services/src/main/java/at/tuwien/config/EndpointConfig.java diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/config/TemplateConfig.java b/fda-identifier-service/services/src/main/java/at/tuwien/config/TemplateConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..c2d1721c0ceaadb4c2f211deb9805b16b5b71c70 --- /dev/null +++ b/fda-identifier-service/services/src/main/java/at/tuwien/config/TemplateConfig.java @@ -0,0 +1,30 @@ +package at.tuwien.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.thymeleaf.spring5.SpringTemplateEngine; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +import java.nio.charset.StandardCharsets; + +@Configuration +public class TemplateConfig { + + @Bean + public SpringTemplateEngine springTemplateEngine() { + final SpringTemplateEngine springTemplateEngine = new SpringTemplateEngine(); + springTemplateEngine.addTemplateResolver(oaiTemplateResolver()); + return springTemplateEngine; + } + + private ClassLoaderTemplateResolver oaiTemplateResolver() { + final ClassLoaderTemplateResolver oaiTemplateResolver = new ClassLoaderTemplateResolver(); + oaiTemplateResolver.setPrefix("/templates/"); + oaiTemplateResolver.setSuffix(".xml"); + oaiTemplateResolver.setTemplateMode(TemplateMode.TEXT); + oaiTemplateResolver.setCharacterEncoding(StandardCharsets.UTF_8.name()); + oaiTemplateResolver.setCacheable(false); + return oaiTemplateResolver; + } +} diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/service/IdentifierService.java b/fda-identifier-service/services/src/main/java/at/tuwien/service/IdentifierService.java index 261b5b71af3c407ec631dae7638cc9baff110a3e..e68c67d7ea1655b388dea23482e159e9ac7cc029 100644 --- a/fda-identifier-service/services/src/main/java/at/tuwien/service/IdentifierService.java +++ b/fda-identifier-service/services/src/main/java/at/tuwien/service/IdentifierService.java @@ -72,7 +72,7 @@ public interface IdentifierService { * @return The export, if successful. * @throws IdentifierNotFoundException The identifier was not found in the metadata database or was deleted. */ - ExportResource exportMetadata(Long id) throws IdentifierNotFoundException; + InputStreamResource exportMetadata(Long id) throws IdentifierNotFoundException; /** * Exports an identifier to XML @@ -81,10 +81,10 @@ public interface IdentifierService { * @return The XML resource, if successful. * @throws IdentifierNotFoundException The identifier was not found in the metadata database or was deleted. * @throws QueryNotFoundException The query was not found in the metadata database or was deleted. - * @throws RemoteUnavailableException + * @throws RemoteUnavailableException The remote service is not available */ InputStreamResource exportResource(Long identifierId) - throws IdentifierNotFoundException, QueryNotFoundException, RemoteUnavailableException; + throws IdentifierNotFoundException, QueryNotFoundException, RemoteUnavailableException, IdentifierRequestException; /** * Updated the metadata (only) on the identifier for a given id in the metadata database. diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java b/fda-identifier-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java index d2fba7a6fb7450db5fba58d9c1ec454b69b4a92a..8e5aee90bebda8a45f73c89033aa2693430dca18 100644 --- a/fda-identifier-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java +++ b/fda-identifier-service/services/src/main/java/at/tuwien/service/impl/IdentifierServiceImpl.java @@ -1,12 +1,11 @@ package at.tuwien.service.impl; -import at.tuwien.ExportResource; -import at.tuwien.api.database.query.ExportDto; import at.tuwien.api.database.query.QueryDto; import at.tuwien.api.identifier.IdentifierCreateDto; import at.tuwien.api.identifier.IdentifierDto; import at.tuwien.api.identifier.IdentifierTypeDto; import at.tuwien.api.identifier.VisibilityTypeDto; +import at.tuwien.config.EndpointConfig; import at.tuwien.entities.database.Database; import at.tuwien.entities.identifier.*; import at.tuwien.entities.user.User; @@ -20,14 +19,15 @@ import at.tuwien.service.DatabaseService; import at.tuwien.service.IdentifierService; import at.tuwien.service.UserService; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.springframework.core.io.InputStreamResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.IOException; +import java.nio.charset.Charset; import java.security.Principal; import java.util.List; import java.util.Optional; @@ -39,18 +39,23 @@ public class IdentifierServiceImpl implements IdentifierService { private final UserService userService; private final DocumentMapper documentMapper; + private final EndpointConfig endpointConfig; + private final TemplateEngine templateEngine; private final DatabaseService databaseService; private final IdentifierMapper identifierMapper; private final QueryServiceGateway queryServiceGateway; private final IdentifierRepository identifierRepository; private final RelatedIdentifierRepository relatedIdentifierRepository; - public IdentifierServiceImpl(UserService userService, DocumentMapper documentMapper, - DatabaseService databaseService, IdentifierMapper identifierMapper, - QueryServiceGateway queryServiceGateway, IdentifierRepository identifierRepository, + public IdentifierServiceImpl(UserService userService, DocumentMapper documentMapper, EndpointConfig endpointConfig, + TemplateEngine templateEngine, DatabaseService databaseService, + IdentifierMapper identifierMapper, QueryServiceGateway queryServiceGateway, + IdentifierRepository identifierRepository, RelatedIdentifierRepository relatedIdentifierRepository) { this.userService = userService; this.documentMapper = documentMapper; + this.endpointConfig = endpointConfig; + this.templateEngine = templateEngine; this.databaseService = databaseService; this.identifierMapper = identifierMapper; this.queryServiceGateway = queryServiceGateway; @@ -170,22 +175,30 @@ public class IdentifierServiceImpl implements IdentifierService { @Override @Transactional(readOnly = true) - public ExportResource exportMetadata(Long id) throws IdentifierNotFoundException { + public InputStreamResource exportMetadata(Long id) throws IdentifierNotFoundException { /* check */ final Identifier identifier = find(id); + /* context */ + final Context context = new Context(); + context.setVariable("doi", endpointConfig.getWebsiteUrl() + "/pid/" + identifier.getId()); + context.setVariable("creators", identifier.getCreators()); + context.setVariable("title", identifier.getTitle()); + context.setVariable("publisher", identifier.getPublisher()); + context.setVariable("publicationYear", identifier.getPublicationYear()); + context.setVariable("created", documentMapper.instantToDate(identifier.getCreated())); + context.setVariable("relatedIdentifiers", identifier.getRelated()); + context.setVariable("description", identifier.getDescription()); /* map */ - final InputStreamResource resource = documentMapper.identifierToInputStreamResource(identifier); + final String body = templateEngine.process("doi.xml", context); + final InputStreamResource resource = new InputStreamResource(IOUtils.toInputStream(body, Charset.defaultCharset())); log.debug("mapped file stream {}", resource.getDescription()); - return ExportResource.builder() - .filename("metadata.xml") - .resource(resource) - .build(); + return resource; } @Override @Transactional(readOnly = true) - public InputStreamResource exportResource(Long identifierId) - throws IdentifierNotFoundException, QueryNotFoundException, RemoteUnavailableException { + public InputStreamResource exportResource(Long identifierId) throws IdentifierNotFoundException, + QueryNotFoundException, RemoteUnavailableException, IdentifierRequestException { /* check */ final Identifier identifier = find(identifierId); if (identifier.getType().equals(IdentifierType.DATABASE)) { @@ -194,11 +207,20 @@ public class IdentifierServiceImpl implements IdentifierService { throw new IdentifierNotFoundException("Failed to find identifier"); } /* export */ - final byte[] file = queryServiceGateway.export(identifier.getContainerId(), - identifier.getDatabaseId(), identifier.getQueryId()); - final InputStreamResource resource = new InputStreamResource(new ByteArrayInputStream(file)); - log.trace("found resource {}", resource); - return resource; + if (identifier.getType().equals(IdentifierType.SUBSET)) { + /* subset */ + final byte[] file = queryServiceGateway.export(identifier.getContainerId(), + identifier.getDatabaseId(), identifier.getQueryId()); + final InputStreamResource resource = new InputStreamResource(new ByteArrayInputStream(file)); + log.trace("found resource {}", resource); + return resource; + } else if (identifier.getType().equals(IdentifierType.DATABASE)) { + /* database, we cannot export this to csv */ + log.warn("Failed to export database to csv, fallback to default http redirect"); + throw new IdentifierRequestException("Failed to export database to csv"); + } + log.warn("Failed to export database, fallback to default http redirect"); + throw new IdentifierRequestException("Failed to export database"); } @Override diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierBriefDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierBriefDto.java index 4a7ac95ce07e4608489cee3cd09615bc1399db1c..c60e1e75076c6169392405c9b31216db2467eeac 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierBriefDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierBriefDto.java @@ -1,6 +1,7 @@ package at.tuwien.api.identifier; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; @@ -41,9 +42,11 @@ public class IdentifierBriefDto { @NotNull private IdentifierTypeDto type; + @JsonIgnore @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") private Instant created; + @JsonIgnore @JsonProperty("last_modified") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") private Instant lastModified; diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java index 8bd0bab047a7cde4923c4cdbfe1f4a6e67df638a..fb9bd450a10e242859d0a7807cd79de9137a9e82 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java @@ -1,5 +1,6 @@ package at.tuwien.api.user; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; @@ -22,10 +23,12 @@ public class UserBriefDto { @Schema(example = "user", description = "Only contains lowercase characters") private String username; + @JsonIgnore @JsonProperty("titles_before") @Schema(example = "Prof.") private String titlesBefore; + @JsonIgnore @JsonProperty("titles_after") private String titlesAfter; @@ -41,12 +44,12 @@ public class UserBriefDto { @Schema(example = "0000-0002-1825-0097") private String orcid; - @NotNull + @JsonIgnore @JsonProperty("theme_dark") @Schema(example = "true") private Boolean themeDark; - @NotNull + @JsonIgnore @JsonProperty("email_verified") @Schema(example = "true") private Boolean emailVerified; diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/RelatedType.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/RelatedType.java index 9e3174ed9ceae60377cf727663af07366908d7e6..34f98ef59116e35115a8324262b417b85862d9fc 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/RelatedType.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/RelatedType.java @@ -1,22 +1,51 @@ package at.tuwien.entities.identifier; public enum RelatedType { - DOI, - URL, - URN, - ARK, - ARXIV, - BIBCODE, - EAN13, - EISSN, - HANDLE, - IGSN, - ISBN, - ISTC, - LISSN, - LSID, - PMID, - PURL, - UPC, - W3ID; + + DOI("DOI"), + + URL("URL"), + + URN("URN"), + + ARK("ARK"), + + ARXIV("arXiv"), + + BIBCODE("bibcode"), + + EAN13("EAN13"), + + EISSN("EISSN"), + + HANDLE("Handle"), + + IGSN("IGSN"), + + ISBN("ISBN"), + + ISTC("ISTC"), + + LISSN("LISSN"), + + LSID("LSID"), + + PMID("PMID"), + + PURL("PURL"), + + UPC("UPC"), + + W3ID("w3id"); + + private String name; + + RelatedType(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } } diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/RelationType.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/RelationType.java index 5ba9f7726ef66b09523b7a3f3b4bbda956f63ecc..65fc23fddbdbe3dca3dedf0669ad4bdb9a2feb7e 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/RelationType.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/RelationType.java @@ -1,38 +1,83 @@ package at.tuwien.entities.identifier; public enum RelationType { - IS_CITED_BY, - CITES, - IS_SUPPLEMENT_TO, - IS_SUPPLEMENTED_BY, - IS_CONTINUED_BY, - CONTINUES, - IS_DESCRIBED_BY, - DESCRIBES, - HAS_METADATA, - IS_METADATA_FOR, - HAS_VERSION, - IS_VERSION_OF, - IS_NEW_VERSION_OF, - IS_PREVIOUS_VERSION_OF, - IS_PART_OF, - HAS_PART, - IS_PUBLISHED_IN, - IS_REFERENCED_BY, - REFERENCES, - IS_DOCUMENTED_BY, - DOCUMENTS, - IS_COMPILED_BY, - COMPILES, - IS_VARIANT_FORM_OF, - IS_ORIGINAL_FORM_OF, - IS_IDENTICAL_TO, - IS_REVIEWED_BY, - REVIEWS, - IS_DERIVED_FROM, - IS_SOURCE_OF, - IS_REQUIRED_BY, - REQUIRES, - IS_OBSOLETED_BY, - OBSOLETES, + + IS_CITED_BY("IsCitedBy"), + + CITES("Cites"), + + IS_SUPPLEMENT_TO("IsSupplementTo"), + + IS_SUPPLEMENTED_BY("IsSupplementedBy"), + + IS_CONTINUED_BY("IsContinuedBy"), + + CONTINUES("Continues"), + + IS_DESCRIBED_BY("IsDescribedBy"), + + DESCRIBES("Describes"), + + HAS_METADATA("HasMetadata"), + + IS_METADATA_FOR("IsMetadataFor"), + + HAS_VERSION("HasVersion"), + + IS_VERSION_OF("IsVersionOf"), + + IS_NEW_VERSION_OF("IsNewVersionOf"), + + IS_PREVIOUS_VERSION_OF("IsPreviousVersionOf"), + + IS_PART_OF("IsPartOf"), + + HAS_PART("HasPart"), + + IS_PUBLISHED_IN("IsPublishedIn"), + + IS_REFERENCED_BY("IsReferencedBy"), + + REFERENCES("References"), + + IS_DOCUMENTED_BY("IsDocumentedBy"), + + DOCUMENTS("Documents"), + + IS_COMPILED_BY("IsCompiledBy"), + + COMPILES("Compiles"), + + IS_VARIANT_FORM_OF("IsVariantFormOf"), + + IS_ORIGINAL_FORM_OF("IsOriginalFormOf"), + + IS_IDENTICAL_TO("IsIdenticalTo"), + + IS_REVIEWED_BY("IsReviewedBy"), + + REVIEWS("Reviews"), + + IS_DERIVED_FROM("IsDerivedFrom"), + + IS_SOURCE_OF("IsSourceOf"), + + IS_REQUIRED_BY("IsRequiredBy"), + + REQUIRES("Requires"), + + IS_OBSOLETED_BY("IsObsoletedBy"), + + OBSOLETES("Obsoletes"); + + private String name; + + RelationType(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } } diff --git a/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java b/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java index 87ba422bb1758f065cfaa3721a3dcf8de2094fa1..71b11475f3e852ba7102224c3d65aed0a7c294e3 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java +++ b/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java @@ -318,9 +318,9 @@ public interface QueryMapper { table.getColumns() .forEach(column -> { statement.append(idx[0] != 0 ? "," : "") - .append("`") + .append("'") .append(column.getInternalName()) - .append("`"); + .append("'"); idx[0]++; }); statement.append(" UNION ALL SELECT "); @@ -333,7 +333,7 @@ public interface QueryMapper { .append("`"); jdx[0]++; }); - statement.append("FROM `") + statement.append(" FROM `") .append(table.getInternalName()) .append("`"); if (timestamp != null) { diff --git a/fda-ui/components/dialogs/PersistQuery.vue b/fda-ui/components/dialogs/Persist.vue similarity index 88% rename from fda-ui/components/dialogs/PersistQuery.vue rename to fda-ui/components/dialogs/Persist.vue index 7ef730fcde71c39c566ff4cbe5d9e1378ff88eba..bcc0e3ec393bca55d586369b83a8e46da87aac1c 100644 --- a/fda-ui/components/dialogs/PersistQuery.vue +++ b/fda-ui/components/dialogs/Persist.vue @@ -2,11 +2,10 @@ <div> <v-card> <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> - <v-card-title> - Persist Subset and Result - </v-card-title> + <v-card-title v-text="`Persist ${title}`" /> <v-card-text> <v-alert + v-if="is_subset" border="left" color="info"> Choose an expressive subset title and describe what it produces. @@ -18,7 +17,8 @@ id="title" v-model="identifier.title" name="title" - label="Subset Title *" + :label="`${prefix} title *`" + :disabled="is_database" :rules="[v => !!v || $t('Required')]" required /> <v-textarea @@ -26,7 +26,7 @@ v-model="identifier.description" name="description" rows="2" - label="Subset Description" /> + :label="`${prefix} description *`" /> </v-col> </v-row> <v-row dense> @@ -35,7 +35,7 @@ id="publisher" v-model="identifier.publisher" name="publisher" - label="Subset Publisher *" + :label="`${prefix} publisher *`" :rules="[v => !!v || $t('Required')]" required /> </v-col> @@ -46,21 +46,21 @@ id="publication-day" v-model.number="identifier.publication_day" type="number" - label="Publication Day" /> + label="Publication day" /> </v-col> <v-col cols="2"> <v-text-field id="publication-month" v-model.number="identifier.publication_month" type="number" - label="Publication Month" /> + label="Publication month" /> </v-col> <v-col cols="3"> <v-text-field id="publication-year" v-model.number="identifier.publication_year" type="number" - label="Publication Year *" + label="Publication year *" :rules="[v => !!v || $t('Required')]" required /> </v-col> @@ -74,7 +74,7 @@ item-value="value" :disabled="database.is_public" item-text="name" - label="Visibility *" + :label="`${prefix} visibility *`" :rules="[v => !!v || $t('Required')]" required /> </v-col> @@ -84,7 +84,7 @@ <v-text-field v-model="creator.name" name="name" - label="Name *" + label="Lastname, Firstname *" :rules="[v => !!v || $t('Required')]" required /> </v-col> @@ -178,6 +178,12 @@ <script> import { formatYearUTC, formatMonthUTC, formatDayUTC } from '@/utils' export default { + props: { + type: { + type: String, + default: 'subset' + } + }, data () { return { formValid: false, @@ -289,12 +295,35 @@ export default { return null } return { Authorization: `Bearer ${this.token}` } + }, + is_subset () { + return this.type === 'subset' + }, + is_database () { + return this.type === 'database' + }, + title () { + if (this.is_subset) { + return 'subset' + } else if (this.is_database) { + return 'database' + } + return '' + }, + prefix () { + if (this.is_subset) { + return 'Subset' + } else if (this.is_database) { + return 'Database' + } + return '' } }, mounted () { this.loadUser() .then(() => this.addCreatorSelf()) this.loadDatabase() + .then(() => this.prefill()) }, methods: { cancel () { @@ -358,11 +387,11 @@ export default { } catch (err) { this.error = true this.loading = false - this.$toast.error('Failed to persist query') + this.$toast.error('Failed to persist') console.error('persist failed', err) return } - this.$toast.success('Query persisted.') + this.$toast.success(this.prefix + ' successfully persisted') this.$emit('close', { action: 'persisted' }) this.loading = false }, @@ -383,6 +412,14 @@ export default { console.error('load user data failed', err) } this.loading = false + }, + prefill () { + if (!this.is_database) { + return + } + this.identifier.title = this.database.name + this.identifier.type = 'database' + console.debug('pre-filled identifier', this.identifier) } } } diff --git a/fda-ui/components/dialogs/PersistDatabase.vue b/fda-ui/components/dialogs/PersistDatabase.vue deleted file mode 100644 index c5c82e13c85c335b35500b365af07dd3c693f2bf..0000000000000000000000000000000000000000 --- a/fda-ui/components/dialogs/PersistDatabase.vue +++ /dev/null @@ -1,508 +0,0 @@ -<template> - <div> - <v-form ref="form" v-model="valid" @submit.prevent="submit"> - <v-card flat> - <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> - <v-card-title> - Persist Database - </v-card-title> - <v-card-text> - <v-alert - border="left" - color="info"> - Choose an expressive database description for the information stored. - </v-alert> - <v-row dense> - <v-col> - <v-text-field - id="title" - v-model="identifier.title" - name="title" - label="Title *" - disabled - :rules="[v => !!v || $t('Required')]" - required /> - </v-col> - </v-row> - <v-row dense> - <v-col> - <v-text-field - id="publisher" - v-model="identifier.publisher" - name="publisher" - label="Publisher *" - :rules="[v => !!v || $t('Required')]" - required /> - </v-col> - </v-row> - <v-row dense> - <v-col> - <v-textarea - id="description" - v-model="identifier.description" - name="description" - rows="2" - label="Description" /> - </v-col> - </v-row> - <v-row dense> - <v-col> - <v-select - id="language" - v-model="identifier.language" - name="language" - label="Language" - :items="languages" - clearable - item-value="value" - item-text="text" /> - </v-col> - </v-row> - <v-row dense> - <v-col cols="4"> - <v-text-field - id="publication-day" - v-model.number="identifier.publication_day" - name="publication-day" - label="Publication Day" - hint="e.g. 08" - type="number" - clearable - min="1" - max="31" /> - </v-col> - <v-col cols="4"> - <v-text-field - id="publication-month" - v-model.number="identifier.publication_month" - name="publication-month" - label="Publication Month" - hint="e.g. 12" - type="number" - clearable - min="1" - max="12" /> - </v-col> - <v-col cols="4"> - <v-text-field - id="publication-year" - v-model.number="identifier.publication_year" - name="publication-year" - label="Publication Year *" - hint="e.g. 2022" - type="number" - clearable - :rules="[v => !!v || $t('Required')]" - required /> - </v-col> - </v-row> - <v-row dense> - <v-col> - <v-select - id="license" - v-model="identifier.license" - name="license" - label="License" - :items="licenses" - clearable - item-value="identifier" - item-text="identifier" - return-object /> - </v-col> - </v-row> - <v-row dense> - <v-col> - <v-select - v-if="false" - id="contact" - v-model="identifier.contact_person" - name="contact" - label="Contact" - :items="users" - clearable - item-value="username" - :item-text="item => `${printUser(item)}`" /> - </v-col> - </v-row> - </v-card-text> - <v-card-actions> - <v-spacer /> - <v-btn - class="mb-2" - @click="cancel"> - Close - </v-btn> - <v-btn - id="database" - class="mb-2 mr-2" - :disabled="!valid || loading" - color="primary" - type="submit" - @click="persist"> - Persist - </v-btn> - </v-card-actions> - </v-card> - </v-form> - </div> -</template> - -<script> -import { formatDayUTC, formatMonthUTC, formatUser, formatYearUTC } from '@/utils' -export default { - props: { - database: { - type: Object, - default () { - return {} - } - } - }, - data () { - return { - valid: false, - loading: false, - error: false, - menu: false, - users: [], - identifier: { - cid: parseInt(this.$route.params.container_id), - dbid: parseInt(this.$route.params.database_id), - title: null, - publisher: 'TU Wien', - description: null, - publication_year: formatYearUTC(Date.now()), - publication_month: formatMonthUTC(Date.now()), - publication_day: formatDayUTC(Date.now()), - type: 'database', - visibility: 'everyone', - doi: null, - creators: [], - related_identifiers: [] - }, - relatedTypes: [ - { value: 'DOI' }, - { value: 'URL' }, - { value: 'URN' }, - { value: 'ARK' }, - { value: 'arXiv' }, - { value: 'bibcode' }, - { value: 'EAN13' }, - { value: 'EISSN' }, - { value: 'Handle' }, - { value: 'IGSN' }, - { value: 'ISBN' }, - { value: 'ISTC' }, - { value: 'LISSN' }, - { value: 'LSID' }, - { value: 'PMID' }, - { value: 'PURL' }, - { value: 'UPC' }, - { value: 'w3id' } - ], - relationTypes: [ - { value: 'IsCitedBy' }, - { value: 'Cites' }, - { value: 'IsSupplementTo' }, - { value: 'IsSupplementedBy' }, - { value: 'IsContinuedBy' }, - { value: 'Continues' }, - { value: 'IsDescribedBy' }, - { value: 'Describes' }, - { value: 'HasMetadata' }, - { value: 'IsMetadataFor' }, - { value: 'HasVersion' }, - { value: 'IsVersionOf' }, - { value: 'IsNewVersionOf' }, - { value: 'IsPreviousVersionOf' }, - { value: 'IsPartOf' }, - { value: 'HasPart' }, - { value: 'IsPublishedIn' }, - { value: 'IsReferencedBy' }, - { value: 'References' }, - { value: 'IsDocumentedBy' }, - { value: 'Documents' }, - { value: 'IsCompiledBy' }, - { value: 'Compiles' }, - { value: 'IsVariantFormOf' }, - { value: 'IsOriginalFormOf' }, - { value: 'IsIdenticalTo' }, - { value: 'IsReviewedBy' }, - { value: 'Reviews' }, - { value: 'IsDerivedFrom' }, - { value: 'IsSourceOf' }, - { value: 'IsRequiredBy' }, - { value: 'Requires' }, - { value: 'IsObsoletedBy' }, - { value: 'Obsoletes' } - ], - licenses: [], - languages: [ - { text: 'aa', value: 'aa' }, - { text: 'ab', value: 'ab' }, - { text: 'ae', value: 'ae' }, - { text: 'af', value: 'af' }, - { text: 'ak', value: 'ak' }, - { text: 'am', value: 'am' }, - { text: 'an', value: 'an' }, - { text: 'ar', value: 'ar' }, - { text: 'as', value: 'as' }, - { text: 'av', value: 'av' }, - { text: 'ay', value: 'ay' }, - { text: 'az', value: 'az' }, - { text: 'ba', value: 'ba' }, - { text: 'be', value: 'be' }, - { text: 'bg', value: 'bg' }, - { text: 'bh', value: 'bh' }, - { text: 'bi', value: 'bi' }, - { text: 'bm', value: 'bm' }, - { text: 'bn', value: 'bn' }, - { text: 'bo', value: 'bo' }, - { text: 'br', value: 'br' }, - { text: 'bs', value: 'bs' }, - { text: 'ca', value: 'ca' }, - { text: 'ce', value: 'ce' }, - { text: 'ch', value: 'ch' }, - { text: 'co', value: 'co' }, - { text: 'cr', value: 'cr' }, - { text: 'cs', value: 'cs' }, - { text: 'cu', value: 'cu' }, - { text: 'cv', value: 'cv' }, - { text: 'cy', value: 'cy' }, - { text: 'da', value: 'da' }, - { text: 'de', value: 'de' }, - { text: 'dv', value: 'dv' }, - { text: 'dz', value: 'dz' }, - { text: 'ee', value: 'ee' }, - { text: 'el', value: 'el' }, - { text: 'en', value: 'en' }, - { text: 'eo', value: 'eo' }, - { text: 'es', value: 'es' }, - { text: 'et', value: 'et' }, - { text: 'eu', value: 'eu' }, - { text: 'fa', value: 'fa' }, - { text: 'ff', value: 'ff' }, - { text: 'fi', value: 'fi' }, - { text: 'fj', value: 'fj' }, - { text: 'fo', value: 'fo' }, - { text: 'fr', value: 'fr' }, - { text: 'fy', value: 'fy' }, - { text: 'ga', value: 'ga' }, - { text: 'gd', value: 'gd' }, - { text: 'gl', value: 'gl' }, - { text: 'gn', value: 'gn' }, - { text: 'gu', value: 'gu' }, - { text: 'gv', value: 'gv' }, - { text: 'ha', value: 'ha' }, - { text: 'he', value: 'he' }, - { text: 'hi', value: 'hi' }, - { text: 'ho', value: 'ho' }, - { text: 'hr', value: 'hr' }, - { text: 'ht', value: 'ht' }, - { text: 'hu', value: 'hu' }, - { text: 'hy', value: 'hy' }, - { text: 'hz', value: 'hz' }, - { text: 'ia', value: 'ia' }, - { text: 'id', value: 'id' }, - { text: 'ie', value: 'ie' }, - { text: 'ig', value: 'ig' }, - { text: 'ii', value: 'ii' }, - { text: 'ik', value: 'ik' }, - { text: 'io', value: 'io' }, - { text: 'is', value: 'is' }, - { text: 'it', value: 'it' }, - { text: 'iu', value: 'iu' }, - { text: 'ja', value: 'ja' }, - { text: 'jv', value: 'jv' }, - { text: 'ka', value: 'ka' }, - { text: 'kg', value: 'kg' }, - { text: 'ki', value: 'ki' }, - { text: 'kj', value: 'kj' }, - { text: 'kk', value: 'kk' }, - { text: 'kl', value: 'kl' }, - { text: 'km', value: 'km' }, - { text: 'kn', value: 'kn' }, - { text: 'ko', value: 'ko' }, - { text: 'kr', value: 'kr' }, - { text: 'ks', value: 'ks' }, - { text: 'ku', value: 'ku' }, - { text: 'kv', value: 'kv' }, - { text: 'kw', value: 'kw' }, - { text: 'ky', value: 'ky' }, - { text: 'la', value: 'la' }, - { text: 'lb', value: 'lb' }, - { text: 'lg', value: 'lg' }, - { text: 'li', value: 'li' }, - { text: 'ln', value: 'ln' }, - { text: 'lo', value: 'lo' }, - { text: 'lt', value: 'lt' }, - { text: 'lu', value: 'lu' }, - { text: 'lv', value: 'lv' }, - { text: 'mg', value: 'mg' }, - { text: 'mh', value: 'mh' }, - { text: 'mi', value: 'mi' }, - { text: 'mk', value: 'mk' }, - { text: 'ml', value: 'ml' }, - { text: 'mn', value: 'mn' }, - { text: 'mr', value: 'mr' }, - { text: 'ms', value: 'ms' }, - { text: 'mt', value: 'mt' }, - { text: 'my', value: 'my' }, - { text: 'na', value: 'na' }, - { text: 'nb', value: 'nb' }, - { text: 'nd', value: 'nd' }, - { text: 'ne', value: 'ne' }, - { text: 'ng', value: 'ng' }, - { text: 'nl', value: 'nl' }, - { text: 'nn', value: 'nn' }, - { text: 'no', value: 'no' }, - { text: 'nr', value: 'nr' }, - { text: 'nv', value: 'nv' }, - { text: 'ny', value: 'ny' }, - { text: 'oc', value: 'oc' }, - { text: 'oj', value: 'oj' }, - { text: 'om', value: 'om' }, - { text: 'or', value: 'or' }, - { text: 'os', value: 'os' }, - { text: 'pa', value: 'pa' }, - { text: 'pi', value: 'pi' }, - { text: 'pl', value: 'pl' }, - { text: 'ps', value: 'ps' }, - { text: 'pt', value: 'pt' }, - { text: 'qu', value: 'qu' }, - { text: 'rm', value: 'rm' }, - { text: 'rn', value: 'rn' }, - { text: 'ro', value: 'ro' }, - { text: 'ru', value: 'ru' }, - { text: 'rw', value: 'rw' }, - { text: 'sa', value: 'sa' }, - { text: 'sc', value: 'sc' }, - { text: 'sd', value: 'sd' }, - { text: 'se', value: 'se' }, - { text: 'sg', value: 'sg' }, - { text: 'si', value: 'si' }, - { text: 'sk', value: 'sk' }, - { text: 'sl', value: 'sl' }, - { text: 'sm', value: 'sm' }, - { text: 'sn', value: 'sn' }, - { text: 'so', value: 'so' }, - { text: 'sq', value: 'sq' }, - { text: 'sr', value: 'sr' }, - { text: 'ss', value: 'ss' }, - { text: 'st', value: 'st' }, - { text: 'su', value: 'su' }, - { text: 'sv', value: 'sv' }, - { text: 'sw', value: 'sw' }, - { text: 'ta', value: 'ta' }, - { text: 'te', value: 'te' }, - { text: 'tg', value: 'tg' }, - { text: 'th', value: 'th' }, - { text: 'ti', value: 'ti' }, - { text: 'tk', value: 'tk' }, - { text: 'tl', value: 'tl' }, - { text: 'tn', value: 'tn' }, - { text: 'to', value: 'to' }, - { text: 'tr', value: 'tr' }, - { text: 'ts', value: 'ts' }, - { text: 'tt', value: 'tt' }, - { text: 'tw', value: 'tw' }, - { text: 'ty', value: 'ty' }, - { text: 'ug', value: 'ug' }, - { text: 'uk', value: 'uk' }, - { text: 'ur', value: 'ur' }, - { text: 'uz', value: 'uz' }, - { text: 've', value: 've' }, - { text: 'vi', value: 'vi' }, - { text: 'vo', value: 'vo' }, - { text: 'wa', value: 'wa' }, - { text: 'wo', value: 'wo' }, - { text: 'xh', value: 'xh' }, - { text: 'yi', value: 'yi' }, - { text: 'yo', value: 'yo' }, - { text: 'za', value: 'za' }, - { text: 'zh', value: 'zh' }, - { text: 'zu', value: 'zu' } - ] - } - }, - computed: { - loadingColor () { - return this.error ? 'red lighten-2' : 'primary' - }, - token () { - return this.$store.state.token - }, - config () { - if (this.token === null) { - return {} - } - return { - headers: { Authorization: `Bearer ${this.token}` } - } - } - }, - mounted () { - this.loadLicenses() - this.loadUsers() - this.identifier.title = this.database.name - this.identifier.publication_year = parseInt(new Date().getFullYear()) - }, - methods: { - printUser (item) { - return formatUser(item) - }, - submit () { - this.$refs.form.validate() - }, - cancel () { - this.$emit('close-dialog', { success: false }) - }, - reset () { - this.menu = false - }, - async loadLicenses () { - try { - this.loading = true - const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/license`) - this.licenses = res.data - console.debug('licenses', this.licenses) - } catch (err) { - this.error = true - this.$toast.error('Failed to fetch licenses') - } - this.loading = false - }, - async loadUsers () { - try { - this.loading = true - const res = await this.$axios.get('/api/user') - this.users = res.data - console.debug('users', this.users) - } catch (err) { - this.error = true - this.$toast.error('Failed to fetch users') - } - this.loading = false - }, - async persist () { - this.loading = true - try { - this.loading = true - const res = await this.$axios.post('/api/identifier', this.identifier, this.config) - console.debug('persist', res.data) - this.$toast.success('Database persisted.') - this.$emit('close-dialog', { action: 'persisted', success: true }) - } catch (err) { - this.error = true - this.loading = false - this.$toast.error('Failed to persist database') - console.error('persist failed', err) - return - } - this.loading = false - } - } -} -</script> -<style scoped> -</style> diff --git a/fda-ui/pages/container/_container_id/database/_database_id/info.vue b/fda-ui/pages/container/_container_id/database/_database_id/info.vue index c5e15c0bbb0792102ea61eb16df999ccad2f8214..e6009507e31100af4d9f35fb64c9ae9e143f348d 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/info.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/info.vue @@ -24,6 +24,15 @@ <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" /> <span v-if="!loading">{{ publisher }}</span> </v-list-item-content> + <v-list-item-title v-if="database.identifier.creators.length > 0" class="mt-2"> + Creators + </v-list-item-title> + <v-list-item-content> + <span v-for="(person_or_org, i) in database.identifier.creators" :key="`c-${i}`" class="mt-1"> + <OrcidIcon v-if="person_or_org.orcid" :orcid="person_or_org.orcid" /> + {{ person_or_org.name }} <sup v-if="person_or_org.affiliation">{{ person_or_org.affiliation }}</sup> + </span> + </v-list-item-content> <v-list-item-title v-if="language" class="mt-2"> Language </v-list-item-title> @@ -38,6 +47,33 @@ <v-skeleton-loader v-if="loading" type="text" class="skeleton-small" /> <span v-if="!loading" v-text="publication" /> </v-list-item-content> + <v-list-item-title v-if="database.identifier.related.length > 0" class="mt-2"> + Related Identifiers + </v-list-item-title> + <v-list-item-content v-if="database.identifier.related.length > 0"> + <div v-for="(rel, i) in database.identifier.related" :key="`r-${i}`"> + <span v-if="rel.type === 'DOI'"> + {{ rel.type }}: <a :href="`https://doi.org/${rel.value}`" target="_blank">{{ rel.value }}</a> + <span v-if="rel.relation">({{ rel.relation }})</span> + </span> + <span v-if="rel.type === 'URL'"> + {{ rel.type }}: <a :href="`${rel.value}`" target="_blank">{{ rel.value }}</a> + <span v-if="rel.relation">({{ rel.relation }})</span> + </span> + <span v-if="rel.type === 'arXiv'"> + {{ rel.type }}: <a :href="`https://arxiv.org/abs/${rel.value}`" target="_blank">{{ rel.value }}</a> + <span v-if="rel.relation">({{ rel.relation }})</span> + </span> + <span v-if="rel.type === 'EISSN'"> + {{ rel.type }}: <a :href="`https://portal.issn.org/resource/ISSN/${rel.value}`" target="_blank">{{ rel.value }}</a> + <span v-if="rel.relation">({{ rel.relation }})</span> + </span> + <span v-if="rel.type !== 'DOI' && rel.type !== 'URL' && rel.type !== 'arXiv' && rel.type !== 'EISSN'"> + {{ rel.type }}: {{ rel.value }} + <span v-if="rel.relation">({{ rel.relation }})</span> + </span> + </div> + </v-list-item-content> <v-list-item-title v-if="database.identifier.license" class="mt-2"> License </v-list-item-title> @@ -49,6 +85,16 @@ </v-list-item-content> </v-list-item> </v-list> + <v-card-actions> + <v-btn + v-if="hasIdentifier" + small + color="secondary" + :loading="metadataLoading" + @click.stop="download"> + <v-icon left>mdi-code-tags</v-icon> Metadata .xml + </v-btn> + </v-card-actions> </v-card-text> </v-card> <v-divider v-if="hasIdentifier" /> @@ -146,7 +192,7 @@ v-model="editDbDialog" persistent max-width="860"> - <PersistDatabase :database="database" @close-dialog="closeDialog" /> + <Persist type="database" @close="closeDialog" /> </v-dialog> <v-dialog v-model="editVisibilityDialog" @@ -179,8 +225,9 @@ <script> import DBToolbar from '@/components/DBToolbar' -import PersistDatabase from '@/components/dialogs/PersistDatabase' +import Persist from '@/components/dialogs/Persist' import EditVisibility from '@/components/dialogs/EditVisibility' +import OrcidIcon from '@/components/icons/OrcidIcon' import { formatTimestampUTCLabel, formatUser } from '@/utils' import { decodeJwt } from 'jose' @@ -188,13 +235,15 @@ export default { components: { EditVisibility, DBToolbar, - PersistDatabase + Persist, + OrcidIcon }, data () { return { loading: false, editDbDialog: false, editVisibilityDialog: false, + metadataLoading: false, user: { username: null }, @@ -338,12 +387,32 @@ export default { this.loading = false }, closeDialog (event) { - if (event.success) { + if (event.action === 'persisted') { this.loadDatabase() } this.editDbDialog = false this.editVisibilityDialog = false }, + async download () { + this.metadataLoading = true + try { + const config = this.config + config.headers.Accept = 'text/xml' + const res = await this.$axios.get(`/api/pid/${this.database.identifier.id}`, config) + console.debug('export identifier', res) + const url = window.URL.createObjectURL(new Blob([res.data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', 'identifier.xml') + document.body.appendChild(link) + link.click() + } catch (err) { + console.error('Could not export identifier', err) + this.$toast.error('Could not export identifier') + this.error = true + } + this.metadataLoading = false + }, loadUser () { if (!this.token) { return diff --git a/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue b/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue index fcfe0476215d16cc33d77c068f32cd06fb15a227..2ecde3b1de04b138ca29d7ec4a2ef9626d881598 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue @@ -18,7 +18,10 @@ <v-btn v-if="token && query.is_persisted && !identifier.id && !loadingIdentifier && is_owner" class="mb-1 mr-2" color="primary" :disabled="error || erroneous || !executionUTC" @click.stop="openDialog()"> <v-icon left>mdi-content-save-outline</v-icon> Get PID </v-btn> - <v-btn v-if="result_visibility" class="mb-1" :loading="downloadLoading" @click.stop="download('text/csv')"> + <v-btn v-if="result_visibility && !identifier.id" class="mb-1" :loading="downloadLoading" @click.stop="downloadData"> + <v-icon left>mdi-download</v-icon> Data .csv + </v-btn> + <v-btn v-if="result_visibility && identifier.id" class="mb-1" :loading="downloadLoading" @click.stop="download('text/csv')"> <v-icon left>mdi-download</v-icon> Data .csv </v-btn> <v-btn @@ -241,12 +244,12 @@ v-model="persistQueryDialog" persistent max-width="860"> - <PersistQuery @close="closeDialog" /> + <Persist @close="closeDialog" /> </v-dialog> </div> </template> <script> -import PersistQuery from '@/components/dialogs/PersistQuery' +import Persist from '@/components/dialogs/Persist' import OrcidIcon from '@/components/icons/OrcidIcon' import { formatTimestampUTCLabel, formatDateUTC } from '@/utils' import { decodeJwt } from 'jose' @@ -254,7 +257,7 @@ import { decodeJwt } from 'jose' export default { name: 'QueryShow', components: { - PersistQuery, + Persist, OrcidIcon }, data () { @@ -449,7 +452,11 @@ export default { this.$refs.queryResults.reExecute(this.query.id) }, async download (mime) { - this.downloadLoading = true + if (mime === 'text/csv') { + this.downloadLoading = true + } else if (mime === 'text/xml') { + this.metadataLoading = true + } try { const config = this.config config.headers.Accept = mime @@ -471,6 +478,27 @@ export default { this.error = true } this.downloadLoading = false + this.metadataLoading = false + }, + async downloadData () { + this.downloadLoading = true + try { + const config = this.config + config.headers.Accept = 'text/csv' + const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/query/${this.$route.params.query_id}/export`, config) + console.debug('export query data', res) + const url = window.URL.createObjectURL(new Blob([res.data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', 'subset.csv') + document.body.appendChild(link) + link.click() + } catch (err) { + console.error('Could not export query data', err) + this.$toast.error('Could not export query data') + this.error = true + } + this.downloadLoading = false }, async loadQuery () { this.loadingQuery = true