diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dc922e66f2054c74d7b15fd62b57f8609f6d16c3..c05352a0fc318df99098d9bc409c1e2cc54de00d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -134,6 +134,7 @@ frontend-test: deploy: stage: deploy cache: [] + needs: [] before_script: - mkdir -p ~/.ssh - echo "${SSH_BKY_DEPLOY}" > ~/.ssh/id_rsa diff --git a/Screenshot from 2024-04-22 20-07-48.png b/Screenshot from 2024-04-22 20-07-48.png new file mode 100644 index 0000000000000000000000000000000000000000..632e3f2ffc4c87224b7a19b31a88351709819b75 Binary files /dev/null and b/Screenshot from 2024-04-22 20-07-48.png differ diff --git a/manual_deploy.sh b/manual_deploy.sh index afbab0850dd5e29b1a854de8359271eadb454feb..372c68c8211019953c3331170a5211ad24dc9dd8 100755 --- a/manual_deploy.sh +++ b/manual_deploy.sh @@ -1,5 +1,5 @@ #!/bin/bash -echo "Deploying Gitsearch in directory ${GITSEARCH_PATH}" +echo "Deploying Gitsearch in directory ${GITSEARCH_PATH}" if [[ -z "${CI_COMMIT_REF_NAME}" ]]; then @@ -17,6 +17,7 @@ else else # Handle other branches or provide a default echo "Using default or other branch settings for ${GITBRANCH}" + source src/main/docker/.env fi echo "Deploying via pipeline" diff --git a/pom.xml b/pom.xml index 7f53733c0a26170a98218bdc9498663f98acbb1b..592ec78290e0ded48f12c9d4fc4b9f127906ea5a 100644 --- a/pom.xml +++ b/pom.xml @@ -158,7 +158,7 @@ <dependency> <groupId>org.codeability.sharing</groupId> <artifactId>SharingPluginPlatformAPI</artifactId> - <version>0.4.16</version> + <version>0.4.17</version> </dependency> <dependency> <groupId>org.springdoc</groupId> @@ -185,6 +185,16 @@ <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-collections4</artifactId> + <version>4.4</version> + </dependency> + <dependency> + <groupId>org.apache.tika</groupId> + <artifactId>tika-core</artifactId> + <version>2.9.1</version> + </dependency> <dependency> <groupId>javax.cache</groupId> <artifactId>cache-api</artifactId> @@ -304,6 +314,10 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-webflux</artifactId> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> diff --git a/src/main/docker/gitsearch.yml b/src/main/docker/gitsearch.yml index 0ff2cdd939a0d303a7fb4b1d21fc806ce275e4e3..f5bb1a365e9ea32a7ea12254931c1245c30144fb 100644 --- a/src/main/docker/gitsearch.yml +++ b/src/main/docker/gitsearch.yml @@ -33,6 +33,8 @@ services: - MAIL_USERNAME=${MAIL_USERNAME} - MAIL_PASSWORD=${MAIL_PASSWORD} - MYSQL_PASSWORD=${MYSQL_PASSWORD} + - EDU_SHARING_USER=${EDU_SHARING_USER} + - EDU_SHARING_PASSWORD=${EDU_SHARING_PASSWORD} #KEYCLOAK # - SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=http://keycloak:9080/auth/realms/jhipster # - SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=web_app diff --git a/src/main/java/at/ac/uibk/gitsearch/config/EduSharingApiConfiguration.java b/src/main/java/at/ac/uibk/gitsearch/config/EduSharingApiConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..c7bd43c7b834a6e58e080c157d9a61a4d5c1b92a --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/config/EduSharingApiConfiguration.java @@ -0,0 +1,107 @@ +package at.ac.uibk.gitsearch.config; + +import at.ac.uibk.gitsearch.edu_sharing.model.EditorialGroup; +import at.ac.uibk.gitsearch.properties.ApplicationProperties; +import at.ac.uibk.gitsearch.service.edu_sharing.EduSharingConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Configuration +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public class EduSharingApiConfiguration { + + @Autowired + private ApplicationProperties applicationProperties; + + @Value("${edu-sharing-integration.enabled}") + private boolean enabled; + + @SuppressWarnings("PMD.ImmutableField") + @Value("${edu-sharing-integration.base-url}") + private String eduSharingUrl; + + @SuppressWarnings("PMD.ImmutableField") + @Value("${edu-sharing-integration.auth.username}") + private String user = "admin"; + + @SuppressWarnings("PMD.ImmutableField") + @Value("${edu-sharing-integration.auth.password}") + private String password = "admin"; + + @SuppressWarnings("PMD.ImmutableField") + @Value("${edu-sharing-integration.content.editorial.group-authority-name}") + private String editorialGroupAuthorityName; + + @SuppressWarnings("PMD.ImmutableField") + @Value("${edu-sharing-integration.content.repository}") + private String repository; + + @SuppressWarnings("PMD.ImmutableField") + @Value("${edu-sharing-integration.content.base-node}") + private String baseNode; + + @SuppressWarnings("PMD.ImmutableField") + @Value("${edu-sharing-integration.content.editorial.status-to-check}") + private String workflowToCheckStatus; + + @Bean + WebClient webClient() { + return WebClient + .builder() + .baseUrl(eduSharingUrl) + .filter(new EduSharingLogger()) + .defaultHeaders(header -> header.setBasicAuth(user, password)) + .build(); + } + + @Bean + EduSharingConfiguration eduSharingConfiguration() { + final var editorialGroup = new EditorialGroup(); + editorialGroup.setEditable(true); + editorialGroup.setAuthorityName(editorialGroupAuthorityName); + editorialGroup.setAuthorityType("GROUP"); + return new EduSharingConfiguration( + enabled, + editorialGroup, + repository, + baseNode, + workflowToCheckStatus, + this.applicationProperties.getFrontEndUrl() + ); + } + + public static class EduSharingLogger implements ExchangeFilterFunction { + + private static final Logger LOGGER = LoggerFactory.getLogger(EduSharingLogger.class); + + @Override + public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Request: {} {}", request.method(), request.url()); + request.headers().forEach((name, values) -> values.forEach(value -> LOGGER.debug("{}={}", name, value))); + } + + return next + .exchange(request) + .doOnNext(response -> { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Response: {}", response.statusCode()); + response + .headers() + .asHttpHeaders() + .forEach((name, values) -> values.forEach(value -> LOGGER.debug("{}={}", name, value))); + } + }); + } + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/domain/LinkedEduSharingProject.java b/src/main/java/at/ac/uibk/gitsearch/domain/LinkedEduSharingProject.java new file mode 100644 index 0000000000000000000000000000000000000000..710789a16c7b62963798ccd18ee6eb94d2fd3bb6 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/domain/LinkedEduSharingProject.java @@ -0,0 +1,111 @@ +package at.ac.uibk.gitsearch.domain; + +import java.io.Serializable; +import java.time.Instant; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +@Entity +@Table(name = "linked_edu_sharing_project") +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class LinkedEduSharingProject implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id") + private Long id; + + @NotNull + @Column(name = "resource_id", unique = true) + private String resourceId; + + @NotNull + @Column(name = "edu_sharing_id") + private String eduSharingId; + + @NotNull + @Column(name = "created_at") + private Instant createdAt; + + @NotNull + @Column(name = "updated_at") + private Instant updatedAt; + + public LinkedEduSharingProject() {} + + public LinkedEduSharingProject(@NotNull String resourceId, @NotNull String eduSharingId) { + this.resourceId = resourceId; + this.eduSharingId = eduSharingId; + var now = Instant.now(); + this.createdAt = now; + this.updatedAt = now; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @NotNull + public String getResourceId() { + return resourceId; + } + + public void setResourceId(@NotNull String projectID) { + this.resourceId = projectID; + } + + @NotNull + public String getEduSharingId() { + return eduSharingId; + } + + public void setEduSharingId(@NotNull String eduSharingID) { + this.eduSharingId = eduSharingID; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof LinkedEduSharingProject)) { + return false; + } + LinkedEduSharingProject that = (LinkedEduSharingProject) o; + return Objects.equals(getId(), that.getId()); + } + + @Override + public int hashCode() { + return Objects.hash(getId()); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/AggregationLevel.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/AggregationLevel.java new file mode 100644 index 0000000000000000000000000000000000000000..699f75d76a59627ed1114648764c07df5e9cfe65 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/AggregationLevel.java @@ -0,0 +1,21 @@ +package at.ac.uibk.gitsearch.edu_sharing.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum AggregationLevel { + ATOMIC("1"), + MATERIALS("2"), + COURSE("3"); + + private String eduSharingRepresentation; + + AggregationLevel(String eduSharingRepresentation) { + this.eduSharingRepresentation = eduSharingRepresentation; + } + + public String getEduSharingRepresentation() { + return eduSharingRepresentation; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EditorialGroup.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EditorialGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..c867e115da7dbc9f8b77d7b64c3db8fb51abacef --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EditorialGroup.java @@ -0,0 +1,38 @@ +package at.ac.uibk.gitsearch.edu_sharing.model; + +import java.io.Serializable; + +public class EditorialGroup implements Serializable { + + private static final long serialVersionUID = 1L; + + private boolean editable; + + private String authorityName; + + private String authorityType; + + public boolean isEditable() { + return editable; + } + + public void setEditable(boolean editable) { + this.editable = editable; + } + + public String getAuthorityName() { + return authorityName; + } + + public void setAuthorityName(String authorityName) { + this.authorityName = authorityName; + } + + public String getAuthorityType() { + return authorityType; + } + + public void setAuthorityType(String authorityType) { + this.authorityType = authorityType; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingMetadataDTO.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingMetadataDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..ab1a73e99f61df26b1bf77185e5271d090c95bd2 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingMetadataDTO.java @@ -0,0 +1,258 @@ +package at.ac.uibk.gitsearch.edu_sharing.model; + +import at.ac.uibk.gitsearch.edu_sharing.model.serializer.*; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import org.apache.commons.collections4.CollectionUtils; +import org.codeability.sharing.plugins.api.search.PersonDTO; + +public class EduSharingMetadataDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + @JsonProperty("cclom:version") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String version; + + @JsonProperty("cclom:title") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String title; + + @JsonProperty("cm:name") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String name; + + @JsonProperty("cclom:general_description") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String description; + + @JsonUnwrapped + private LicenceDTO licence; + + @JsonProperty("ccm:metadatacontributer_creator") + @JsonSerialize(using = VCardDTOSerializer.class) + @JsonDeserialize(using = VCardDTODeserializer.class) + private List<PersonDTO> metadataContributor; + + @JsonProperty("ccm:lifecyclecontributer_author") + @JsonSerialize(using = VCardDTOSerializer.class) + @JsonDeserialize(using = VCardDTODeserializer.class) + private List<PersonDTO> contentContributor; + + @JsonProperty("cclom:general_language") + private List<String> languages; + + /** + * file format + */ + @JsonProperty("cclom:format") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String format; + + @JsonProperty("cclom:general_keyword") + private List<String> keywords; + + @JsonProperty("cclom:aggregationlevel") + @JsonSerialize(using = AggregationLevelSerializer.class) + @JsonDeserialize(using = AggregationLevelDeserializer.class) + private AggregationLevel aggregationLevel; + + @JsonProperty("ccm:educationallearningresourcetype") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String educationalLearningResourceType = "https://w3id.org/kim/hcrt/drill_and_practice"; + + @JsonProperty("ccm:educationalcontext") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String educationalContext; + + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + @JsonProperty("ccm:taxonid") + private String taxonomy; + + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + @JsonProperty("ccm:furtherReferences") + private String reverseLink; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public LicenceDTO getLicence() { + return licence; + } + + public void setLicence(LicenceDTO licence) { + this.licence = licence; + } + + public List<PersonDTO> getMetadataContributor() { + return metadataContributor; + } + + public void setMetadataContributor(List<PersonDTO> metadataContributor) { + this.metadataContributor = metadataContributor; + } + + public List<PersonDTO> getContentContributor() { + return contentContributor; + } + + public void setContentContributor(List<PersonDTO> contentContributor) { + this.contentContributor = contentContributor; + } + + public List<String> getLanguages() { + return languages; + } + + public void setLanguages(List<String> languages) { + this.languages = languages; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public List<String> getKeywords() { + return keywords; + } + + public void setKeywords(List<String> keywords) { + this.keywords = keywords; + } + + public AggregationLevel getAggregationLevel() { + return aggregationLevel; + } + + public void setAggregationLevel(AggregationLevel aggregationLevel) { + this.aggregationLevel = aggregationLevel; + } + + public String getEducationalLearningResourceType() { + return educationalLearningResourceType; + } + + public void setEducationalLearningResourceType(String educationalLearningResourceType) { + this.educationalLearningResourceType = educationalLearningResourceType; + } + + public String getEducationalContext() { + return educationalContext; + } + + public void setEducationalContext(String educationalContext) { + this.educationalContext = educationalContext; + } + + public String getTaxonomy() { + return taxonomy; + } + + public void setTaxonomy(String taxonomy) { + this.taxonomy = taxonomy; + } + + public String getReverseLink() { + return reverseLink; + } + + public void setReverseLink(String reverseLink) { + this.reverseLink = reverseLink; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof EduSharingMetadataDTO)) { + return false; + } + EduSharingMetadataDTO that = (EduSharingMetadataDTO) o; + return ( + Objects.equals(getVersion(), that.getVersion()) && + Objects.equals(getTitle(), that.getTitle()) && + Objects.equals(getName(), that.getName()) && + Objects.equals(getDescription(), that.getDescription()) && + Objects.equals(getLicence(), that.getLicence()) && + CollectionUtils.isEqualCollection(this.getMetadataContributor(), that.getMetadataContributor()) && + CollectionUtils.isEqualCollection(getContentContributor(), that.getContentContributor()) && + Objects.equals(getLanguages(), that.getLanguages()) && + Objects.equals(getFormat(), that.getFormat()) && + Objects.equals(getKeywords(), that.getKeywords()) && + getAggregationLevel() == that.getAggregationLevel() && + Objects.equals(getEducationalLearningResourceType(), that.getEducationalLearningResourceType()) && + Objects.equals(getEducationalContext(), that.getEducationalContext()) && + Objects.equals(getTaxonomy(), that.getTaxonomy()) && + Objects.equals(getReverseLink(), that.getReverseLink()) + ); + } + + @Override + public int hashCode() { + return Objects.hash( + getVersion(), + getTitle(), + getName(), + getDescription(), + getLicence(), + getMetadataContributor(), + getContentContributor(), + getLanguages(), + getFormat(), + getKeywords(), + getAggregationLevel(), + getEducationalLearningResourceType(), + getEducationalContext(), + getTaxonomy(), + getReverseLink() + ); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingProjectDTO.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingProjectDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..7703dab6d211ee6e31f947fb28c17ca33981dd24 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingProjectDTO.java @@ -0,0 +1,44 @@ +package at.ac.uibk.gitsearch.edu_sharing.model; + +import java.time.Instant; + +public class EduSharingProjectDTO { + + private String nodeId; + + private String viewUrl; + + private Instant createdAt; + + public EduSharingProjectDTO() {} + + public EduSharingProjectDTO(String nodeId, String viewUrl, Instant createdAt) { + this.nodeId = nodeId; + this.viewUrl = viewUrl; + this.createdAt = createdAt; + } + + public String getNodeId() { + return nodeId; + } + + public void setNodeId(String nodeId) { + this.nodeId = nodeId; + } + + public String getViewUrl() { + return viewUrl; + } + + public void setViewUrl(String viewUrl) { + this.viewUrl = viewUrl; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingStatusDTO.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingStatusDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..59c5e176f21b5e69034e15503f7ef861539af5a7 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingStatusDTO.java @@ -0,0 +1,64 @@ +package at.ac.uibk.gitsearch.edu_sharing.model; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.time.Instant; +import java.util.List; +import org.codeability.sharing.plugins.api.jsonSupport.UTCInstantSerializer; + +public class EduSharingStatusDTO { + + private EduSharingProjectDTO baseEduSharingProject; + + private List<EduSharingProjectDTO> publishedCopies; + + private List<EduSharingWorkflowDTO> workflows; + + @JsonSerialize(converter = UTCInstantSerializer.class) + private Instant lastUpdate; + + public EduSharingStatusDTO() {} + + public EduSharingStatusDTO( + EduSharingProjectDTO baseEduSharingProject, + List<EduSharingProjectDTO> publishedCopies, + List<EduSharingWorkflowDTO> workflows, + Instant lastUpdate + ) { + this.baseEduSharingProject = baseEduSharingProject; + this.publishedCopies = publishedCopies; + this.workflows = workflows; + this.lastUpdate = lastUpdate; + } + + public EduSharingProjectDTO getBaseEduSharingProject() { + return baseEduSharingProject; + } + + public void setBaseEduSharingProject(EduSharingProjectDTO baseEduSharingProject) { + this.baseEduSharingProject = baseEduSharingProject; + } + + public List<EduSharingProjectDTO> getPublishedCopies() { + return publishedCopies; + } + + public void setPublishedCopies(List<EduSharingProjectDTO> publishedCopies) { + this.publishedCopies = publishedCopies; + } + + public List<EduSharingWorkflowDTO> getWorkflows() { + return workflows; + } + + public void setWorkflows(List<EduSharingWorkflowDTO> workflows) { + this.workflows = workflows; + } + + public Instant getLastUpdate() { + return lastUpdate; + } + + public void setLastUpdate(Instant lastUpdate) { + this.lastUpdate = lastUpdate; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingWorkflowDTO.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingWorkflowDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..ae9f8f4df99371996399bc5a027b58698eaa0148 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/EduSharingWorkflowDTO.java @@ -0,0 +1,51 @@ +package at.ac.uibk.gitsearch.edu_sharing.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.time.Instant; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class EduSharingWorkflowDTO implements Comparable<EduSharingWorkflowDTO> { + + private Instant time; + + private String status; + + private String comment; + + public EduSharingWorkflowDTO() {} + + public EduSharingWorkflowDTO(Instant time, String status, String comment) { + this.time = time; + this.status = status; + this.comment = comment; + } + + @Override + public int compareTo(EduSharingWorkflowDTO eduSharingWorkflow) { + return this.getTime().compareTo(eduSharingWorkflow.getTime()); + } + + public Instant getTime() { + return time; + } + + public void setTime(Instant time) { + this.time = time; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/LicenceDTO.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/LicenceDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..28a408b739b76721de62de8cd5b14bbe7897d691 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/LicenceDTO.java @@ -0,0 +1,109 @@ +package at.ac.uibk.gitsearch.edu_sharing.model; + +import at.ac.uibk.gitsearch.edu_sharing.model.serializer.DefaultNodeDeserializer; +import at.ac.uibk.gitsearch.edu_sharing.model.serializer.DefaultNodeSerializer; +import at.ac.uibk.gitsearch.edu_sharing.model.serializer.IllegalCreativeCommonsException; +import at.ac.uibk.gitsearch.edu_sharing.model.serializer.LicenseSerializationException; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import java.io.Serializable; +import java.util.Objects; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; + +public class LicenceDTO implements Serializable { + + public static final Pattern CC_0_PATTERN = Pattern.compile("CC-?_? ?0"); + public static final Pattern PDM_PATTERN = Pattern.compile("PDM"); + public static final Pattern CC_BY_SA_PATTERN = Pattern.compile("(CC(([-_ ]BY[-_ ]?SA)|[-_ ]SA[-_ ]BY?))[-_ ]?(4.0)"); + + public static final Pattern CC_ANY = Pattern.compile("CC.*"); + + public static final String CC_0_REPRESENTATION = "CC_0"; + public static final String CC_BY_SA_REPRESENTATION = "CC_BY_SA"; + + public static final String CC_BY_SA_VERSION = "4.0"; + private static final long serialVersionUID = 1L; + + @JsonIgnore + private String sharingRepresentation; + + @JsonProperty("ccm:commonlicense_key") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String eduSharingLicenseKey; + + @JsonProperty("ccm:commonlicense_cc_version") + @JsonSerialize(using = DefaultNodeSerializer.class) + @JsonDeserialize(using = DefaultNodeDeserializer.class) + private String eduSharingLicenseVersion; + + public LicenceDTO() {} + + public LicenceDTO(String sharingRepresentation) throws LicenseSerializationException { + this.sharingRepresentation = sharingRepresentation; + this.setEduSharingFields(sharingRepresentation); + } + + public String getSharingRepresentation() { + if (StringUtils.isNotBlank(sharingRepresentation)) { + return this.sharingRepresentation; + } + return String.join(" ", eduSharingLicenseKey.replace("_", "-"), eduSharingLicenseVersion); + } + + public String getEduSharingLicenseKey() { + return eduSharingLicenseKey; + } + + public void setEduSharingLicenseKey(String eduSharingLicenseKey) { + this.eduSharingLicenseKey = eduSharingLicenseKey; + } + + public String getEduSharingLicenseVersion() { + return eduSharingLicenseVersion; + } + + public void setEduSharingLicenseVersion(String eduSharingLicenseVersion) { + this.eduSharingLicenseVersion = eduSharingLicenseVersion; + } + + private void setEduSharingFields(String value) throws LicenseSerializationException { + this.eduSharingLicenseVersion = ""; + if (CC_0_PATTERN.matcher(value).matches()) { + this.eduSharingLicenseKey = CC_0_REPRESENTATION; + } else if (PDM_PATTERN.matcher(value).matches()) { + this.eduSharingLicenseKey = CC_0_REPRESENTATION; // PDM is equivalent to CC-0 + } else if (CC_BY_SA_PATTERN.matcher(value).matches()) { + this.eduSharingLicenseVersion = CC_BY_SA_VERSION; + this.eduSharingLicenseKey = CC_BY_SA_REPRESENTATION; + } else if (CC_ANY.matcher(value).matches()) { + throw new IllegalCreativeCommonsException(value); + } else { + throw new LicenseSerializationException(value); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof LicenceDTO)) { + return false; + } + LicenceDTO that = (LicenceDTO) o; + return ( + Objects.equals(getSharingRepresentation(), that.getSharingRepresentation()) && + Objects.equals(getEduSharingLicenseKey(), that.getEduSharingLicenseKey()) && + Objects.equals(getEduSharingLicenseVersion(), that.getEduSharingLicenseVersion()) + ); + } + + @Override + public int hashCode() { + return Objects.hash(getSharingRepresentation(), getEduSharingLicenseKey(), getEduSharingLicenseVersion()); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/AggregationLevelDeserializer.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/AggregationLevelDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..5b27b9ba3ef83912b0e3226672fea15030b8d0fd --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/AggregationLevelDeserializer.java @@ -0,0 +1,39 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import at.ac.uibk.gitsearch.edu_sharing.model.AggregationLevel; +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.TextNode; +import java.io.IOException; + +public class AggregationLevelDeserializer extends JsonDeserializer<AggregationLevel> { + + private AggregationLevel fromJson(String representation) { + for (AggregationLevel level : AggregationLevel.values()) { + if (level.getEduSharingRepresentation().equals(representation)) { + return level; + } + } + throw new IllegalArgumentException("Illegal representation"); + } + + @Override + public AggregationLevel deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + JsonNode node = mapper.readTree(p); + if (node instanceof ArrayNode) { + ArrayNode arrayNode = (ArrayNode) node; + if (!arrayNode.isEmpty()) { + return fromJson(mapper.treeToValue(arrayNode.get(0), String.class)); + } + } else if (node instanceof TextNode) { + return fromJson(mapper.treeToValue((TextNode) node, String.class)); + } + return null; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/AggregationLevelSerializer.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/AggregationLevelSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..7c13f0e28a7b6a693275609ed2b552b43a4185ac --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/AggregationLevelSerializer.java @@ -0,0 +1,17 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import at.ac.uibk.gitsearch.edu_sharing.model.AggregationLevel; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; + +public class AggregationLevelSerializer extends JsonSerializer<AggregationLevel> { + + @Override + public void serialize(AggregationLevel value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartArray(); + gen.writeString(value.getEduSharingRepresentation()); + gen.writeEndArray(); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/DefaultNodeDeserializer.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/DefaultNodeDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..b383a692dda770e26b91e1819fed7909a45d1675 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/DefaultNodeDeserializer.java @@ -0,0 +1,29 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.TextNode; +import java.io.IOException; + +public class DefaultNodeDeserializer extends JsonDeserializer<String> { + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + JsonNode node = mapper.readTree(p); + if (node instanceof ArrayNode) { + ArrayNode arrayNode = (ArrayNode) node; + if (!arrayNode.isEmpty()) { + return mapper.treeToValue(arrayNode.get(0), String.class); + } + } else if (node instanceof TextNode) { + return mapper.treeToValue((TextNode) node, String.class); + } + return null; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/DefaultNodeSerializer.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/DefaultNodeSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..9aa76f45ec44aeddd48778e546f6c8cec2b466d4 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/DefaultNodeSerializer.java @@ -0,0 +1,19 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import java.io.IOException; + +public class DefaultNodeSerializer extends JsonSerializer<String> { + + @Override + public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException { + gen.writeStartArray(); + if (value != null) { + gen.writeString(value); + } + gen.writeEndArray(); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/IllegalCreativeCommonsException.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/IllegalCreativeCommonsException.java new file mode 100644 index 0000000000000000000000000000000000000000..777d2971ea1032372a940d249ac06076479eba0f --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/IllegalCreativeCommonsException.java @@ -0,0 +1,10 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +public class IllegalCreativeCommonsException extends LicenseSerializationException { + + private static final long serialVersionUID = 1L; + + public IllegalCreativeCommonsException(String message) { + super(message); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/LicenseSerializationException.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/LicenseSerializationException.java new file mode 100644 index 0000000000000000000000000000000000000000..c638afa457b9a3cd2a354648c1c0fff2350e1b2f --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/LicenseSerializationException.java @@ -0,0 +1,12 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import java.io.IOException; + +public class LicenseSerializationException extends IOException { + + private static final long serialVersionUID = 1L; + + public LicenseSerializationException(String message) { + super(message); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTODeserializer.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTODeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..91612cd2314d4adf1905be943aabc56511bb1bb8 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTODeserializer.java @@ -0,0 +1,63 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.TextNode; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +import org.codeability.sharing.plugins.api.search.PersonDTO; + +public class VCardDTODeserializer extends JsonDeserializer<List<PersonDTO>> { + + @SuppressWarnings( + { "PMD.CyclomaticComplexity", "PMD.NullAssignment", "PMD.ImplicitSwitchFallThrough", "PMD.SwitchStmtsShouldHaveDefault" } + ) + public static PersonDTO getFromVCardString(String vcardString) throws VCardSerializationException { + PersonDTO result = new PersonDTO(); + for (String part : vcardString.split("\n")) { + String key = StringUtils.substringBefore(part, ":"); + String value = StringUtils.substringAfter(part, ":"); + value = StringUtils.isEmpty(value) ? null : value; + if (key == null || key.isBlank()) { + throw new VCardSerializationException(vcardString); + } + switch (key) { + case "FN": + result.setName(value); + break; + case "ORG": + result.setAffiliation(value); + break; + case "EMAIL": + result.setEmail(value); + } + } + if (StringUtils.isEmpty(result.getName())) { + throw new VCardSerializationException(vcardString); + } + return result; + } + + @Override + public List<PersonDTO> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + List<PersonDTO> resultList = new LinkedList<>(); + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + JsonNode node = mapper.readTree(p); + if (node instanceof ArrayNode) { + ArrayNode arrayNode = (ArrayNode) node; + for (JsonNode childNode : arrayNode) { + resultList.add(getFromVCardString(mapper.treeToValue(childNode, String.class))); + } + } else if (node instanceof TextNode) { + resultList.add(getFromVCardString(node.asText())); + } + return resultList; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTOSerializer.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTOSerializer.java new file mode 100644 index 0000000000000000000000000000000000000000..02da1b9949789df35b783108e6c36555acd2532b --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTOSerializer.java @@ -0,0 +1,61 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.codeability.sharing.plugins.api.search.PersonDTO; + +public class VCardDTOSerializer extends JsonSerializer<List<PersonDTO>> { + + private static String VCARD_TEMPLATE = "BEGIN:VCARD\nN:{0}\nFN:{1}\nEMAIL:{2}\nORG:{3}\nVERSION:3.0\nEND:VCARD"; + + @Override + public void serialize(List<PersonDTO> value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartArray(); + for (PersonDTO personDTO : value) { + gen.writeString(getVCardString(personDTO)); + } + gen.writeEndArray(); + } + + public static String getVCardString(PersonDTO personDTO) throws IOException { + return MessageFormat.format( + VCARD_TEMPLATE, + getVCardName(personDTO.getName()), + personDTO.getName(), + Optional.ofNullable(personDTO.getEmail()).orElse(""), + Optional.ofNullable(personDTO.getAffiliation()).orElse("") + ); + } + + @SuppressWarnings("PMD.PrematureDeclaration") + protected static String getVCardName(String fullName) throws IOException { + if (fullName == null || fullName.isBlank()) { + throw new VCardSerializationException(fullName); + } + String name = fullName.contains(".") ? StringUtils.substringAfterLast(fullName, ".") : fullName; + String namePrefix = fullName.contains(".") ? (StringUtils.substringBeforeLast(fullName, ".") + ".").trim() : ""; + String nameSuffix = name.contains(",") ? (StringUtils.substringAfter(name, ",")).trim() : ""; + name = StringUtils.substringBefore(name, ","); + + List<String> nameParts = new ArrayList<>(List.of(StringUtils.split(name, ' '))); + if (nameParts.size() < 2) { + throw new VCardSerializationException(fullName); + } + + String lastName = nameParts.get(nameParts.size() - 1); + String firstName = nameParts.get(0); + nameParts.remove(nameParts.size() - 1); + nameParts.remove(0); + + String middleName = String.join(" ", nameParts); + + return lastName + ";" + firstName + ";" + middleName + ";" + namePrefix + ";" + nameSuffix; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardSerializationException.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardSerializationException.java new file mode 100644 index 0000000000000000000000000000000000000000..7932b9793b6bc3c7e92ba95b353262cf94caceb2 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardSerializationException.java @@ -0,0 +1,12 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import java.io.IOException; + +public class VCardSerializationException extends IOException { + + private static final long serialVersionUID = 1L; + + public VCardSerializationException(String name) { + super(name); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/properties/ApplicationProperties.java b/src/main/java/at/ac/uibk/gitsearch/properties/ApplicationProperties.java index 621a59d4aec9fe11811d0f445b6daf319d324343..ab4e5ac2857a851cdb58cc867f4c168e72d20e6b 100644 --- a/src/main/java/at/ac/uibk/gitsearch/properties/ApplicationProperties.java +++ b/src/main/java/at/ac/uibk/gitsearch/properties/ApplicationProperties.java @@ -12,6 +12,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "application", ignoreUnknownFields = false) public class ApplicationProperties { + private String frontEndUrl; + private Search search; private GitLab gitLab; @@ -100,6 +102,14 @@ public class ApplicationProperties { this.gitLab = gitLab; } + public String getFrontEndUrl() { + return frontEndUrl; + } + + public void setFrontEndUrl(String frontEndUrl) { + this.frontEndUrl = frontEndUrl; + } + /** * @return the registeredConnectorsCallBackURL */ diff --git a/src/main/java/at/ac/uibk/gitsearch/repository/jpa/AuthorityRepository.java b/src/main/java/at/ac/uibk/gitsearch/repository/jpa/AuthorityRepository.java index 1e4ef8636f72591b625ec23ebc3abbd9ec494390..607c0602e8670f71fbf4b841d4a9871223a327b0 100644 --- a/src/main/java/at/ac/uibk/gitsearch/repository/jpa/AuthorityRepository.java +++ b/src/main/java/at/ac/uibk/gitsearch/repository/jpa/AuthorityRepository.java @@ -28,4 +28,7 @@ public interface AuthorityRepository extends JpaRepository<Authority, String> { @Query("SELECT u FROM User u JOIN u.authorities auth WHERE auth.name = :authority") List<User> findAllUsersWithAuthority(@Param("authority") String authority); + + @Query("SELECT COUNT(u) > 0 FROM User u JOIN u.authorities auth WHERE auth.name = :authority AND u = :user") + boolean hasUserAuthority(@Param("authority") String authority, @Param("user") User user); } diff --git a/src/main/java/at/ac/uibk/gitsearch/repository/jpa/LinkedEduSharingProjectRepository.java b/src/main/java/at/ac/uibk/gitsearch/repository/jpa/LinkedEduSharingProjectRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..ddd0d76a48db4dead4ae6172655a40ca5ee58602 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/repository/jpa/LinkedEduSharingProjectRepository.java @@ -0,0 +1,11 @@ +package at.ac.uibk.gitsearch.repository.jpa; + +import at.ac.uibk.gitsearch.domain.LinkedEduSharingProject; +import javax.validation.constraints.NotNull; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface LinkedEduSharingProjectRepository extends JpaRepository<LinkedEduSharingProject, Long> { + LinkedEduSharingProject getByResourceId(@NotNull String resourceId); + + boolean existsByResourceId(@NotNull String resourceId); +} diff --git a/src/main/java/at/ac/uibk/gitsearch/repository/search/MetaDataRepository.java b/src/main/java/at/ac/uibk/gitsearch/repository/search/MetaDataRepository.java index 2feabf907abbfe955d01b81cac623c4a47227aeb..c6ad273a9f3ba3d8fda307b16b012f503807ba26 100644 --- a/src/main/java/at/ac/uibk/gitsearch/repository/search/MetaDataRepository.java +++ b/src/main/java/at/ac/uibk/gitsearch/repository/search/MetaDataRepository.java @@ -63,11 +63,11 @@ import liquibase.repackaged.org.apache.commons.text.StringSubstitutor; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.codeability.sharing.plugins.api.search.PersonDTO; import org.codeability.sharing.plugins.api.search.SearchInputDTO; import org.codeability.sharing.plugins.api.search.SearchOrdering; import org.codeability.sharing.plugins.api.search.SearchResultDTO; import org.codeability.sharing.plugins.api.search.SearchResultsDTO; -import org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO.Person; import org.codeability.sharing.plugins.api.search.util.ExerciseId; import org.elasticsearch.ElasticsearchException; import org.springframework.beans.factory.annotation.Value; @@ -281,7 +281,7 @@ public class MetaDataRepository { } } if (entry.getMetadata().getContributor() != null) { - for (Person contributor : entry.getMetadata().getContributor()) { + for (PersonDTO contributor : entry.getMetadata().getContributor()) { addTo( reloadedCachedCompletions.get(SearchRepositoryConstants.METADATA_CONTRIBUTOR), split(contributor.getName()), @@ -290,7 +290,7 @@ public class MetaDataRepository { } } if (entry.getMetadata().getCreator() != null) { - for (Person creator : entry.getMetadata().getCreator()) { + for (PersonDTO creator : entry.getMetadata().getCreator()) { addTo( reloadedCachedCompletions.get(SearchRepositoryConstants.METADATA_CREATOR), split(creator.getName()), diff --git a/src/main/java/at/ac/uibk/gitsearch/service/AuthorityService.java b/src/main/java/at/ac/uibk/gitsearch/service/AuthorityService.java index f6cfba573a6f6d4aac6c4b01cd5315d10f7a84b4..9896ebddb32e6a0157912cbc33c3cff05acf772c 100644 --- a/src/main/java/at/ac/uibk/gitsearch/service/AuthorityService.java +++ b/src/main/java/at/ac/uibk/gitsearch/service/AuthorityService.java @@ -3,17 +3,22 @@ package at.ac.uibk.gitsearch.service; import at.ac.uibk.gitsearch.domain.User; import at.ac.uibk.gitsearch.repository.jpa.AuthorityRepository; import java.util.List; +import javax.validation.constraints.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; @Service @Transactional +@Validated public class AuthorityService { private final Logger log = LoggerFactory.getLogger(AuthorityService.class); + private static final String ROLE_ADMIN = "ROLE_ADMIN"; + private final AuthorityRepository authorityRepository; public AuthorityService(AuthorityRepository authorityRepository) { @@ -26,4 +31,8 @@ public class AuthorityService { log.info("Found {} users with authority: {}", users.size(), authority); return users; } + + public boolean isUserAdmin(@NotNull User user) { + return this.authorityRepository.hasUserAuthority(ROLE_ADMIN, user); + } } diff --git a/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingConfiguration.java b/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..69b7b4f263cd54cc10806b02ffff9d35c7faf87f --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingConfiguration.java @@ -0,0 +1,53 @@ +package at.ac.uibk.gitsearch.service.edu_sharing; + +import at.ac.uibk.gitsearch.edu_sharing.model.EditorialGroup; + +public class EduSharingConfiguration { + + private final boolean enabled; + private final EditorialGroup editorialGroup; + private final String defaultRepository; + private final String baseNode; + private final String workflowToCheckStatus; + private final String frontEndUrl; + + public EduSharingConfiguration( + boolean enabled, + EditorialGroup editorialGroup, + String defaultRepository, + String baseNode, + String workflowToCheckStatus, + String frontEndUrl + ) { + this.enabled = enabled; + this.editorialGroup = editorialGroup; + this.defaultRepository = defaultRepository; + this.baseNode = baseNode; + this.workflowToCheckStatus = workflowToCheckStatus; + this.frontEndUrl = frontEndUrl; + } + + public boolean isEnabled() { + return enabled; + } + + public EditorialGroup getEditorialGroup() { + return editorialGroup; + } + + public String getDefaultRepository() { + return defaultRepository; + } + + public String getBaseNode() { + return baseNode; + } + + public String getWorkflowToCheckStatus() { + return workflowToCheckStatus; + } + + public String getFrontEndUrl() { + return frontEndUrl; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingDisabledException.java b/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingDisabledException.java new file mode 100644 index 0000000000000000000000000000000000000000..9ee88221cb4f753db6d45a34310deac982f7ed75 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingDisabledException.java @@ -0,0 +1,6 @@ +package at.ac.uibk.gitsearch.service.edu_sharing; + +public class EduSharingDisabledException extends RuntimeException { + + private static final long serialVersionUID = 1L; +} diff --git a/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingService.java b/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingService.java new file mode 100644 index 0000000000000000000000000000000000000000..8742a2a044f5b68cd847ec27256c21e9396f0edd --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/EduSharingService.java @@ -0,0 +1,569 @@ +package at.ac.uibk.gitsearch.service.edu_sharing; + +import at.ac.uibk.gitsearch.domain.LinkedEduSharingProject; +import at.ac.uibk.gitsearch.domain.User; +import at.ac.uibk.gitsearch.edu_sharing.model.AggregationLevel; +import at.ac.uibk.gitsearch.edu_sharing.model.EduSharingMetadataDTO; +import at.ac.uibk.gitsearch.edu_sharing.model.EduSharingProjectDTO; +import at.ac.uibk.gitsearch.edu_sharing.model.EduSharingStatusDTO; +import at.ac.uibk.gitsearch.edu_sharing.model.EduSharingWorkflowDTO; +import at.ac.uibk.gitsearch.edu_sharing.model.LicenceDTO; +import at.ac.uibk.gitsearch.edu_sharing.model.serializer.IllegalCreativeCommonsException; +import at.ac.uibk.gitsearch.edu_sharing.model.serializer.LicenseSerializationException; +import at.ac.uibk.gitsearch.edu_sharing.model.serializer.VCardSerializationException; +import at.ac.uibk.gitsearch.repository.jpa.LinkedEduSharingProjectRepository; +import at.ac.uibk.gitsearch.security.SecurityUtils; +import at.ac.uibk.gitsearch.service.AuthorityService; +import at.ac.uibk.gitsearch.service.SearchService; +import at.ac.uibk.gitsearch.service.UserService; +import at.ac.uibk.gitsearch.service.vocabulary.TranslatedKeywordsCache; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.time.Instant; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import javax.validation.constraints.NotNull; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.tika.Tika; +import org.codeability.sharing.plugins.api.search.PersonDTO; +import org.codeability.sharing.plugins.api.search.SearchResultDTO; +import org.codeability.sharing.plugins.api.search.util.ExerciseId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@Validated +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +public class EduSharingService { + + private static final String DEFAULT_VERSION = "1.4"; + private static final String DEFAULT_FORMAT = "zip"; + private static final String DEFAULT_EDUCATIONAL_LEARNING_RESOURCE_TYPE = "https://w3id.org/kim/hcrt/drill_and_practice"; + private static final String DEFAULT_EDUCATIONAL_CONTEXT = "higher education"; + private static final String DEFAULT_MIME_TYPE = "application/zip"; + private static final String DEFAULT_TAXONOMY = "https://oer-repo.uibk.ac.at/w3id.org/vocabs/oefos2012/1020"; + + private static final String FILE_NAME_DEFAULT_LOGO = "codeability-logo.png"; + private static final String FILE_NAME_JAVA_LOGO = "java-language-logo.png"; + private static final String FILE_NAME_PYTHON_LOGO = "python-language-logo.png"; + private static final String FILE_NAME_C_LOGO = "c-language-logo.png"; + + private static final Logger LOGGER = LoggerFactory.getLogger(EduSharingService.class); + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); + + @Autowired + private SearchService searchService; + + @Autowired + private LinkedEduSharingProjectRepository linkedEduSharingProjectRepository; + + @Autowired + private WebClient webClient; + + @Autowired + private EduSharingConfiguration eduSharingConfiguration; + + @Autowired + private UserService userService; + + @Autowired + private AuthorityService authorityService; + + @Autowired + private TranslatedKeywordsCache translatedKeywordsCache; + + public boolean isEduSharingEnabled() { + return this.eduSharingConfiguration.isEnabled(); + } + + @Transactional(readOnly = true) + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public Optional<EduSharingStatusDTO> getEduSharingStatus(@NotNull String resourceId) { + if (!this.isEduSharingEnabled()) { + throw new EduSharingDisabledException(); + } + + LinkedEduSharingProject linkedEduSharingProject = this.linkedEduSharingProjectRepository.getByResourceId(resourceId); + if (linkedEduSharingProject == null) { + return Optional.empty(); + } + try { + return Optional.of( + new EduSharingStatusDTO( + getEduSharingNode(linkedEduSharingProject.getEduSharingId()), + getPublishedCopiesOfNode(linkedEduSharingProject.getEduSharingId()), + getWorkflowsOfNode(linkedEduSharingProject.getEduSharingId()), + linkedEduSharingProject.getUpdatedAt() + ) + ); + } catch (final Exception e) { + LOGGER.error("error on getEduSharingStatus for resource " + resourceId, e); + } + return Optional.empty(); + } + + private EduSharingProjectDTO getEduSharingNode(@NotNull final String nodeId) throws JsonProcessingException { + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}/metadata") + .build(this.eduSharingConfiguration.getDefaultRepository(), nodeId); + + final var answer = this.webClient.get().uri(uri.toString()).retrieve().bodyToMono(String.class).block(); + + JsonNode root = OBJECT_MAPPER.readTree(answer); + + return new EduSharingProjectDTO( + nodeId, + root.get("node").get("content").get("url").asText(), + Instant.parse(root.get("node").get("createdAt").asText()) + ); + } + + private List<EduSharingProjectDTO> getPublishedCopiesOfNode(@NotNull final String nodeId) throws JsonProcessingException { + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}/publish") + .build(this.eduSharingConfiguration.getDefaultRepository(), nodeId); + + final var answer = this.webClient.get().uri(uri.toString()).retrieve().bodyToMono(String.class).block(); + + JsonNode root = OBJECT_MAPPER.readTree(answer); + JsonNode nodes = root.get("nodes"); + + return StreamSupport + .stream(nodes.spliterator(), false) + .map(child -> + new EduSharingProjectDTO( + child.get("ref").get("id").asText(), + child.get("content").get("url").asText(), + Instant.parse(child.get("createdAt").asText()) + ) + ) + .collect(Collectors.toList()); + } + + private List<EduSharingWorkflowDTO> getWorkflowsOfNode(@NotNull final String nodeId) throws JsonProcessingException { + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}/workflow") + .build(this.eduSharingConfiguration.getDefaultRepository(), nodeId); + + final var answer = this.webClient.get().uri(uri.toString()).retrieve().bodyToMono(String.class).block(); + + JsonNode root = OBJECT_MAPPER.readTree(answer); + + return StreamSupport + .stream(root.spliterator(), false) + .map(child -> + new EduSharingWorkflowDTO( + Instant.ofEpochMilli(child.get("time").asLong()), + child.get("comment").asText(), + child.get("status").asText() + ) + ) + .collect(Collectors.toList()); + } + + @Transactional(rollbackFor = Throwable.class) + @NotNull + @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.ConfusingTernary", "PMD.CyclomaticComplexity" }) + public EduSharingStatusDTO tryUpsertToEduSharing(@NotNull String resourceId) + throws ParseException, IOException, IllegalAccessException { + if (!this.isEduSharingEnabled()) { + throw new EduSharingDisabledException(); + } + + SearchResultDTO searchResultDTO = + this.searchService.findExerciseById(ExerciseId.fromString(resourceId)).orElseThrow(IllegalArgumentException::new); + + User currentUser = SecurityUtils + .getCurrentUserLogin() + .flatMap(this.userService::getUserByLogin) + .orElseThrow(IllegalAccessException::new); + boolean isPublisher = Arrays + .stream(searchResultDTO.getMetadata().getPublisher()) + .map(PersonDTO::getEmail) + .anyMatch(email -> email.equals(currentUser.getEmail())); + + if (!isPublisher && !this.authorityService.isUserAdmin(currentUser)) { + throw new IllegalAccessException("User is not allowed to upload to edu-sharing"); + } + + LinkedEduSharingProject linkedEduSharingProject = this.linkedEduSharingProjectRepository.getByResourceId(resourceId); + + String eduSharingId = null; + boolean update = false; + + try { + if (linkedEduSharingProject != null) { + update = true; + eduSharingId = linkedEduSharingProject.getEduSharingId(); + this.updateNode(eduSharingId, searchResultDTO); + } else { + eduSharingId = this.createNode(searchResultDTO); + } + + this.uploadContentForNode(eduSharingId, searchResultDTO); + + this.uploadPreviewForNode(eduSharingId, searchResultDTO); + + this.assignWorkflowToNode(eduSharingId); + + if (update) { + linkedEduSharingProject.setUpdatedAt(Instant.now()); + } else { + linkedEduSharingProject = new LinkedEduSharingProject(resourceId, eduSharingId); + } + + this.linkedEduSharingProjectRepository.save(linkedEduSharingProject); + + return this.getEduSharingStatus(resourceId).orElseThrow(); + } catch (Exception e) { + if (eduSharingId != null && !update) { + this.deleteNode(eduSharingId); + } + throw e; + } + } + + private boolean isBcp47CompliantAndContainsNoSubtag(String tag) { + Locale locale = Locale.forLanguageTag(tag); + boolean isCompliant = StringUtils.isNotEmpty(locale.getLanguage()); + boolean containsNoSubTag = StringUtils.isAllEmpty(locale.getCountry(), locale.getVariant()); + return isCompliant && containsNoSubTag; + } + + @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.CognitiveComplexity" }) + private EduSharingMetadataDTO createMetadataFromSearchResultDTO(@NotNull final SearchResultDTO searchResultDTO) + throws MetadataComplianceException { + List<MetadataComplianceException.Reason> reasons = new LinkedList<>(); + + if (searchResultDTO.getMetadata() == null) { + reasons.add(MetadataComplianceException.Reason.PROJECT_INVALID); + } + + List<PersonDTO> creators = new LinkedList<>(Arrays.asList(searchResultDTO.getMetadata().getCreator())); + List<PersonDTO> publishers = Arrays.asList(searchResultDTO.getMetadata().getPublisher()); + if (ArrayUtils.isNotEmpty(searchResultDTO.getMetadata().getContributor())) { + creators.addAll(Arrays.asList(searchResultDTO.getMetadata().getContributor())); + } + + if (CollectionUtils.isEmpty(creators)) { + reasons.add(MetadataComplianceException.Reason.CREATORS_EMPTY); + } + + if (CollectionUtils.isEmpty(publishers)) { + reasons.add(MetadataComplianceException.Reason.PUBLISHERS_EMPTY); + } + + if (Stream.of(searchResultDTO.getMetadata().getLanguage()).anyMatch(Predicate.not(this::isBcp47CompliantAndContainsNoSubtag))) { + reasons.add(MetadataComplianceException.Reason.LANGUAGE_CODE_INVALID); + } + + if (StringUtils.isEmpty(searchResultDTO.getMetadata().getDescription())) { + reasons.add(MetadataComplianceException.Reason.DESCRIPTION_EMPTY); + } + + if (ArrayUtils.isEmpty(searchResultDTO.getMetadata().getKeyword())) { + reasons.add(MetadataComplianceException.Reason.KEYWORDS_EMPTY); + searchResultDTO.getMetadata().setKeyword(ArrayUtils.EMPTY_STRING_ARRAY); + } + + if (ArrayUtils.isEmpty(searchResultDTO.getMetadata().getProgrammingLanguage())) { + reasons.add(MetadataComplianceException.Reason.PROGRAMMING_LANGUAGE_EMPTY); + searchResultDTO.getMetadata().setProgrammingLanguage(ArrayUtils.EMPTY_STRING_ARRAY); + } + + var dto = new EduSharingMetadataDTO(); + dto.setContentContributor(creators); + dto.setMetadataContributor(publishers); + dto.setDescription(searchResultDTO.getMetadata().getDescription()); + dto.setLanguages(Arrays.asList(searchResultDTO.getMetadata().getLanguage())); + String proposedFileName = searchResultDTO.getExerciseId() + "_" + searchResultDTO.getMetadata().getTitle() + ".zip"; + dto.setName(proposedFileName.replaceAll("[^a-zA-Z0-9.\\-]", "_")); + dto.setVersion(DEFAULT_VERSION); + dto.setTitle(searchResultDTO.getMetadata().getTitle()); + dto.setEducationalContext(DEFAULT_EDUCATIONAL_CONTEXT); + dto.setTaxonomy(DEFAULT_TAXONOMY); + dto.setReverseLink( + UriComponentsBuilder + .fromHttpUrl(eduSharingConfiguration.getFrontEndUrl()) + .path("/item") + .path("/" + URLEncoder.encode(searchResultDTO.getExerciseId(), StandardCharsets.UTF_8)) + .build() + .toUriString() + ); + + String programmingLanguagesForTitle = Arrays + .stream(searchResultDTO.getMetadata().getProgrammingLanguage()) + .filter(pl -> !dto.getTitle().contains(pl)) + .collect(Collectors.joining(", ")); + if (StringUtils.isNotEmpty(programmingLanguagesForTitle)) { + dto.setTitle(dto.getTitle() + " (" + programmingLanguagesForTitle + ")"); + } + + dto.setKeywords( + Stream + .concat( + Arrays.stream(searchResultDTO.getMetadata().getProgrammingLanguage()), + Arrays.stream(searchResultDTO.getMetadata().getKeyword()) + ) + .map(keyword -> this.translatedKeywordsCache.getGermanKeywordForEnglish(keyword).orElse(keyword)) + .collect(Collectors.toList()) + ); + + dto.setFormat(DEFAULT_FORMAT); + dto.setEducationalLearningResourceType(DEFAULT_EDUCATIONAL_LEARNING_RESOURCE_TYPE); + + if (ArrayUtils.isEmpty(searchResultDTO.getFile().getChildren()) && StringUtils.isEmpty(searchResultDTO.getFile().getParentId())) { + dto.setAggregationLevel(AggregationLevel.MATERIALS); + } else { + dto.setAggregationLevel(AggregationLevel.COURSE); + } + try { + dto.setLicence(new LicenceDTO(searchResultDTO.getMetadata().getLicense())); + } catch (final IllegalCreativeCommonsException e) { + reasons.add(MetadataComplianceException.Reason.LICENSE_NOT_SUPPORTED); + } catch (final LicenseSerializationException e) { + reasons.add(MetadataComplianceException.Reason.LICENSE_INVALID); + } + + if (CollectionUtils.isNotEmpty(reasons)) { + throw new MetadataComplianceException(reasons); + } + return dto; + } + + private void deleteNode(@NotNull String nodeId) { + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}") + .queryParam("recycle", false) + .build(this.eduSharingConfiguration.getDefaultRepository(), nodeId); + + final var answer = this.webClient.delete().uri(uri.toString()).accept(MediaType.APPLICATION_JSON).retrieve(); + + ResponseEntity<String> response = answer.toEntity(String.class).block(); + if (response == null || !response.getStatusCode().is2xxSuccessful()) { + LOGGER.error("DELETE request to " + uri.toString() + " failed!"); + LOGGER.error(response.getBody()); + } + } + + /** + * Creates a Node in edusharing + * + * @param searchResultDTO the metadata for the node to create + * @return the identifier for edusharing + */ + @NotNull + @SuppressWarnings({ "PMD.AvoidCatchingGenericException", "PMD.PreserveStackTrace" }) + private String createNode(@NotNull final SearchResultDTO searchResultDTO) throws MetadataComplianceException, IOException { + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}/children") + .queryParam("type", "ccm:io") + .build(this.eduSharingConfiguration.getDefaultRepository(), this.eduSharingConfiguration.getBaseNode()); + + try { + final var answer = + this.webClient.post() + .uri(uri.toString()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(createMetadataFromSearchResultDTO(searchResultDTO)) + .retrieve(); + + final String body = answer.bodyToMono(String.class).block(); + JsonNode root = OBJECT_MAPPER.readTree(body); + return root.get("node").get("ref").get("id").asText(); + } catch (WebClientResponseException e) { + if (e.getStatusCode() == HttpStatus.CONFLICT) { + throw new MetadataComplianceException(MetadataComplianceException.Reason.RESOURCE_WITH_TITLE_EXISTS); + } else { + throw e; + } + } catch (Exception e) { + if (ExceptionUtils.indexOfThrowable(e, VCardSerializationException.class) >= 0) { + throw new MetadataComplianceException(MetadataComplianceException.Reason.PERSONS_NOT_SERIALIZABLE, e); + } else { + throw e; + } + } + } + + @SuppressWarnings("PMD.AvoidCatchingGenericException") + private String updateNode(@NotNull String nodeId, @NotNull SearchResultDTO searchResultDTO) + throws MetadataComplianceException, JsonProcessingException { + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}/metadata") + .build(this.eduSharingConfiguration.getDefaultRepository(), nodeId); + + try { + final var answer = + this.webClient.post() + .uri(uri.toString()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(createMetadataFromSearchResultDTO(searchResultDTO)) + .retrieve(); + + final String body = answer.bodyToMono(String.class).block(); + JsonNode root = OBJECT_MAPPER.readTree(body); + return root.get("node").get("ref").get("id").asText(); + } catch (Exception e) { + if (ExceptionUtils.indexOfThrowable(e, VCardSerializationException.class) >= 0) { + throw new MetadataComplianceException(MetadataComplianceException.Reason.PERSONS_NOT_SERIALIZABLE, e); + } else { + throw e; + } + } + } + + private String uploadContentForNode(@NotNull String nodeId, @NotNull SearchResultDTO searchResultDTO) + throws IOException, ParseException { + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}/content") + .queryParam("mimetype", DEFAULT_MIME_TYPE) + .queryParam("versionComment", searchResultDTO.getProject().getCommit_id()) + .build(this.eduSharingConfiguration.getDefaultRepository(), nodeId); + + File zipFile; + if (ArrayUtils.isNotEmpty(searchResultDTO.getFile().getChildren())) { + zipFile = + this.searchService.exportExercise( + ExerciseId.fromString(searchResultDTO.getExerciseId()), + SearchService.ExtractionDepth.WITH_DESCENDANTS + ); + } else { + zipFile = this.searchService.exportExercise(searchResultDTO.getExerciseId()); + } + + MultipartBodyBuilder builder = new MultipartBodyBuilder(); + builder.part("file", new FileSystemResource(zipFile)); + + var result = + this.webClient.post() + .uri(uri.toString()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(builder.build())) + .retrieve() + .bodyToMono(String.class) + .block(); + + FileUtils.deleteQuietly(zipFile); + + return result; + } + + @SuppressWarnings("PMD.UnusedFormalParameter") + private String uploadPreviewForNode(@NotNull String nodeId, @NotNull SearchResultDTO searchResultDTO) + throws MetadataComplianceException { + if (ArrayUtils.isEmpty(searchResultDTO.getMetadata().getProgrammingLanguage())) { + throw new MetadataComplianceException(MetadataComplianceException.Reason.PROGRAMMING_LANGUAGE_EMPTY); + } + + try (InputStream logo = this.getClass().getResourceAsStream(determineLogoFileNameForProgrammingLanguage(searchResultDTO))) { + if (logo == null) { + throw new MetadataComplianceException(MetadataComplianceException.Reason.PREVIEW_IMAGE_ERROR); + } + + String mimeType = new Tika().detect(logo); + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}/preview") + .queryParam("mimetype", mimeType) + .build(this.eduSharingConfiguration.getDefaultRepository(), nodeId); + + MultipartBodyBuilder builder = new MultipartBodyBuilder(); + builder.part("image", new InputStreamResource(logo)); + + return this.webClient.post() + .uri(uri.toString()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(builder.build())) + .retrieve() + .bodyToMono(String.class) + .block(); + } catch (IOException e) { + throw new MetadataComplianceException(MetadataComplianceException.Reason.PREVIEW_IMAGE_ERROR, e); + } + } + + private String determineLogoFileNameForProgrammingLanguage(SearchResultDTO searchResultDTO) { + switch (searchResultDTO.getMetadata().getProgrammingLanguage()[0].toLowerCase(Locale.getDefault())) { + case "java": + return FILE_NAME_JAVA_LOGO; + case "python": + return FILE_NAME_PYTHON_LOGO; + case "c": + return FILE_NAME_C_LOGO; + default: + return FILE_NAME_DEFAULT_LOGO; + } + } + + private String assignWorkflowToNode(@NotNull String nodeId) { + final var uri = UriComponentsBuilder + .newInstance() + .path("/rest/node/v1/nodes/{repository}/{node}/workflow") + .build(this.eduSharingConfiguration.getDefaultRepository(), nodeId); + + return this.webClient.put() + .uri(uri.toString()) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue( + Map.of( + "receiver", + List.of(this.eduSharingConfiguration.getEditorialGroup()), + "status", + this.eduSharingConfiguration.getWorkflowToCheckStatus(), + "comment", + "Automatic upload from UIBK Sharing platform" + ) + ) + .retrieve() + .bodyToMono(String.class) + .block(); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/MetadataComplianceException.java b/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/MetadataComplianceException.java new file mode 100644 index 0000000000000000000000000000000000000000..50132ebba3ae24449f83df26c959aa7a9f905630 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/service/edu_sharing/MetadataComplianceException.java @@ -0,0 +1,51 @@ +package at.ac.uibk.gitsearch.service.edu_sharing; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +public class MetadataComplianceException extends IOException { + + private static final long serialVersionUID = 1L; + + private final Collection<Reason> reasons; + + public MetadataComplianceException(Collection<Reason> reasons) { + super(); + this.reasons = reasons; + } + + public MetadataComplianceException(Collection<Reason> reasons, Throwable cause) { + super(cause); + this.reasons = reasons; + } + + public MetadataComplianceException(Reason reason) { + this(List.of(reason)); + } + + public MetadataComplianceException(Reason reason, Throwable cause) { + this(List.of(reason), cause); + } + + public enum Reason { + //todo remove + COLLECTIONS_NOT_SUPPORTED, + LICENSE_INVALID, + LICENSE_NOT_SUPPORTED, + KEYWORDS_EMPTY, + DESCRIPTION_EMPTY, + LANGUAGE_CODE_INVALID, + PUBLISHERS_EMPTY, + CREATORS_EMPTY, + PROJECT_INVALID, + PERSONS_NOT_SERIALIZABLE, + PREVIEW_IMAGE_ERROR, + PROGRAMMING_LANGUAGE_EMPTY, + RESOURCE_WITH_TITLE_EXISTS, + } + + public Collection<Reason> getReasons() { + return reasons; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/ExtraEntry.java b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/ExtraEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..0659bbbbd0db9db137dba3773f988822ef19f06d --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/ExtraEntry.java @@ -0,0 +1,28 @@ +package at.ac.uibk.gitsearch.service.vocabulary; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ExtraEntry { + + @JsonProperty("de") + private String germanEntry; + + @JsonProperty("en") + private String englishEntry; + + public String getGermanEntry() { + return germanEntry; + } + + public void setGermanEntry(String germanEntry) { + this.germanEntry = germanEntry; + } + + public String getEnglishEntry() { + return englishEntry; + } + + public void setEnglishEntry(String englishEntry) { + this.englishEntry = englishEntry; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/TranslatedKeywordsCache.java b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/TranslatedKeywordsCache.java new file mode 100644 index 0000000000000000000000000000000000000000..cf25bcf7c30e4a063555fe92e774b049aace71d7 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/TranslatedKeywordsCache.java @@ -0,0 +1,66 @@ +package at.ac.uibk.gitsearch.service.vocabulary; + +import at.ac.uibk.gitsearch.domain.vocabulary.VocabularyItem; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotEmpty; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +@Service +@Validated +public class TranslatedKeywordsCache { + + @Autowired + private VocabularyService vocabularyService; + + private AtomicReference<Map<String, String>> englishToGermanKeywords; + + @PostConstruct + protected void init() { + this.englishToGermanKeywords = new AtomicReference<>(null); + this.reIndexTranslationMap(); + } + + @Scheduled(cron = "0 30 2 * * ?") + protected void reIndexTranslationMap() { + Map<String, String> defaultVocabulary = Arrays + .stream(this.vocabularyService.getKeywordItems()) + .map(VocabularyItem::getLanguageItem) + .map(languageItems -> { + String englishWord = null; + String germanWord = null; + for (VocabularyItem.LanguageItem languageItem : languageItems) { + switch (languageItem.getLanguageCode()) { + case "de": + germanWord = languageItem.getTitle(); + break; + case "en": + default: + englishWord = languageItem.getTitle(); + break; + } + } + return StringUtils.isNoneEmpty(englishWord, germanWord) ? Pair.of(englishWord, germanWord) : null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Pair::getKey, Pair::getValue)); + + Map<String, String> extraEntriesVocabulary = Arrays + .stream(this.vocabularyService.getKeywordConfig().getExtraEntries()) + .collect(Collectors.toMap(ExtraEntry::getEnglishEntry, ExtraEntry::getGermanEntry, (o1, o2) -> o1, HashMap::new)); + + extraEntriesVocabulary.putAll(defaultVocabulary); + this.englishToGermanKeywords.setRelease(Collections.unmodifiableMap(extraEntriesVocabulary)); + } + + public Optional<String> getGermanKeywordForEnglish(@NotEmpty String englishKeyword) { + return Optional.ofNullable(this.englishToGermanKeywords.get().get(englishKeyword)); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyService.java b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyService.java index 218ae6e4b33dc17fa6bff5b18a064f7f5dd18feb..aff5edbbdc553ff258a1adf910d20e7c7fb1cdad 100644 --- a/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyService.java +++ b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyService.java @@ -3,30 +3,20 @@ package at.ac.uibk.gitsearch.service.vocabulary; import at.ac.uibk.gitsearch.domain.vocabulary.VocabularyItem; import at.ac.uibk.gitsearch.domain.vocabulary.VocabularyItem.LanguageItem; import at.ac.uibk.gitsearch.repository.vocabulary.VocabularyRepository; -import at.ac.uibk.gitsearch.service.vocabulary.VocabularyService.VocabularyServiceConfig.RequiredEnum; -import at.ac.uibk.gitsearch.service.vocabulary.VocabularyService.VocabularyServiceConfig.VocabularySetting; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonSetter; +import at.ac.uibk.gitsearch.service.vocabulary.VocabularyServiceConfig.RequiredEnum; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; -import java.beans.IntrospectionException; -import java.beans.PropertyDescriptor; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import javax.annotation.PostConstruct; import org.apache.lucene.search.spell.LevenshteinDistance; +import org.codeability.sharing.plugins.api.search.PersonDTO; import org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO; -import org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO.Person; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -37,7 +27,7 @@ import org.springframework.transaction.annotation.Transactional; @Transactional public class VocabularyService { - private static final String PERSON_ARRAY_CLASSNAME = Person.class.getCanonicalName() + "[]"; + private static final String PERSON_ARRAY_CLASSNAME = PersonDTO.class.getCanonicalName() + "[]"; private final Logger log = LoggerFactory.getLogger(VocabularyService.class); private static final float MAXIMAL_DISTANCE = 0.70f; @@ -78,6 +68,10 @@ public class VocabularyService { return vocabularyRepository.getVocabularyItemsFor("Difficulty", VocabularyItem.class); } + public VocabularySetting getKeywordConfig() { + return getConfig().getConfigSettingByOEResourceType("Keyword").orElseThrow(); + } + /** * validates the metadata * @@ -88,7 +82,7 @@ public class VocabularyService { public ValidationResultDTO validateMetaData(UserProvidedMetadataDTO metadata, boolean isTopLevel) { ValidationResultDTO result = new ValidationResultDTO(); - for (VocabularySetting setting : config.config) { + for (VocabularySetting setting : config.getConfig()) { try { checkField( setting.getGetter().invoke(metadata), @@ -98,7 +92,7 @@ public class VocabularyService { ? (setting.getRequired() == RequiredEnum.REQUIRED_ON_TOP_LEVEL || setting.getRequired() == RequiredEnum.REQUIRED) : setting.getRequired() == RequiredEnum.REQUIRED, setting.isExactMatch(), - setting.getExtraEntries(), + setting.getExtraEntriesUnion(), result ); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { @@ -285,125 +279,6 @@ public class VocabularyService { } } - public static class VocabularyServiceConfig { - - private VocabularySetting[] config; - - public enum RequiredEnum { - REQUIRED, - REQUIRED_ON_TOP_LEVEL, - OPTIONAL, - } - - public static class VocabularySetting { - - private static final Logger log = LoggerFactory.getLogger(VocabularySetting.class); - - private String property; - private String type; - private String oeResourceType; - private RequiredEnum required; - private boolean exactMatch = false; - - @com.fasterxml.jackson.annotation.JsonIgnore - private Method getter; - - private String[] extraEntries = {}; - - public String getProperty() { - return property; - } - - @SuppressWarnings("PMD.NullAssignment") - public void setProperty(String property) { - this.property = property; - getter = null; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type.strip(); - } - - @JsonGetter("OEResourceType") - public String getOEResourceType() { - return oeResourceType; - } - - @JsonSetter("OEResourceType") - public void setOEResourceType(String oEResourceType) { - oeResourceType = oEResourceType; - } - - public RequiredEnum getRequired() { - return required; - } - - public void setRequired(RequiredEnum required) { - this.required = required; - } - - @com.fasterxml.jackson.annotation.JsonIgnore - public Method getGetter() { - if (getter == null) { - try { - getter = new PropertyDescriptor(property, UserProvidedMetadataDTO.class).getReadMethod(); - if (!getter.getReturnType().getCanonicalName().equals(getType())) { - log.error("getter for {} has wrong type: expected {}", property, getType()); - return null; - } - } catch (IntrospectionException e) { - log.error("Cannot find getter for {}", property, e); - } - } - - return getter; - } - - public String[] getExtraEntries() { - return extraEntries.clone(); - } - - public void setExtraEntries(String[] extraEntries) { - this.extraEntries = extraEntries.clone(); - } - - @Override - public String toString() { - return "VocabularySetting: for " + property + "(" + type + "/" + oeResourceType + ")"; - } - - public boolean isExactMatch() { - return exactMatch; - } - - public void setExactMatch(boolean exactMatch) { - this.exactMatch = exactMatch; - } - } - - @SuppressWarnings("PMD.MethodReturnsInternalArray") - protected VocabularySetting[] getConfig() { - return config; - } - - @SuppressWarnings("PMD.MethodReturnsInternalArray") - protected Optional<VocabularySetting> getConfigSettingByOEResourceType(String oeResourceType) { - return Arrays - .stream(config) - .filter(s -> s.getOEResourceType() != null && s.getOEResourceType().equals(oeResourceType)) - .findAny(); - } - - @SuppressWarnings("PMD.ArrayIsStoredDirectly") - protected void setConfig(VocabularySetting[] config) { - this.config = config; - } - } - public static class ValidationResultDTO { List<String> errors = new ArrayList<>(); diff --git a/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyServiceConfig.java b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyServiceConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..5e57e2b3bcaa347335dfb5817ee96636266417f6 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyServiceConfig.java @@ -0,0 +1,30 @@ +package at.ac.uibk.gitsearch.service.vocabulary; + +import java.util.Arrays; +import java.util.Optional; + +public class VocabularyServiceConfig { + + private VocabularySetting[] config; + + public enum RequiredEnum { + REQUIRED, + REQUIRED_ON_TOP_LEVEL, + OPTIONAL, + } + + @SuppressWarnings("PMD.MethodReturnsInternalArray") + protected VocabularySetting[] getConfig() { + return config; + } + + @SuppressWarnings("PMD.MethodReturnsInternalArray") + protected Optional<VocabularySetting> getConfigSettingByOEResourceType(String oeResourceType) { + return Arrays.stream(config).filter(s -> s.getOEResourceType() != null && s.getOEResourceType().equals(oeResourceType)).findAny(); + } + + @SuppressWarnings("PMD.ArrayIsStoredDirectly") + protected void setConfig(VocabularySetting[] config) { + this.config = config; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularySetting.java b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularySetting.java new file mode 100644 index 0000000000000000000000000000000000000000..e296b7a22798525b05d571305c09e439607ee7c8 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularySetting.java @@ -0,0 +1,112 @@ +package at.ac.uibk.gitsearch.service.vocabulary; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSetter; +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.stream.Stream; +import org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VocabularySetting { + + private static final Logger log = LoggerFactory.getLogger(VocabularySetting.class); + + private String property; + private String type; + private String oeResourceType; + private VocabularyServiceConfig.RequiredEnum required; + private boolean exactMatch = false; + + @JsonIgnore + private Method getter; + + private ExtraEntry[] extraEntries = {}; + + public String getProperty() { + return property; + } + + @SuppressWarnings("PMD.NullAssignment") + public void setProperty(String property) { + this.property = property; + getter = null; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type.strip(); + } + + @JsonGetter("OEResourceType") + public String getOEResourceType() { + return oeResourceType; + } + + @JsonSetter("OEResourceType") + public void setOEResourceType(String oEResourceType) { + oeResourceType = oEResourceType; + } + + public VocabularyServiceConfig.RequiredEnum getRequired() { + return required; + } + + public void setRequired(VocabularyServiceConfig.RequiredEnum required) { + this.required = required; + } + + @JsonIgnore + public Method getGetter() { + if (getter == null) { + try { + getter = new PropertyDescriptor(property, UserProvidedMetadataDTO.class).getReadMethod(); + if (!getter.getReturnType().getCanonicalName().equals(getType())) { + log.error("getter for {} has wrong type: expected {}", property, getType()); + return null; + } + } catch (IntrospectionException e) { + log.error("Cannot find getter for {}", property, e); + } + } + + return getter; + } + + public ExtraEntry[] getExtraEntries() { + return extraEntries.clone(); + } + + public void setExtraEntries(ExtraEntry[] extraEntries) { + this.extraEntries = extraEntries.clone(); + } + + @JsonIgnore + public String[] getExtraEntriesUnion() { + return Arrays + .stream(this.getExtraEntries()) + .flatMap(tuple -> Stream.of(tuple.getGermanEntry(), tuple.getEnglishEntry())) + .distinct() + .toArray(String[]::new); + } + + @Override + public String toString() { + return "VocabularySetting: for " + property + "(" + type + "/" + oeResourceType + ")"; + } + + public boolean isExactMatch() { + return exactMatch; + } + + public void setExactMatch(boolean exactMatch) { + this.exactMatch = exactMatch; + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/web/rest/EduSharingStatusResource.java b/src/main/java/at/ac/uibk/gitsearch/web/rest/EduSharingStatusResource.java new file mode 100644 index 0000000000000000000000000000000000000000..5d66294acf2c376898684d51760018075ffebba2 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/web/rest/EduSharingStatusResource.java @@ -0,0 +1,24 @@ +package at.ac.uibk.gitsearch.web.rest; + +import at.ac.uibk.gitsearch.service.edu_sharing.EduSharingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class EduSharingStatusResource { + + @Autowired + private EduSharingService eduSharingService; + + @GetMapping("/eduSharingAvailability") + public ResponseEntity<?> getEduSharingStatus() { + if (!eduSharingService.isEduSharingEnabled()) { + return ResponseEntity.notFound().build(); + } + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/web/rest/ExerciseResource.java b/src/main/java/at/ac/uibk/gitsearch/web/rest/ExerciseResource.java index ecc1150d7f3c87aa63ba3b69a075f1525cdc9a22..88288ceff8661afa3d76aed8497b99dba14ff2f9 100644 --- a/src/main/java/at/ac/uibk/gitsearch/web/rest/ExerciseResource.java +++ b/src/main/java/at/ac/uibk/gitsearch/web/rest/ExerciseResource.java @@ -2,23 +2,17 @@ package at.ac.uibk.gitsearch.web.rest; import at.ac.uibk.gitsearch.domain.ChildInfo; import at.ac.uibk.gitsearch.domain.User; +import at.ac.uibk.gitsearch.edu_sharing.model.EduSharingStatusDTO; import at.ac.uibk.gitsearch.es.model.ArtemisExerciseInfo; import at.ac.uibk.gitsearch.security.SecurityUtils; -import at.ac.uibk.gitsearch.service.ArtemisImportError; -import at.ac.uibk.gitsearch.service.ExerciseImportService; -import at.ac.uibk.gitsearch.service.GitlabService; -import at.ac.uibk.gitsearch.service.SearchService; +import at.ac.uibk.gitsearch.service.*; import at.ac.uibk.gitsearch.service.SearchService.ExtractionDepth; -import at.ac.uibk.gitsearch.service.StatisticsService; -import at.ac.uibk.gitsearch.service.UserService; import at.ac.uibk.gitsearch.service.dto.StatisticsDTO; +import at.ac.uibk.gitsearch.service.edu_sharing.EduSharingDisabledException; +import at.ac.uibk.gitsearch.service.edu_sharing.EduSharingService; import at.ac.uibk.gitsearch.web.rest.utils.RestUtils; import at.ac.uibk.gitsearch.web.util.HeaderUtil; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -45,6 +39,7 @@ import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -77,6 +72,9 @@ public class ExerciseResource { @Value("${application.registeredConnectorsCallBackURL}") String baseApiUrl; + @Autowired + private EduSharingService eduSharingService; + @Autowired @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" }) private GitlabService gitLabService; @@ -493,6 +491,28 @@ public class ExerciseResource { } } + @GetMapping("exercises/{id}/edu-sharing-status") + public ResponseEntity<EduSharingStatusDTO> getEduSharingStatus(@PathVariable("id") String projectId) { + try { + return this.eduSharingService.getEduSharingStatus(projectId).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build()); + } catch (EduSharingDisabledException e) { + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build(); + } + } + + @PutMapping("exercises/{id}/edu-sharing-status") + public ResponseEntity<EduSharingStatusDTO> upsertEduSharing(@PathVariable("id") String projectId) throws IOException { + try { + return ResponseEntity.ok(this.eduSharingService.tryUpsertToEduSharing(projectId)); + } catch (ParseException | IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } catch (IllegalAccessException e) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } catch (EduSharingDisabledException e) { + return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).build(); + } + } + /** * Utility method used to retrieve the Base URL from a HTTP request * @param request the request to extract the Url from diff --git a/src/main/java/at/ac/uibk/gitsearch/web/rest/errors/ErrorConstants.java b/src/main/java/at/ac/uibk/gitsearch/web/rest/errors/ErrorConstants.java index 8524bc4c9b4e415cb3306986e4dc423842b5e7a2..5830043aea5ed46dcdfdd9e6065e722933adef50 100644 --- a/src/main/java/at/ac/uibk/gitsearch/web/rest/errors/ErrorConstants.java +++ b/src/main/java/at/ac/uibk/gitsearch/web/rest/errors/ErrorConstants.java @@ -13,5 +13,7 @@ public final class ErrorConstants { public static final URI EMAIL_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/email-already-used"); public static final URI LOGIN_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/login-already-used"); + public static final URI METADATA_NON_COMPLIANT_TYPE = URI.create(PROBLEM_BASE_URL + "/metadata-non-compliant"); + private ErrorConstants() {} } diff --git a/src/main/java/at/ac/uibk/gitsearch/web/rest/errors/ExceptionTranslator.java b/src/main/java/at/ac/uibk/gitsearch/web/rest/errors/ExceptionTranslator.java index b4c3a9c470ee3172026fd0038ac508ed5669540b..c06dcb8de4216bb0cde25ab0a05e97314a9bc891 100644 --- a/src/main/java/at/ac/uibk/gitsearch/web/rest/errors/ExceptionTranslator.java +++ b/src/main/java/at/ac/uibk/gitsearch/web/rest/errors/ExceptionTranslator.java @@ -1,5 +1,6 @@ package at.ac.uibk.gitsearch.web.rest.errors; +import at.ac.uibk.gitsearch.service.edu_sharing.MetadataComplianceException; import java.net.URI; import java.util.Arrays; import java.util.Collection; @@ -165,6 +166,18 @@ public class ExceptionTranslator implements ProblemHandling, SecurityAdviceTrait return create(ex, problem, request); } + @ExceptionHandler(MetadataComplianceException.class) + public ResponseEntity<Problem> handleMetadataNonCompliantException(MetadataComplianceException ex, NativeWebRequest request) { + Problem problem = Problem + .builder() + .withStatus(Status.BAD_REQUEST) + .withType(ErrorConstants.METADATA_NON_COMPLIANT_TYPE) + .with(MESSAGE_KEY, "Meta data is non compliant") + .with(VIOLATIONS_KEY, ex.getReasons().stream().map(Enum::toString).collect(Collectors.toList())) + .build(); + return create(ex, problem, request); + } + @Override public ProblemBuilder prepare(final @Nonnull Throwable throwable, final @Nonnull StatusType status, final @Nonnull URI type) { Collection<String> activeProfiles = Arrays.asList(env.getActiveProfiles()); diff --git a/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/c-language-logo.png b/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/c-language-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d8a3de2835dcc490a868f298a6cc34d0bdb10e39 Binary files /dev/null and b/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/c-language-logo.png differ diff --git a/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/codeability-logo.png b/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/codeability-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..37ee79ab4b98c6e215370892f2effa9a504987f7 Binary files /dev/null and b/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/codeability-logo.png differ diff --git a/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/java-language-logo.png b/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/java-language-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1bbe00559b06a0644d447eba329960dd393c57b3 Binary files /dev/null and b/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/java-language-logo.png differ diff --git a/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/python-language-logo.png b/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/python-language-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..806df440e3ad6620e36c0a19f214f5c25aafd417 Binary files /dev/null and b/src/main/resources/at/ac/uibk/gitsearch/service/edu_sharing/python-language-logo.png differ diff --git a/src/main/resources/at/ac/uibk/gitsearch/service/vocabulary/vocabularyServiceConfig.json b/src/main/resources/at/ac/uibk/gitsearch/service/vocabulary/vocabularyServiceConfig.json index f4b32f44435b725f17eaf0bdf55f55186785bb67..7012d5b28d38d62121dafee3f89a9a9446d873bc 100644 --- a/src/main/resources/at/ac/uibk/gitsearch/service/vocabulary/vocabularyServiceConfig.json +++ b/src/main/resources/at/ac/uibk/gitsearch/service/vocabulary/vocabularyServiceConfig.json @@ -2,12 +2,12 @@ "config": [ { "property": "creator", - "type": "org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO.Person[]", + "type": "org.codeability.sharing.plugins.api.search.PersonDTO[]", "required": "REQUIRED_ON_TOP_LEVEL" }, { "property": "publisher", - "type": "org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO.Person[]", + "type": "org.codeability.sharing.plugins.api.search.PersonDTO[]", "required": "REQUIRED_ON_TOP_LEVEL" }, { @@ -22,77 +22,77 @@ "OEResourceType": "Keyword", "required": "REQUIRED", "extraEntries": [ - "Artemis", - "artemis", - "Greenfoot", - "CodeRunner", - "compiler", - "transformation", - "private", - "strings", - "functions", - "character manipulation", - "binary representation", - "command line arguments", - "object orientation", - "inheritance", - "4c/id-model", - "modeling example", - "completion example", - "conventional example", - "worked example", - "Example", - "JUnit Tests", - "structural tests", - "example", - "arrays", - "code analysis", - "infrastructure checks", - "fixed width integer types", - "pointers", - "java", - "collection", - "dynamic memory allocation", - "array list", - "linked list", - "modular programming", - "theory", - "files", - "tokenization", - "function pointers", - "linear recurrence relation", - "recursion", - "literals", - "datatypes", - "geometric shape", - "operators", - "control flow", - "logical expressions", - "while", - "if", - "modulo operator", - "divisibility", - "Atbasch", - "encoding", - "decoding", - "cipher", - "sort", - "introduction", - "JUnit Tests", - "junit", - "java parser", - "IO Tests", - "Glass Box Testing", - "Testen", - "unspecified behavior", - "order of evaluation", - "gcc", - "clang", - "constant pointer", - "pointer to constant", - "c11", - "generic selection", - "tutorial" + { "en": "Artemis", "de": "Artemis" }, + { "en": "artemis", "de": "artemis" }, + { "en": "Greenfoot", "de": "Greenfoot" }, + { "en": "CodeRunner", "de": "CodeRunner" }, + { "en": "compiler", "de": "Compiler" }, + { "en": "transformation", "de": "Transformation" }, + { "en": "private", "de": "private" }, + { "en": "strings", "de": "Zeichenketten" }, + { "en": "functions", "de": "Funktionen" }, + { "en": "character manipulation", "de": "Zeichenmanipulation" }, + { "en": "binary representation", "de": "Binäre Darstellung" }, + { "en": "command line arguments", "de": "Befehlszeilenargumente" }, + { "en": "object orientation", "de": "Objektorientierung" }, + { "en": "inheritance", "de": "Vererbung" }, + { "en": "4c/id-model", "de": "4c/id-Modell" }, + { "en": "modeling example", "de": "Modellierung Beispiel" }, + { "en": "completion example", "de": "Vervollständigung Beispiel" }, + { "en": "conventional example", "de": "Konventionelles Beispiel" }, + { "en": "worked example", "de": "Bearbeitetes Beispiel" }, + { "en": "Example", "de": "Beispiel" }, + { "en": "JUnit Tests", "de": "JUnit-Tests" }, + { "en": "structural tests", "de": "Strukturtests" }, + { "en": "example", "de": "Beispiel" }, + { "en": "arrays", "de": "Arrays" }, + { "en": "code analysis", "de": "Codeanalyse" }, + { "en": "infrastructure checks", "de": "Infrastrukturprüfungen" }, + { "en": "fixed width integer types", "de": "Feste Breite Ganzzahltypen" }, + { "en": "pointers", "de": "Zeiger" }, + { "en": "java", "de": "Java" }, + { "en": "collection", "de": "Collection" }, + { "en": "dynamic memory allocation", "de": "Dynamische Speicherzuweisung" }, + { "en": "array list", "de": "Arrayliste" }, + { "en": "linked list", "de": "Verkettete Liste" }, + { "en": "modular programming", "de": "Modulare Programmierung" }, + { "en": "theory", "de": "Theorie" }, + { "en": "files", "de": "Dateien" }, + { "en": "tokenization", "de": "Tokenisierung" }, + { "en": "function pointers", "de": "Funktionszeiger" }, + { "en": "linear recurrence relation", "de": "Lineare Rekurrenzrelation" }, + { "en": "recursion", "de": "Rekursion" }, + { "en": "literals", "de": "Literale" }, + { "en": "datatypes", "de": "Datentypen" }, + { "en": "geometric shape", "de": "Geometrische Form" }, + { "en": "operators", "de": "Operatoren" }, + { "en": "control flow", "de": "Kontrollfluss" }, + { "en": "logical expressions", "de": "Logische Ausdrücke" }, + { "en": "while", "de": "while" }, + { "en": "if", "de": "if" }, + { "en": "modulo operator", "de": "Modulo-Operator" }, + { "en": "divisibility", "de": "Teilbarkeit" }, + { "en": "Atbasch", "de": "Atbasch" }, + { "en": "encoding", "de": "Codierung" }, + { "en": "decoding", "de": "Dekodierung" }, + { "en": "cipher", "de": "Chiffre" }, + { "en": "sort", "de": "Sortieren" }, + { "en": "introduction", "de": "Einführung" }, + { "en": "JUnit Tests", "de": "JUnit-Tests" }, + { "en": "junit", "de": "junit" }, + { "en": "java parser", "de": "Java-Parser" }, + { "en": "IO Tests", "de": "IO-Tests" }, + { "en": "Glass Box Testing", "de": "Glasbox-Testen" }, + { "en": "Testen", "de": "Testen" }, + { "en": "unspecified behavior", "de": "Unspezifiziertes Verhalten" }, + { "en": "order of evaluation", "de": "Auswertungsreihenfolge" }, + { "en": "gcc", "de": "gcc" }, + { "en": "clang", "de": "clang" }, + { "en": "constant pointer", "de": "Konstanter Zeiger" }, + { "en": "pointer to constant", "de": "Zeiger auf Konstante" }, + { "en": "c11", "de": "c11" }, + { "en": "generic selection", "de": "Generische Auswahl" }, + { "en": "tutorial", "de": "Tutorial" } ] }, { @@ -100,7 +100,10 @@ "type": "java.lang.String", "OEResourceType": "License", "required": "REQUIRED_ON_TOP_LEVEL", - "extraEntries": ["CC-SA-BY 4.0", "CC-SA 4.0"] + "extraEntries": [ + { "en": "CC-SA-BY 4.0", "de": "CC-SA-BY 4.0" }, + { "en": "CC-SA 4.0", "de": "CC-SA 4.0" } + ] }, { "property": "difficulty", @@ -108,34 +111,56 @@ "type": "java.lang.String", "required": "OPTIONAL", "exactMatch": true, - "extraEntries": ["simple", "advanced"] + "extraEntries": [ + { "en": "simple", "de": "einfach" }, + { "en": "advanced", "de": "fortgeschritten" } + ] }, { "property": "learningResourceType", "OEResourceType": "Learning Resource Type", "type": "java.lang.String", "required": "OPTIONAL", - "extraEntries": ["Artemis", "collection", "programming exercise", "exercise"] + "extraEntries": [ + { "en": "Artemis", "de": "Artemis" }, + { "en": "collection", "de": "Collection" }, + { "en": "programming exercise", "de": "Programmierübung" }, + { "en": "exercise", "de": "Übung" } + ] }, { "property": "format", "type": "java.lang.String[]", "required": "REQUIRED", - "extraEntries": ["Artemis", "artemis", "md", "any", "pdf"] + "extraEntries": [ + { "en": "Artemis", "de": "Artemis" }, + { "en": "artemis", "de": "artemis" }, + { "en": "md", "de": "md" }, + { "en": "any", "de": "beliebig" }, + { "en": "pdf", "de": "pdf" } + ] }, { "property": "subject", "OEResourceType": "Subject", "type": "java.lang.String[]", "required": "OPTIONAL", - "extraEntries": ["Universität Innsbruck-Einführung in die Programmierung"] + "extraEntries": [ + { + "en": "University of Innsbruck - Introduction to programming", + "de": "Universität Innsbruck-Einführung in die Programmierung" + } + ] }, { "property": "programmingLanguage", "OEResourceType": "Programming Language", "type": "java.lang.String[]", "required": "OPTIONAL", - "extraEntries": ["Uml", "Python"] + "extraEntries": [ + { "en": "Uml", "de": "Uml" }, + { "en": "Python", "de": "Python" } + ] }, { "property": "typicalAgeRange", @@ -148,14 +173,24 @@ "OEResourceType": "Audience", "type": "java.lang.String", "required": "OPTIONAL", - "extraEntries": ["Anfänger"] + "extraEntries": [ + { + "de": "Anfänger", + "en": "beginners" + } + ] }, { "property": "educationalLevel", "OEResourceType": "Education Level", "type": "java.lang.String[]", "required": "OPTIONAL", - "extraEntries": ["beginners"] + "extraEntries": [ + { + "de": "Anfänger", + "en": "beginners" + } + ] }, { "property": "educationalAlignment", @@ -187,10 +222,10 @@ "type": "java.lang.String", "required": "OPTIONAL", "extraEntries": [ - "atomic", "hierarchical", "networked" - ] - - + { "en": "atomic", "de": "atomar" }, + { "en": "hierarchical", "de": "hierarchisch" }, + { "en": "networked", "de": "vernetzt" } + ] } ] } diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 176c3284fa2ec73eff9c794f0257e21f91966f77..4dac0698ee60c8636c9a3999046e6e4829de1d39 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -157,6 +157,7 @@ jhipster: # =================================================================== application: + frontEndUrl: http://localhost:9001 registeredConnectors: - url: 'http://localhost:8081/api/sharing/config' accessToken: ${SHARING_CONFIG_ACCESS_TOKEN} @@ -171,3 +172,6 @@ application: oeResource: oerLink: https://oeresource-dev.logic.at apiLink: https://oeresource-dev.logic.at/en/meta/api/v1?format=json + +edu-sharing-integration: + enabled: true diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index edcca44e787d1f019592b4fdd3b2ac3c7d0d3641..076455fb213d062f69bc47a32388646bafd5fb3e 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -166,6 +166,7 @@ jhipster: # =================================================================== application: + frontEndUrl: https://search.sharing-codeability.uibk.ac.at/ registeredConnectors: - url: 'https://artemis.codeability.uibk.ac.at/api/sharing/config' accessToken: ${CONNECTOR_ARTEMIS_TOKEN} diff --git a/src/main/resources/config/application-staging.yml b/src/main/resources/config/application-staging.yml index ef1ae58400e1e884d491af5038a87d481deb9455..e7b283c3bcfae7d48efc86d2242ca7b68fc0faeb 100644 --- a/src/main/resources/config/application-staging.yml +++ b/src/main/resources/config/application-staging.yml @@ -161,6 +161,7 @@ jhipster: # =================================================================== application: + frontEndUrl: https://dev-exchange.codeability-austria.uibk.ac.at registeredConnectors: - url: 'https://artemis.codeability-austria.uibk.ac.at/api/sharing/config' accessToken: ${CONNECTOR_ARTEMIS_TOKEN} @@ -173,4 +174,6 @@ application: oeResource: oerLink: https://oeresource-dev.logic.at apiLink: https://oeresource-dev.logic.at/en/meta/api/v1?format=json - \ No newline at end of file + +edu-sharing-integration: + enabled: true diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index b47ee76b8f550a4c4a66e1ee58df4018499d0183..c84e35eda5f5d974115fd5029ae17345d3af5bb5 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -214,6 +214,7 @@ jhipster: # =================================================================== application: + frontEndUrl: http://localhost:9001 search: highlight-pre: <mark><strong> highlight-post: </strong></mark> @@ -225,3 +226,16 @@ application: commit-id: '${gitCommitId}' branch: '${gitBranch}' deploymentDate: ${gitCommitDate} + +edu-sharing-integration: + enabled: false + base-url: 'https://es-dev2.uibk.ac.at/edu-sharing/' + auth: + username: ${EDU_SHARING_USER} + password: ${EDU_SHARING_PASSWORD} + content: + repository: 'edu-sharing' + base-node: '6561f315-6c29-4631-8b71-bcdde8910089' + editorial: + group-authority-name: 'GROUP_editor_group' + status-to-check: '200_tocheck' diff --git a/src/main/resources/config/liquibase/changelog/20240304092200_added_entity_linked_edu_sharing_project.xml b/src/main/resources/config/liquibase/changelog/20240304092200_added_entity_linked_edu_sharing_project.xml new file mode 100644 index 0000000000000000000000000000000000000000..12ce88c7f7d8494446a67da9e3295cdf1c8aac34 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20240304092200_added_entity_linked_edu_sharing_project.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<databaseChangeLog + xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd"> + + <!-- + Added the entity Review. + --> + <changeSet id="20240304092200-1" author="jhipster"> + <preConditions onFail="MARK_RAN"> + <not> + <tableExists tableName="linked_edu_sharing_project"/> + </not> + </preConditions> + <createTable tableName="linked_edu_sharing_project"> + <column name="id" type="bigint" autoIncrement="true"> + <constraints primaryKey="true" nullable="false"/> + </column> + <column name="resource_id" type="varchar(255)"> + <constraints nullable="false" unique="true" uniqueConstraintName="ux_linked_edu_sharing_project__resource"/> + </column> + <column name="edu_sharing_id" type="varchar(255)"> + <constraints nullable="false" /> + </column> + <column name="created_at" type="timestamp"> + <constraints nullable="false"/> + </column> + <column name="updated_at" type="timestamp"> + <constraints nullable="false"/> + </column> + <!-- jhipster-needle-liquibase-add-column - JHipster will add columns here --> + </createTable> + </changeSet> +</databaseChangeLog> diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 515320463f98695742c43658c26bb00d4ea7221b..9434fec54a6a00142289c48fd932ddea0d8fc912 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -42,6 +42,7 @@ <include file="config/liquibase/changelog/20230805241110_update_review_rating_comment.xml" relativeToChangelogFile="false"/> <include file="config/liquibase/changelog/202402281336_add_columnLastCheckToUserWatchList.xml" relativeToChangelogFile="false"/> <include file="config/liquibase/changelog/202403071845_add_columnSavedSearches.xml" relativeToChangelogFile="false"/> + <include file="config/liquibase/changelog/20240304092200_added_entity_linked_edu_sharing_project.xml" relativeToChangelogFile="false"/> <!-- jhipster-needle-liquibase-add-constraints-changelog - JHipster will add liquibase constraints changelogs here --> <!-- jhipster-needle-liquibase-add-incremental-changelog - JHipster will add incremental liquibase changelogs here --> </databaseChangeLog> diff --git a/src/main/webapp/app/core/config/edu-sharing-config.service.ts b/src/main/webapp/app/core/config/edu-sharing-config.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..d96ef8a22456accac1ea0f97de44a0040e2a677d --- /dev/null +++ b/src/main/webapp/app/core/config/edu-sharing-config.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; +import { ApplicationConfigService } from './application-config.service'; + +@Injectable({ + providedIn: 'root', +}) +export class EduSharingConfigService { + private enabled = false; + + constructor(private http: HttpClient, private applicationConfigService: ApplicationConfigService) { + this.fetchEduSharingAvailability(); + } + + public isEduSharingEnabled(): boolean { + return this.enabled; + } + + private fetchEduSharingAvailability(): void { + const endpoint = this.applicationConfigService.getEndpointFor(SERVER_API_URL + '/api/eduSharingAvailability'); + this.http.get(endpoint, { observe: 'response' }).subscribe({ + next: (response: HttpResponse<any>) => { + this.enabled = response.status === 200; + }, + error: () => { + console.log('Edusharing is disabled'); + }, + }); + } +} diff --git a/src/main/webapp/app/exercise/exercise-details/exercise-details-nonmodal.component.html b/src/main/webapp/app/exercise/exercise-details/exercise-details-nonmodal.component.html index 42aa3d45af015f4163f66f854424f750136249d4..cba9511abb514c2c2e7599ccd1a4b26cc35e924a 100644 --- a/src/main/webapp/app/exercise/exercise-details/exercise-details-nonmodal.component.html +++ b/src/main/webapp/app/exercise/exercise-details/exercise-details-nonmodal.component.html @@ -1,5 +1,5 @@ <div *ngIf="!exercise || !exercise.exerciseId" style="padding: 50px" jhiTranslate="exercise.notFoundLogin"> - The exercise cannot be loaded. Perhaps you must log in to view it. + The exercise cannot be loaded. Perhaps you must log in to view it. test </div> <div *ngIf="exercise && exercise.exerciseId"> <div class="modal-dialog modal-lg modal-dialog-centered"> @@ -16,9 +16,9 @@ <!-- Modal body --> <div class="modal-body"> - <jhi-exercise-body [referencedExercise]="exercise"></jhi-exercise-body> + <jhi-exercise-body [displayEdusharingDetails]="true" [referencedExercise]="exercise"></jhi-exercise-body> </div> </div> + <jhi-markdown-viewer [exercise]="exercise"></jhi-markdown-viewer> </div> - <jhi-markdown-viewer [exercise]="exercise"></jhi-markdown-viewer> </div> diff --git a/src/main/webapp/app/exercise/exercise-details/exercise-details.component.ts b/src/main/webapp/app/exercise/exercise-details/exercise-details.component.ts index d88ca7c33d48abee52d49cdb9ef55857bff748a9..094edce38aa72d546ac6c59ad646639578dc81a6 100644 --- a/src/main/webapp/app/exercise/exercise-details/exercise-details.component.ts +++ b/src/main/webapp/app/exercise/exercise-details/exercise-details.component.ts @@ -1,4 +1,4 @@ -import { HttpResponse } from '@angular/common/http'; +import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Router } from '@angular/router'; import { faArrowRight } from '@fortawesome/free-solid-svg-icons'; @@ -8,7 +8,7 @@ import { Account } from 'app/core/auth/account.model'; import { AccountService } from 'app/core/auth/account.service'; import { AlertService } from 'app/core/util/alert.service'; import { LikesService } from 'app/entities/likes/likes.service'; -import { ExerciseService } from 'app/exercise/service/exercise.service'; +import { encodeURIforExerciseId, ExerciseService } from 'app/exercise/service/exercise.service'; import { SearchService } from 'app/search/service/search-service'; import { ShoppingBasketInfo, ShoppingBasketRedirectInfoDTO } from 'app/shared/model/basket/shopping-basket-info.model'; import { Person } from 'app/shared/model/person.model'; @@ -22,6 +22,9 @@ import { import { PluginService } from 'app/shared/service/plugin-service'; import { WatchlistManager } from 'app/shared/watchlist/watchlist-manager'; import { Subscription } from 'rxjs'; +import { EduSharingStatusDtoModel } from '../../shared/model/search/edu-sharing-status-dto.model'; +import { EduSharingConfigService } from '../../core/config/edu-sharing-config.service'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'jhi-exercise-modal-details', @@ -79,6 +82,8 @@ export class ExerciseHeaderComponent { styleUrls: ['./exerciseComponents/exercise-body.component.scss'], }) export class ExerciseBodyComponent implements OnInit, OnDestroy, AfterViewInit { + @Input() displayEdusharingDetails = false; + @Output() exerciseChangedEvent = new EventEmitter<SearchResultDTO>(); @Input() get referencedExercise(): SearchResultDTO | undefined { return this.exercise; @@ -89,10 +94,21 @@ export class ExerciseBodyComponent implements OnInit, OnDestroy, AfterViewInit { if (newExercise && newExercise.exerciseId !== this.exercise?.exerciseId) { this.exercise = exercise as ExtendedSearchResultDTO; this.updateParent(this.exercise); + if (this.displayEdusharingDetails) { + this.exerciseService.getEduSharingStatus(exercise!.exerciseId).subscribe({ + next: (status: EduSharingStatusDtoModel) => { + this.eduSharingStatus = status; + }, + error: () => console.warn('Could not load edu-sharing status'), + }); + } this.updateGitIsAccessibleForUser(); } } + eduSharingStatus?: EduSharingStatusDtoModel | undefined; + eduSharingExportViolations: string[] = []; + exercise: ExtendedSearchResultDTO | undefined; gitlabIsAccessible = false; @@ -107,11 +123,7 @@ export class ExerciseBodyComponent implements OnInit, OnDestroy, AfterViewInit { hasLiked: boolean | null = null; likeSubscription?: Subscription; authenticated = false; - - faArrowRight = faArrowRight; - - oerLink?: string; - oerExerciseMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/; + isLoading = false; constructor( private reviewManagementService: ReviewManagementService, @@ -123,9 +135,16 @@ export class ExerciseBodyComponent implements OnInit, OnDestroy, AfterViewInit { private watchlistManager: WatchlistManager, private exerciseService: ExerciseService, private applicationInfoService: ApplicationInfoService, - private router: Router + private router: Router, + private eduSharingConfigService: EduSharingConfigService, + public translate: TranslateService ) {} + faArrowRight = faArrowRight; + + oerLink?: string; + oerExerciseMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/; + toggleWithChildren() { this.downloadWithChildren = !this.downloadWithChildren; } @@ -197,6 +216,71 @@ export class ExerciseBodyComponent implements OnInit, OnDestroy, AfterViewInit { this.exportProject(this.exercise!.exerciseId); } + public redirectToNonModalDetails(): void { + const link: string = '/item/' + encodeURIforExerciseId(this.exercise!.exerciseId); + window.open(link, '_self'); + } + + public isAllowedToUploadToEduSharing(): boolean { + if (!this.accountService.isAuthenticated()) { + return false; + } + + const userIsPublisher = + this.exercise!.metadata.publisher.map((publisher: Person) => { + return publisher.email; + }).filter((email: string) => { + return email === this.accountService.getUserEMail(); + }).length > 0; + + return this.accountService.hasAnyAuthority(['ROLE_ADMIN']) || userIsPublisher; + } + + public isEduSharingEnabled(): boolean { + return this.eduSharingConfigService.isEduSharingEnabled(); + } + + public openUpdateExerciseToEduSharingDialog() { + const dialog: any = document.querySelector('#areYouSureEduSharingUpdate'); + const style = dialog['style']; + style['display'] = 'unset'; + } + + public closeUpdateExerciseToEduSharingDialog() { + const dialog: any = document.querySelector('#areYouSureEduSharingUpdate'); + const style = dialog['style']; + style['display'] = 'none'; + } + + public tryUpsertExerciseToEduSharing(): void { + this.closeUpdateExerciseToEduSharingDialog(); + this.eduSharingExportViolations = []; + if (!this.isAllowedToUploadToEduSharing()) { + return; + } + this.isLoading = true; + this.exerciseService.exportExerciseToEduSharing(this.exercise!.exerciseId).subscribe({ + next: status => { + this.eduSharingStatus = status; + this.isLoading = false; + }, + error: (error: HttpErrorResponse) => { + const status = error.status; + if (status === 400 && error?.error.type == 'https://www.jhipster.tech/problem/metadata-non-compliant') { + this.eduSharingExportViolations = error.error.violations; + } else if (status === 403) { + console.log('User is not allowed to upload to edu-sharing'); + } else if (status === 404) { + console.log('Exercise not found'); + } else if (status >= 500) { + this.eduSharingExportViolations.push('UNKNOWN_ERROR'); + } + + this.isLoading = false; + }, + }); + } + exportProject(exerciseId: string) { const recursion = this.downloadWithChildren ? 'WITH_DESCENDANTS' : 'JUST_PROJECT'; return this.searchService.exportProject(exerciseId, recursion).subscribe({ diff --git a/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.html b/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.html index 82d4654c0f5cf69aca0e13aaf4dfa0ce926d3484..1094e43023ad69628b731dc820b0233a5e669623 100644 --- a/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.html +++ b/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.html @@ -217,5 +217,175 @@ ></button> </span> </div> + <div *ngIf="!displayEdusharingDetails && isAuthenticated() && isEduSharingEnabled()" class="col-6" style="margin-top: 20px"> + <span> + <a + (click)="redirectToNonModalDetails()" + aria-pressed="true" + class="btn btn-outline-secondary" + jhiTranslate="exercise.export.edu-sharing.repositoryName" + role="button" + style="float: left; margin-right: 5px; margin-top: 5px" + ></a> + </span> + </div> + </div> + <div *ngIf="displayEdusharingDetails && isAuthenticated() && isEduSharingEnabled()" class="row"> + <div class="col-12"> + <p style="text-align: left; margin-top: 40px"><strong jhiTranslate="exercise.export.edu-sharing.repositoryName"></strong></p> + <hr /> + </div> + <div class="col-12"> + <button + (click)="tryUpsertExerciseToEduSharing()" + *ngIf="isAllowedToUploadToEduSharing() && eduSharingStatus == undefined && isEduSharingEnabled()" + [disabled]="this.isLoading" + aria-pressed="true" + class="btn btn-outline-secondary" + style="float: left; margin-right: 5px; margin-top: 5px" + type="button" + > + <span *ngIf="isLoading" class="spinner-border" role="status" style="width: 1em; height: 1em"> </span> + {{ 'exercise.export.edu-sharing.exportNew' | translate }} + </button> + + <button + (click)="openUpdateExerciseToEduSharingDialog()" + *ngIf="isAllowedToUploadToEduSharing() && eduSharingStatus != undefined && isEduSharingEnabled()" + [disabled]="this.isLoading" + aria-pressed="true" + class="btn btn-outline-secondary" + type="button" + style="float: left; margin-right: 5px; margin-top: 5px" + > + <span *ngIf="isLoading" class="spinner-border" role="status" style="width: 1em; height: 1em"> </span> + {{ 'exercise.export.edu-sharing.exportExisting' | translate }} + </button> + </div> + <div *ngIf="this.eduSharingExportViolations.length > 0" class="col-12" style="margin-top: 10px"> + <div class="alert alert-danger" role="alert"> + <p>{{ 'exercise.export.edu-sharing.errors' | translate }}</p> + <ul> + <li *ngFor="let violationCode of this.eduSharingExportViolations"> + {{ 'exercise.export.edu-sharing.exportViolations.' + violationCode | translate }} + </li> + </ul> + </div> + </div> + + <div *ngIf="eduSharingStatus != undefined" class="col-12" style="margin-top: 20px"> + <jhi-exercise-metadata-item + [description]="'exercise.export.edu-sharing.lastUpload'" + [value]="eduSharingStatus?.lastUpdate?.toLocaleString()" + > + </jhi-exercise-metadata-item> + + <jhi-exercise-metadata-item + [description]="'exercise.export.edu-sharing.nodeId'" + [value]="eduSharingStatus?.baseEduSharingProject?.nodeId" + > + </jhi-exercise-metadata-item> + + <jhi-exercise-metadata-item + [description]="'exercise.export.edu-sharing.viewUrl'" + [value]="eduSharingStatus?.baseEduSharingProject?.viewUrl" + link="{{ eduSharingStatus?.baseEduSharingProject?.viewUrl }}" + > + </jhi-exercise-metadata-item> + </div> + + <div *ngIf="eduSharingStatus != undefined" class="col-12" style="margin-top: 40px; margin-bottom: 40px"> + <h6>{{ 'exercise.export.edu-sharing.workflow.updates' | translate }}</h6> + + <table + *ngIf="eduSharingStatus?.workflows != undefined" + [dataSource]="eduSharingStatus!.workflows" + class="edu-sharing-table" + mat-table + > + <ng-container matColumnDef="time"> + <th *matHeaderCellDef mat-header-cell>{{ 'exercise.export.edu-sharing.workflow.time' | translate }}</th> + <td *matCellDef="let element" mat-cell>{{ element?.time?.toLocaleString() }}</td> + </ng-container> + + <ng-container matColumnDef="status"> + <th *matHeaderCellDef mat-header-cell>{{ 'exercise.export.edu-sharing.workflow.status' | translate }}</th> + <td *matCellDef="let element" mat-cell>{{ element?.status }}</td> + </ng-container> + + <ng-container matColumnDef="comment"> + <th *matHeaderCellDef mat-header-cell>{{ 'exercise.export.edu-sharing.workflow.comment' | translate }}</th> + <td *matCellDef="let element" mat-cell>{{ element?.comment }}</td> + </ng-container> + + <tr *matHeaderRowDef="['time', 'status', 'comment']" mat-header-row></tr> + <tr *matRowDef="let row; columns: ['time', 'status', 'comment']" mat-row></tr> + </table> + </div> + + <div *ngIf="eduSharingStatus != undefined" class="col-12" style="margin-top: 20px"> + <h6>{{ 'exercise.export.edu-sharing.publishedProjects.title' | translate }}</h6> + + <span *ngIf="eduSharingStatus?.publishedCopies?.length == 0"> + <i>{{ 'exercise.export.edu-sharing.publishedProjects.noEntries' | translate }}</i> + </span> + + <table + *ngIf="eduSharingStatus!.publishedCopies.length > 0" + [dataSource]="eduSharingStatus!.publishedCopies" + class="edu-sharing-table" + mat-table + > + <ng-container matColumnDef="nodeId"> + <th *matHeaderCellDef mat-header-cell>{{ 'exercise.export.edu-sharing.nodeId' | translate }}</th> + <td *matCellDef="let element" mat-cell>{{ element?.nodeId }}</td> + </ng-container> + + <ng-container matColumnDef="date"> + <th *matHeaderCellDef mat-header-cell>{{ 'exercise.export.edu-sharing.publishedProjects.createdAt' | translate }}</th> + <td *matCellDef="let element" mat-cell>{{ element?.createdAt }}</td> + </ng-container> + + <ng-container matColumnDef="viewUrl"> + <th *matHeaderCellDef mat-header-cell>{{ 'exercise.export.edu-sharing.viewUrl' | translate }}</th> + <td *matCellDef="let element" mat-cell><a [href]="element?.viewUrl" target="_blank">URL</a></td> + </ng-container> + + <tr *matHeaderRowDef="['nodeId', 'date', 'viewUrl']" mat-header-row></tr> + <tr *matRowDef="let row; columns: ['nodeId', 'date', 'viewUrl']" mat-row></tr> + </table> + </div> + </div> + + <div class="modal" id="areYouSureEduSharingUpdate"> + <div class="modal-dialog modal-lg modal-dialog-centered"> + <div class="modal-content"> + <div style="position: absolute; right: 40px"></div> + <div class="modal-header"> + {{ 'global.areYouSure' | translate }} + </div> + + <div class="modal-body"> + <p jhiTranslate="exercise.export.edu-sharing.exportNotice"></p> + </div> + + <div class="modal-footer"> + <button + type="button" + class="btn btn-outline-secondary" + data-dismiss="modal" + (click)="closeUpdateExerciseToEduSharingDialog()" + jhiTranslate="global.no" + ></button> + <button + type="button" + class="btn btn-outline-secondary" + data-dismiss="modal" + jhiTranslate="global.yes" + (click)="this.tryUpsertExerciseToEduSharing()" + ></button> + </div> + </div> + </div> </div> </div> diff --git a/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.scss b/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.scss index c503974ed019730774c6287174a556c4726373de..47be10c7a3e7e6eb991253c74c90134c625cf01f 100644 --- a/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.scss +++ b/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.scss @@ -77,3 +77,17 @@ #detailModal { overflow-y: scroll; } + +table { + &.edu-sharing-table, + .mat-table { + width: 100%; + + td { + &.mat-cell { + padding-left: 10px; + padding-right: 10px; + } + } + } +} diff --git a/src/main/webapp/app/exercise/exercise.module.ts b/src/main/webapp/app/exercise/exercise.module.ts index 527dde66c0860709b7049461505eaf2bc877a5a5..bc04a570250262694257317431619b762a882998 100644 --- a/src/main/webapp/app/exercise/exercise.module.ts +++ b/src/main/webapp/app/exercise/exercise.module.ts @@ -6,15 +6,16 @@ import { MarkDownViewerComponent } from '../exercise/markDownViewer/markDownView import { ExerciseCardComponent } from './exercise-card/exercise-card.component'; import { + ExerciseBodyComponent, ExerciseDetailsModalComponent, ExerciseDetailsNonModalComponent, ExerciseHeaderComponent, - ExerciseBodyComponent, } from './exercise-details/exercise-details.component'; import { ExerciseMetadataItemComponent } from './exercise-details/exercise-metadata/exercise-metadata-item/exercise-metadata-item.component'; import { ExerciseMetadataComponent } from './exercise-details/exercise-metadata/exercise-metadata.component'; import { ExerciseTreeComponent } from './exercise-details/exercise-tree/exercise-tree.component'; import { ReviewBadgeComponent } from './review-badge/review-badge.component'; +import { MatTableModule } from '@angular/material/table'; @NgModule({ imports: [ @@ -32,6 +33,7 @@ import { ReviewBadgeComponent } from './review-badge/review-badge.component'; }, }, }), + MatTableModule, ], declarations: [ ExerciseCardComponent, diff --git a/src/main/webapp/app/exercise/service/exercise.service.ts b/src/main/webapp/app/exercise/service/exercise.service.ts index 4d63047e077b657325d43853cacdaafc3105670f..69708eb4c6875182f6e20ef566f26de3e49b1607 100644 --- a/src/main/webapp/app/exercise/service/exercise.service.ts +++ b/src/main/webapp/app/exercise/service/exercise.service.ts @@ -8,11 +8,13 @@ import { ArtemisExerciseInfo } from 'app/shared/model/artemis-exercise-info.mode import { catchError, map } from 'rxjs/operators'; import { ChildInfo, ExtendedSearchResultDTO, hasChildren, SearchResultDTO } from 'app/shared/model/search/search-result-dto.model'; import { BehaviorSubject, Observable, of } from 'rxjs'; +import { EduSharingStatusDtoModel } from '../../shared/model/search/edu-sharing-status-dto.model'; @Injectable({ providedIn: 'root' }) export class ExerciseService { public resourceUrl = this.applicationConfigService.getEndpointFor('api/exerciseFile/'); public exerciseUrl: string = this.applicationConfigService.getEndpointFor('api/exercise/'); + public exercisesUrl: string = this.applicationConfigService.getEndpointFor('api/exercises'); public exerciseChildrenUrl: string = this.applicationConfigService.getEndpointFor('api/exerciseChildren/'); constructor( @@ -172,6 +174,34 @@ export class ExerciseService { public validateExerciseInfo(exerciseInfo: ArtemisExerciseInfo): Observable<string> { return this.http.post<string>(`${this.exerciseUrl}validate-exercise-info`, exerciseInfo); } + + public getEduSharingStatus(exerciseId: string): Observable<EduSharingStatusDtoModel> { + return this.http.get<EduSharingStatusDtoModel>(`${this.exercisesUrl}/${encodeURIforExerciseId(exerciseId)}/edu-sharing-status`).pipe( + map((dto: EduSharingStatusDtoModel) => { + dto.workflows.forEach(workflow => { + workflow.time = new Date(workflow.time); + }); + return dto; + }) + ); + } + + public exportExerciseToEduSharing(exerciseId: string): Observable<EduSharingStatusDtoModel> { + return this.http + .put<EduSharingStatusDtoModel>( + `${this.exercisesUrl}/${encodeURIforExerciseId(exerciseId)}/edu-sharing-status`, + {}, + { observe: 'response' } + ) + .pipe( + map((response: HttpResponse<any>) => { + if (response.ok) { + return response.body as EduSharingStatusDtoModel; + } + throw new Error('Export failed'); + }) + ); + } } /* diff --git a/src/main/webapp/app/exercisePage/exercise.component.scss b/src/main/webapp/app/exercisePage/exercise.component.scss index e76adba3e6eebc0b1b61bdf10ce2459da831cda4..6e5e00165cd72dd87559cb58abea446ce52c20bf 100644 --- a/src/main/webapp/app/exercisePage/exercise.component.scss +++ b/src/main/webapp/app/exercisePage/exercise.component.scss @@ -1,4 +1,5 @@ .exercise-container { padding: 20px 20px 20px 100px; + width: 100%; vertical-align: center; } diff --git a/src/main/webapp/app/shared/model/search/edu-sharing-status-dto.model.ts b/src/main/webapp/app/shared/model/search/edu-sharing-status-dto.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..e4aa4f59c951d800ca3c29a918d26da78dfa5ee5 --- /dev/null +++ b/src/main/webapp/app/shared/model/search/edu-sharing-status-dto.model.ts @@ -0,0 +1,18 @@ +export interface EduSharingStatusDtoModel { + baseEduSharingProject: EduSharingProjectDtoModel; + publishedCopies: EduSharingProjectDtoModel[]; + workflows: EduSharingWorkflowDtoModel[]; + lastUpdate: Date; +} + +export interface EduSharingProjectDtoModel { + nodeId: string; + viewUrl: string; + createdAt: Date; +} + +export interface EduSharingWorkflowDtoModel { + time: Date; + status: string; + comment: string; +} diff --git a/src/main/webapp/app/shared/shared.module.ts b/src/main/webapp/app/shared/shared.module.ts index 2ad10b6428c147aab58b2e1320674d642504778b..1ac153601c727f145f1d76c09f3b812b615054ff 100644 --- a/src/main/webapp/app/shared/shared.module.ts +++ b/src/main/webapp/app/shared/shared.module.ts @@ -20,9 +20,11 @@ import { FilterEmailPipe } from 'app/admin/review-management/filterEmail.pipe'; import { ReviewHistoryComponent } from './review/review-history/review-history.component'; import { MatTableModule } from '@angular/material/table'; import { EditSearchModalComponent } from 'app/bookmarksAndSearches/savedSearches/editSearch.component'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; @NgModule({ - imports: [SharedLibsModule, MatTableModule], + imports: [SharedLibsModule, MatTableModule, MatDialogModule, MatButtonModule], declarations: [ FindLanguageFromKeyPipe, diff --git a/src/main/webapp/i18n/de/exercise.json b/src/main/webapp/i18n/de/exercise.json index 023286699cb871f5558e23ebaee5eb3294e162d2..2fde4d9bd601b64f35d81ab0d2256496cc75007b 100644 --- a/src/main/webapp/i18n/de/exercise.json +++ b/src/main/webapp/i18n/de/exercise.json @@ -141,7 +141,44 @@ "export": "Exportieren", "artemis": "Artemis", "latex": "LaTeX", - "download": "Herunterladen" + "download": "Herunterladen", + "edu-sharing": { + "errors": "Fehler", + "exportNew": "Neue Edu-Sharing Node erstellen", + "exportExisting": "Edu-Sharing Node updaten", + "exportNotice": "Falls Sie nur Metadaten upgedatet haben, bitten wir Sie ihre Änderungen lediglich per Mail an <a href='mailto:oer@uibk.ac.at'>oer@uibk.ac.at</a> zu senden.<br/><br/>Mit dem automatischen Update fortfahren?", + "repositoryName": "Edu-Sharing UIBK", + "lastUpload": "Letztes Update", + "nodeId": "Edu-Sharing Node ID", + "viewUrl": "URL", + "workflow": { + "updates": "Workflow Updates", + "time": "Zeit", + "status": "Status", + "comment": "Kommentar" + }, + "publishedProjects": { + "title": "Publizierte Kopien", + "noEntries": "Keine publizierten Kopien gefunden", + "createdAt": "Erstellt am" + }, + "exportViolations": { + "UNKNOWN_ERROR": "Ein unbekannter Fehler ist aufgetreten", + "COLLECTIONS_NOT_SUPPORTED": "Sammlungen werden nicht unterstützt", + "LICENSE_INVALID": "Lizenz ist ungültig", + "LICENSE_NOT_SUPPORTED": "Es werden nur CC-BY-SA 4.0 oder CC0 Lizenzen unterstützt", + "KEYWORDS_EMPTY": "Keywords sind leer", + "DESCRIPTION_EMPTY": "Beschreibung ist leer", + "LANGUAGE_CODE_INVALID": "Language-Code ist ungültig", + "PUBLISHERS_EMPTY": "Veröffentlichende fehlen", + "CREATORS_EMPTY": "Erstellende fehlen", + "PROJECT_INVALID": "Projekt ist ungültig", + "PERSONS_NOT_SERIALIZABLE": "Personen sind nicht serialisierbar", + "PREVIEW_IMAGE_ERROR": "Vorschaubild konnte nicht geladen werden", + "PROGRAMMING_LANGUAGE_EMPTY": "Programmiersprachen sind leer", + "RESOURCE_WITH_TITLE_EXISTS": "Der Titel dieser Resource ist bereits im Repository registriert." + } + } }, "open": "Aufgabe öffnen", "more": "Mehr ...", diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index 4e7129d22c47748622854dc3aadba9bc975042ee..87e83116ab86299629aa7c9000f882bcecd9bccb 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -122,7 +122,10 @@ "ribbon": { "dev": "Development" }, - "item-count": "Ergebnis {{first}} - {{second}} von {{total}} Elemente." + "item-count": "Ergebnis {{first}} - {{second}} von {{total}} Elemente.", + "yes": "Ja", + "no": "Nein", + "areYouSure": "Sind Sie sicher?" }, "entity": { "action": { diff --git a/src/main/webapp/i18n/en/exercise.json b/src/main/webapp/i18n/en/exercise.json index 8e98c5fa32901547fac84c6f2c99e5429fc9ac7c..714e54ed247383c1da7ac382cfada7deda424e35 100644 --- a/src/main/webapp/i18n/en/exercise.json +++ b/src/main/webapp/i18n/en/exercise.json @@ -141,7 +141,44 @@ "export": "Export", "artemis": "Artemis", "latex": "LaTeX", - "download": "Download" + "download": "Download", + "edu-sharing": { + "errors": "Errors", + "exportNew": "Upload resource to edu-sharing", + "exportExisting": "Update edu-sharing node", + "exportNotice": "If you have only updated metadata, please only send your changes by email to <a href='mailto:oer@uibk.ac.at'>oer@uibk.ac.at</a>.<br/><br/>Continue with the automatic update?", + "repositoryName": "Edu-Sharing UIBK", + "lastUpload": "Last Upload", + "nodeId": "Edu-Sharing Node ID", + "viewUrl": "URL", + "workflow": { + "updates": "Workflow Updates", + "time": "Time", + "status": "Status", + "comment": "Comment" + }, + "publishedProjects": { + "title": "Published Copies", + "noEntries": "No published copies found", + "createdAt": "Created at" + }, + "exportViolations": { + "UNKNOWN_ERROR": "An unknown error occurred", + "COLLECTIONS_NOT_SUPPORTED": "Collections are not supported", + "LICENSE_INVALID": "License is invalid", + "LICENSE_NOT_SUPPORTED": "Only CC-BY-SA 4.0 or CC0 licenses are supported", + "KEYWORDS_EMPTY": "Keywords are empty", + "DESCRIPTION_EMPTY": "Description is empty", + "LANGUAGE_CODE_INVALID": "Language code is invalid", + "PUBLISHERS_EMPTY": "Publishers are empty", + "CREATORS_EMPTY": "Creators are empty", + "PROJECT_INVALID": "Project is invalid", + "PERSONS_NOT_SERIALIZABLE": "Persons are not serializable", + "PREVIEW_IMAGE_ERROR": "Preview image error", + "PROGRAMMING_LANGUAGE_EMPTY": "Programming languages are empty", + "RESOURCE_WITH_TITLE_EXISTS": "This resource's titles is conflicting with an already present title in the repository" + } + } }, "open": "Open Exercise", "more": "More ...", diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index d18abb7a27cef98c35207bcec0897746a25ba0ea..99601721a9cf4c469e29b79c4dea7271277da307 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -122,7 +122,10 @@ "ribbon": { "dev": "Development" }, - "item-count": "Showing {{first}} - {{second}} of {{total}} items." + "item-count": "Showing {{first}} - {{second}} of {{total}} items.", + "yes": "Yes", + "no": "No", + "areYouSure": "Are you sure?" }, "entity": { "action": { diff --git a/src/test/java/at/ac/uibk/gitsearch/edu_sharing/CreationDtoTest.java b/src/test/java/at/ac/uibk/gitsearch/edu_sharing/CreationDtoTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c94acd0a6fda8dace5f69b29479992f9827309e0 --- /dev/null +++ b/src/test/java/at/ac/uibk/gitsearch/edu_sharing/CreationDtoTest.java @@ -0,0 +1,53 @@ +package at.ac.uibk.gitsearch.edu_sharing; + +import at.ac.uibk.gitsearch.edu_sharing.model.AggregationLevel; +import at.ac.uibk.gitsearch.edu_sharing.model.EduSharingMetadataDTO; +import at.ac.uibk.gitsearch.edu_sharing.model.LicenceDTO; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.util.List; +import org.apache.commons.collections4.CollectionUtils; +import org.codeability.sharing.plugins.api.search.PersonDTO; +import org.junit.Assert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +public class CreationDtoTest { + + @Test + @Timeout(60) + public void shouldCreateValidDto() throws IOException { + var person1 = new PersonDTO(); + person1.setEmail("email1@uibk.ac.at"); + person1.setName("Vorname Nachname1"); + person1.setAffiliation("UIBK"); + + var person2 = new PersonDTO(); + person2.setName("Vorname Mittelname Nachname, Msc"); + + var dto = new EduSharingMetadataDTO(); + dto.setDescription("this is a description"); + dto.setName("repo.zip"); + dto.setAggregationLevel(AggregationLevel.MATERIALS); + dto.setContentContributor(List.of(person1, person2)); + dto.setMetadataContributor(List.of(person1)); + dto.setVersion("1.4"); + dto.setLanguages(List.of("de")); + dto.setTitle("title"); + dto.setFormat("zip"); + dto.setEducationalContext("higher education"); + dto.setLicence(new LicenceDTO("CC-BY-SA 4.0")); + dto.setKeywords(List.of("KW1", "KW2")); + + ObjectMapper objectMapper = new ObjectMapper(); + String json = objectMapper.writeValueAsString(dto); + System.out.println(json); + + var dto2 = objectMapper.readValue(json, EduSharingMetadataDTO.class); + + Assert.assertTrue(CollectionUtils.isEqualCollection(dto.getContentContributor(), dto2.getContentContributor())); + Assert.assertTrue(CollectionUtils.isEqualCollection(dto.getMetadataContributor(), dto2.getMetadataContributor())); + + Assert.assertEquals(dto, dto2); + } +} diff --git a/src/test/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTOSerializerTest.java b/src/test/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTOSerializerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f97009115615141cf9cc2e32cd04e93d123d9b59 --- /dev/null +++ b/src/test/java/at/ac/uibk/gitsearch/edu_sharing/model/serializer/VCardDTOSerializerTest.java @@ -0,0 +1,37 @@ +package at.ac.uibk.gitsearch.edu_sharing.model.serializer; + +import java.io.IOException; +import org.junit.Assert; +import org.junit.jupiter.api.Test; + +class VCardDTOSerializerTest { + + @Test + void shouldCreateCorrectVCard() throws IOException { + Assert.assertEquals("Nachname;Vorname;;Univ.-Prof. Dr.;", VCardDTOSerializer.getVCardName("Univ.-Prof. Dr. Vorname Nachname")); + Assert.assertEquals( + "Nachname;Vorname;;Univ.-Prof. Dr.;BSc MSc", + VCardDTOSerializer.getVCardName("Univ.-Prof. Dr. Vorname Nachname, BSc MSc") + ); + Assert.assertEquals( + "Nachname;Vorname;Zweiter Name;Univ.-Prof. Dr.;BSc MSc", + VCardDTOSerializer.getVCardName("Univ.-Prof. Dr. Vorname Zweiter Name Nachname, BSc MSc") + ); + Assert.assertEquals( + "Nachname;Vorname;Zweiter Name;Univ.-Prof. Dr.;BSc MSc", + VCardDTOSerializer.getVCardName("Univ.-Prof. Dr. Vorname Zweiter Name Nachname, BSc MSc") + ); + Assert.assertEquals( + "Nachname;Vorname;Zweiter Name;;BSc MSc", + VCardDTOSerializer.getVCardName("Vorname Zweiter Name Nachname, BSc MSc") + ); + Assert.assertEquals("Nachname;Vorname;;;BSc MSc", VCardDTOSerializer.getVCardName("Vorname Nachname, BSc MSc")); + Assert.assertEquals("Nachname;Vorname;;;BSc", VCardDTOSerializer.getVCardName("Vorname Nachname, BSc")); + Assert.assertEquals("Nachname;Vorname;;;", VCardDTOSerializer.getVCardName("Vorname Nachname")); + Assert.assertThrows( + VCardSerializationException.class, + () -> VCardDTOSerializer.getVCardName("Univ.-Prof. Dr. Vorname Zweiter Name. Nachname, BSc MSc") + ); + Assert.assertThrows(VCardSerializationException.class, () -> VCardDTOSerializer.getVCardName(" ")); + } +} diff --git a/src/test/java/at/ac/uibk/gitsearch/service/dto/VariousDTOTest.java b/src/test/java/at/ac/uibk/gitsearch/service/dto/VariousDTOTest.java index c09e2ca37d18d6b29254edb658f315e0815b9f81..41b00bf683366a42360b45638c659ebf69a21a0c 100644 --- a/src/test/java/at/ac/uibk/gitsearch/service/dto/VariousDTOTest.java +++ b/src/test/java/at/ac/uibk/gitsearch/service/dto/VariousDTOTest.java @@ -9,10 +9,10 @@ import java.lang.reflect.InvocationTargetException; import nl.jqno.equalsverifier.EqualsVerifier; import nl.jqno.equalsverifier.Warning; import org.codeability.sharing.plugins.api.search.GitProjectDTO; +import org.codeability.sharing.plugins.api.search.PersonDTO; import org.codeability.sharing.plugins.api.search.SearchResultDTO; import org.codeability.sharing.plugins.api.search.SearchResultsDTO; import org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO; -import org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO.Person; import org.junit.Assert; import org.junit.jupiter.api.Test; @@ -57,7 +57,7 @@ class VariousDTOTest { void testSearchResultsDTO() throws IllegalAccessException, InvocationTargetException { propertiesTester.testProperties(SearchResultsDTO.class); propertiesTester.testProperties(SearchResultDTO.class); - propertiesTester.testProperties(Person.class); + propertiesTester.testProperties(PersonDTO.class); propertiesTester.testProperties(GitProjectDTO.class); // just for test coverage @@ -120,6 +120,6 @@ class VariousDTOTest { "commit_id" ) .verify(); - EqualsVerifier.forClass(Person.class).suppress(Warning.NONFINAL_FIELDS).verify(); + EqualsVerifier.forClass(PersonDTO.class).suppress(Warning.NONFINAL_FIELDS).verify(); } } diff --git a/src/test/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyServiceTest.java b/src/test/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyServiceTest.java index 511b7411f018b302caa415ad95322d6b33369908..3b5bb0b047fb37b8e7b3267299de3f1c4ee39e6a 100644 --- a/src/test/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyServiceTest.java +++ b/src/test/java/at/ac/uibk/gitsearch/service/vocabulary/VocabularyServiceTest.java @@ -4,10 +4,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasItemInArray; -import static org.hamcrest.Matchers.hasKey; -import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.*; import at.ac.uibk.gitsearch.GitsearchApp; import at.ac.uibk.gitsearch.domain.vocabulary.VocabularyItem; @@ -16,8 +13,6 @@ import at.ac.uibk.gitsearch.repository.vocabulary.VocabularyRepository; import at.ac.uibk.gitsearch.service.gitlab_events.ExtendedUserProvidedMetadataDTO; import at.ac.uibk.gitsearch.service.vocabulary.VocabularyService.AlternativesProposer; import at.ac.uibk.gitsearch.service.vocabulary.VocabularyService.ValidationResultDTO; -import at.ac.uibk.gitsearch.service.vocabulary.VocabularyService.VocabularyServiceConfig; -import at.ac.uibk.gitsearch.service.vocabulary.VocabularyService.VocabularyServiceConfig.VocabularySetting; import com.fasterxml.jackson.core.exc.StreamReadException; import com.fasterxml.jackson.databind.DatabindException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -25,11 +20,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import java.io.IOException; import java.io.InputStream; import java.net.URI; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Stream; import org.assertj.core.data.Offset; import org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO; @@ -234,7 +225,7 @@ class VocabularyServiceTest { return; } - final String[] extraEntries = setting.get().getExtraEntries(); + final ExtraEntry[] extraEntries = setting.get().getExtraEntries(); Arrays .stream(extraEntries) @@ -247,7 +238,13 @@ class VocabularyServiceTest { ), Arrays .stream(items) - .anyMatch(item -> Arrays.stream(item.getLanguageItem()).anyMatch(li -> extraEntry.equals(li.getTitle()))) + .anyMatch(item -> + Arrays + .stream(item.getLanguageItem()) + .anyMatch(li -> + extraEntry.getGermanEntry().equals(li.getTitle()) && extraEntry.getEnglishEntry().equals(li.getTitle()) + ) + ) ) ); } diff --git a/src/test/java/at/ac/uibk/gitsearch/web/rest/ValidationCheckerResourceIT.java b/src/test/java/at/ac/uibk/gitsearch/web/rest/ValidationCheckerResourceIT.java index 5525e6191476ffd2a368557003056b90e55e8d05..2743be3ea9005360b0b372af82d39b95a223e645 100644 --- a/src/test/java/at/ac/uibk/gitsearch/web/rest/ValidationCheckerResourceIT.java +++ b/src/test/java/at/ac/uibk/gitsearch/web/rest/ValidationCheckerResourceIT.java @@ -31,6 +31,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; import org.apache.http.HttpHost; +import org.codeability.sharing.plugins.api.search.PersonDTO; import org.codeability.sharing.plugins.api.search.SearchResultDTO; import org.codeability.sharing.plugins.api.search.UserProvidedMetadataDTO; import org.elasticsearch.client.RestClient; @@ -161,12 +162,12 @@ class ValidationCheckerResourceIT { private UserProvidedMetadataDTO createBasicMetaData() { UserProvidedMetadataDTO metaData = new UserProvidedMetadataDTO(); - final UserProvidedMetadataDTO.Person person1 = new UserProvidedMetadataDTO.Person(); + final PersonDTO person1 = new PersonDTO(); person1.setName("Toni Tester"); person1.setEmail("Toni.Tester@codeability.org"); person1.setAffiliation("Codeability Org"); - metaData.setCreator(new UserProvidedMetadataDTO.Person[] { person1 }); - metaData.setPublisher(new UserProvidedMetadataDTO.Person[] { person1 }); + metaData.setCreator(new PersonDTO[] { person1 }); + metaData.setPublisher(new PersonDTO[] { person1 }); metaData.setFormat(new String[] { "pdf" }); return metaData; } diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index b7925e1af5528d68d106dca68518e13df00aad59..e6d8ec5f2c2ef1e8cf377d23141f6ef426b62f67 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -184,3 +184,16 @@ testing: testUser: name: 'Toni Tester' gitLabToken: SomeTestToken + +edu-sharing-integration: + enabled: true + base-url: 'PLACEHOLDER_NOTNEEDED' + auth: + username: 'PLACEHOLDER_NOTNEEDED' + password: 'PLACEHOLDER_NOTNEEDED' + content: + repository: 'REPOSITORY' + base-node: 'BASE_NODE' + editorial: + group-authority-name: 'GROUP_EDITOR' + status-to-check: '200_tocheck'