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..c3cb2a39f7cf2b8bd4759a7a39853f75220fe860 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 @@ -43,7 +45,7 @@ services: options: max-size: 50m depends_on: - - keycloak + # - keycloak - sharing_mysql # - sharing_elasticsearch networks: @@ -66,54 +68,54 @@ services: restart: always networks: - backend - postgres: - image: postgres - volumes: - - postgres_data:/var/lib/postgresql/data - logging: - options: - max-size: 50m - environment: - - POSTGRES_DB=keycloak - - POSTGRES_USER=${POSTGRES_USER_KEYCLOAK} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - - PGDATA=/var/lib/postgresql/data/pgdata - restart: always - networks: - - backend - - frontend - depends_on: - - sharing_mysql - keycloak: - image: quay.io/keycloak/keycloak:18.0.2-legacy - environment: - - DB_VENDOR=POSTGRES - - DB_ADDR=postgres - - DB_DATABASE=keycloak - - DB_USER=${POSTGRES_USER_KEYCLOAK} - - DB_SCHEMA=public - - DB_PASSWORD=${POSTGRES_PASSWORD} - - KEYCLOAK_USER=${KEYCLOAK_USER} - - KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD} - - PROXY_ADDRESS_FORWARDING=true - - GITSEARCH_PATH=$GITSEARCH_PATH - # Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it. - #JDBC_PARAMS: "ssl=true" - ports: - - 8082:8080 - logging: - options: - max-size: 50m - restart: always - volumes: - - $GITSEARCH_PATH/src/main/resources/keycloak-theme/themes/gitsearch:/opt/jboss/keycloak/themes/gitsearch - #- /home/contDeploy/gitsearch2/gitsearch/src/main/resources/keycloak-theme/themes/gitsearch:/opt/jboss/keycloak/themes/gitsearch - # - ../resources/keycloak-theme/configuration:/opt/jboss/keycloak/standalone/configuration - depends_on: - - postgres - networks: - - backend - - frontend + # postgres: + # image: postgres + # volumes: + # - postgres_data:/var/lib/postgresql/data + # logging: + # options: + # max-size: 50m + # environment: + # - POSTGRES_DB=keycloak + # - POSTGRES_USER=${POSTGRES_USER_KEYCLOAK} + # - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + # - PGDATA=/var/lib/postgresql/data/pgdata + # restart: always + # networks: + # - backend + # - frontend + # depends_on: + # - sharing_mysql + # keycloak: + # image: quay.io/keycloak/keycloak:18.0.2-legacy + # environment: + # - DB_VENDOR=POSTGRES + # - DB_ADDR=postgres + # - DB_DATABASE=keycloak + # - DB_USER=${POSTGRES_USER_KEYCLOAK} + # - DB_SCHEMA=public + # - DB_PASSWORD=${POSTGRES_PASSWORD} + # - KEYCLOAK_USER=${KEYCLOAK_USER} + # - KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD} + # - PROXY_ADDRESS_FORWARDING=true + # - GITSEARCH_PATH=$GITSEARCH_PATH + # # Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it. + # #JDBC_PARAMS: "ssl=true" + # ports: + # - 8082:8080 + # logging: + # options: + # max-size: 50m + # restart: always + # volumes: + # - $GITSEARCH_PATH/src/main/resources/keycloak-theme/themes/gitsearch:/opt/jboss/keycloak/themes/gitsearch + # #- /home/contDeploy/gitsearch2/gitsearch/src/main/resources/keycloak-theme/themes/gitsearch:/opt/jboss/keycloak/themes/gitsearch + # # - ../resources/keycloak-theme/configuration:/opt/jboss/keycloak/standalone/configuration + # depends_on: + # - postgres + # networks: + # - backend + # - frontend docker-hoster: image: dvdarias/docker-hoster volumes: 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'