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