diff --git a/src/main/java/at/ac/uibk/gitsearch/es/model/ArtemisExerciseInfo.java b/src/main/java/at/ac/uibk/gitsearch/es/model/ArtemisExerciseInfo.java
index b8f68089f494bd4be7cd5eee19b3ac42cbd8d615..93edd4dd14c8b8d381e9a47ecb2d891c0a6de1be 100644
--- a/src/main/java/at/ac/uibk/gitsearch/es/model/ArtemisExerciseInfo.java
+++ b/src/main/java/at/ac/uibk/gitsearch/es/model/ArtemisExerciseInfo.java
@@ -131,7 +131,9 @@ public class ArtemisExerciseInfo extends ExerciseInfo {
             exerciseInfo.setIsBasedOn(this.getIsBasedOn());
         }
         exerciseInfo.setKeyword(this.getKeyword());
-        exerciseInfo.setLanguage(this.getLanguage());
+        if (!this.getLanguage().isEmpty()) {
+            exerciseInfo.setLanguage(this.getLanguage());
+        }
         exerciseInfo.setLearningResourceType(this.getLearningResourceType());
         exerciseInfo.setLicense(this.getLicense());
         exerciseInfo.setMetadataVersion(this.getMetadataVersion());
@@ -205,8 +207,39 @@ public class ArtemisExerciseInfo extends ExerciseInfo {
         return exerciseInfo;
     }
 
+    /**
+     * Method checking if the mandatory metadata fields are contained and not empty
+     *
+     * @return true if all fields are present, false otherwise
+     */
+    public boolean validateMandatoryFields() {
+        return (
+            this.getMetadataVersion() != null &&
+            !this.getMetadataVersion().isBlank() &&
+            this.getTitle() != null &&
+            !this.getTitle().isBlank() &&
+            this.getDescription() != null &&
+            !this.getDescription().isBlank() &&
+            this.getKeyword() != null &&
+            !this.getKeyword().isEmpty() &&
+            this.getCreator() != null &&
+            !this.getCreator().isEmpty() &&
+            this.getPublisher() != null &&
+            !this.getPublisher().isEmpty() &&
+            this.getLicense() != null &&
+            !this.getLicense().isBlank() &&
+            this.getLanguage() != null &&
+            !this.getLanguage().isEmpty() &&
+            this.getFormat() != null &&
+            !this.getFormat().isEmpty() &&
+            this.getLearningResourceType() != null &&
+            !this.getLearningResourceType().isBlank()
+        );
+    }
+
     /**
      * Used to merge attributes of ExerciseInfo into an ArtemisExerciseInfo class
+     *
      * @param exerciseInfo to merge
      */
     public void appendExerciseInfo(ExerciseInfo exerciseInfo) {
@@ -280,4 +313,26 @@ public class ArtemisExerciseInfo extends ExerciseInfo {
         INDIVIDUAL,
         TEAM,
     }
+
+    /**
+     * Represents the mappings of difficulty values between
+     * Artemis and Gitserach.
+     *
+     * Example: Artemis difficulty value EASY maps to Gitserach difficulty value SIMPLE
+     */
+    public enum DifficultyMapping {
+        EASY("SIMPLE"),
+        MEDIUM("MEDIUM"),
+        HARD("ADVANCED");
+
+        private final String difficulty;
+
+        DifficultyMapping(String difficulty) {
+            this.difficulty = difficulty;
+        }
+
+        public String getDifficulty() {
+            return this.difficulty;
+        }
+    }
 }
diff --git a/src/main/java/at/ac/uibk/gitsearch/service/ExerciseService.java b/src/main/java/at/ac/uibk/gitsearch/service/ExerciseImportService.java
similarity index 61%
rename from src/main/java/at/ac/uibk/gitsearch/service/ExerciseService.java
rename to src/main/java/at/ac/uibk/gitsearch/service/ExerciseImportService.java
index 2388fcf1f2956bd50311979fe08b333fabaca9d2..26de1487b4b49cbdbb93c4786183b80f08fc49cb 100644
--- a/src/main/java/at/ac/uibk/gitsearch/service/ExerciseService.java
+++ b/src/main/java/at/ac/uibk/gitsearch/service/ExerciseImportService.java
@@ -2,15 +2,14 @@ package at.ac.uibk.gitsearch.service;
 
 import at.ac.uibk.gitsearch.es.model.ArtemisExerciseInfo;
 import at.ac.uibk.gitsearch.properties.ApplicationProperties;
-import com.fasterxml.jackson.core.JsonFactory;
+import at.ac.uibk.gitsearch.service.dto.MetadataUserDTO;
 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 com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
 import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.FileWriter;
 import java.io.IOException;
 import java.io.InputStream;
@@ -19,10 +18,9 @@ import java.net.URISyntaxException;
 import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
-import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Objects;
-import java.util.zip.ZipEntry;
+import java.util.stream.Collectors;
 import java.util.zip.ZipInputStream;
 import javax.ws.rs.client.Client;
 import javax.ws.rs.client.ClientBuilder;
@@ -30,8 +28,6 @@ import javax.ws.rs.client.WebTarget;
 import net.minidev.json.JSONObject;
 import net.minidev.json.JSONStyle;
 import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.jgit.api.errors.GitAPIException;
 import org.eclipse.jgit.lib.Repository;
 import org.gitlab4j.api.GitLabApiException;
 import org.gitlab4j.api.models.Group;
@@ -41,47 +37,39 @@ import org.springframework.stereotype.Service;
 
 @Service
 @SuppressWarnings({ "PMD.GodClass", "PMD.CognitiveComplexity", "PMD.CyclomaticComplexity", "PMD.NPathComplexity" })
-public class ExerciseService {
+public class ExerciseImportService {
 
     private static final String DIFFICULTY = "difficulty";
 
     private static final String TMP_DIR_PROPERTY = "java.io.tmpdir";
 
-    private final Logger log = LoggerFactory.getLogger(ExerciseService.class);
+    private final Logger log = LoggerFactory.getLogger(ExerciseImportService.class);
 
     private final ApplicationProperties applicationProperties;
 
-    private final FileService fileService;
-
     private final GitlabService gitlabService;
 
-    public ExerciseService(ApplicationProperties applicationProperties, FileService fileService, GitlabService gitlabService) {
+    private final ZipService zipService;
+
+    public ExerciseImportService(ApplicationProperties applicationProperties, ZipService zipService, GitlabService gitlabService) {
         this.applicationProperties = applicationProperties;
-        this.fileService = fileService;
+        this.zipService = zipService;
         this.gitlabService = gitlabService;
     }
 
     /**
      * Retrieve a zipped Artemis Exercise from a given URL
      * and store it in a temporary directory
+     *
      * @param exerciseUrl the url of the exposed exercise
      * @throws ArtemisImportError
      */
     @SuppressWarnings("PMD.AvoidCatchingGenericException")
     public void importExerciseFromArtemis(String exerciseUrl) throws ArtemisImportError {
-        String exerciseToken = exerciseUrl.split("/")[exerciseUrl.split("/").length - 1];
-        Client client = ClientBuilder.newClient();
+        String exerciseToken = getTokenFromUrl(exerciseUrl);
 
-        WebTarget target = client.target(exerciseUrl);
-        try (
-            InputStream inputStream = target
-                .request()
-                .header("Authorization", getApiKeyOfRegisteredConnectorByUrl(new URI(exerciseUrl).getAuthority()))
-                .accept(javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM)
-                .get(InputStream.class);
-            ZipInputStream zipInput = new ZipInputStream(inputStream)
-        ) {
-            storeZipInTmp(zipInput, exerciseToken);
+        try (ZipInputStream zipInput = getZipInputStreamFromUrl(exerciseUrl);) {
+            zipService.storeZipInTmp(zipInput, exerciseToken);
         } catch (URISyntaxException e) {
             log.error("Unable to add apiKey to the import request for [{}]. Try to request resource without apiKey.", exerciseUrl);
         } catch (Exception e) {
@@ -92,6 +80,18 @@ public class ExerciseService {
         }
     }
 
+    protected ZipInputStream getZipInputStreamFromUrl(String exerciseUrl) throws URISyntaxException {
+        Client client = ClientBuilder.newClient();
+        WebTarget target = client.target(exerciseUrl);
+
+        InputStream inputStream = target
+            .request()
+            .header("Authorization", getApiKeyOfRegisteredConnectorByUrl(new URI(exerciseUrl).getAuthority()))
+            .accept(javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM)
+            .get(InputStream.class);
+        return new ZipInputStream(inputStream);
+    }
+
     /**
      * Import exercise to Gitlab.
      * Sequentially executes following processes:
@@ -100,11 +100,15 @@ public class ExerciseService {
      * - Locally checking out the new created repository
      * - Copy, commit and push files to the new repository
      *
-     * @param exerciseInfo of the exercise to import
-     * @param exerciseToken of the stored exercise (in the temporary directory) to import
+     * @param exerciseInfo  of the exercise to import
+     * @param exerciseToken of the stored exercise (in the temporary directory) to
+     *                      import
      */
     public void importExercise(ArtemisExerciseInfo exerciseInfo, String exerciseToken, Integer gitlabGroupId)
-        throws GitLabApiException, GitAPIException, IOException, ArtemisImportError {
+        throws GitLabApiException, IOException, ArtemisImportError {
+        sanitizeExerciseInfo(exerciseInfo);
+        validateExerciseInfo(exerciseInfo);
+
         applyExerciseInfoChanges(exerciseInfo, exerciseToken);
         renameFiles(exerciseToken);
 
@@ -122,16 +126,19 @@ public class ExerciseService {
 
     /**
      * Retrieves metadata from imported Artemis exercise
+     *
      * @param exerciseToken of the imported exercise
      * @return Content of exercise's 'metadata.yaml' and 'artemis.yaml'
      */
     public ArtemisExerciseInfo getArtemisExerciseInfo(String exerciseToken) throws IOException {
-        ObjectMapper mapper = new ObjectMapper(new JsonFactory());
-        mapper
-            .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
-            .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
-            .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
-            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+        ObjectMapper mapper = JsonMapper
+            .builder()
+            .configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true)
+            .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true)
+            .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+            .build();
+
         File exerciseDetailsFile = Objects.requireNonNull(
             new File(System.getProperty(TMP_DIR_PROPERTY), exerciseToken)
                 .listFiles((file, name) -> name.startsWith("Exercise-Details-") && name.endsWith(".json"))
@@ -145,7 +152,8 @@ public class ExerciseService {
     }
 
     /**
-     * Utility method used to map Artemis Exercise metadata values to the ones used in
+     * Utility method used to map Artemis Exercise metadata values to the ones used
+     * in
      * Sharing Metadata
      *
      * @param jsonFile to parse the values from
@@ -155,19 +163,14 @@ public class ExerciseService {
         ObjectMapper mapper = new ObjectMapper();
         JSONObject jsonObject = mapper.readValue(jsonFile, JSONObject.class);
         if (jsonObject.containsKey(DIFFICULTY)) {
-            switch (jsonObject.get(DIFFICULTY).toString()) {
-                case "EASY":
-                    jsonObject.put(DIFFICULTY, "SIMPLE");
-                    break;
-                case "MEDIUM":
-                    jsonObject.put(DIFFICULTY, "MEDIUM");
-                    break;
-                case "HARD":
-                    jsonObject.put(DIFFICULTY, "ADVANCED");
-                    break;
-                default: // should never happen
-                    jsonObject.put(DIFFICULTY, "MEDIUM");
-                    break;
+            try {
+                jsonObject.put(DIFFICULTY, ArtemisExerciseInfo.DifficultyMapping.valueOf(jsonObject.get(DIFFICULTY).toString()));
+            } catch (IllegalArgumentException iae) {
+                log.warn(
+                    "Artemis difficulty value '{}' could not be mapped to its equivalent Gitsearch value. Skipping..",
+                    jsonObject.get(DIFFICULTY)
+                );
+                jsonObject.remove(DIFFICULTY);
             }
         }
 
@@ -183,6 +186,10 @@ public class ExerciseService {
             jsonObject.put("format", "artemis");
         }
 
+        if (!jsonObject.containsKey("learningResourceType")) {
+            jsonObject.put("learningResourceType", "artemis");
+        }
+
         if (!jsonObject.containsKey("structure")) {
             jsonObject.put("structure", "atomic");
         }
@@ -203,7 +210,8 @@ public class ExerciseService {
 
     /**
      * Utility method used to extract Artemis metadata into a 'artemis.yaml' file
-     * @param jsonMetadataFile where the exercise metadata is stored
+     *
+     * @param jsonMetadataFile    where the exercise metadata is stored
      * @param artemisExerciseInfo the entity of the stored metadata
      * @throws IOException if an error occurs
      */
@@ -223,118 +231,10 @@ public class ExerciseService {
     }
 
     /**
-     * Used to temporary store content of Artemis' exercises Zip-Files into a tmp-subdirectory
-     *
-     * Recursion is used in case of nested Zip-Files
+     * Utility method used to apply metadata changes to the exercise's
+     * 'metadata.yaml' file
      *
-     * @param zipInputStream containing the Zip-File to unzip
-     * @param subDir name of the tmp-subdirectory
-     */
-    @SuppressWarnings("PMD.AssignmentInOperand")
-    private void storeZipInTmp(ZipInputStream zipInputStream, String subDir) throws IOException {
-        Path targetDir;
-
-        if (subDir.startsWith(System.getProperty(TMP_DIR_PROPERTY))) {
-            targetDir = Files.createDirectories(Paths.get(subDir));
-        } else {
-            targetDir = Files.createDirectories(Paths.get(System.getProperty(TMP_DIR_PROPERTY), subDir));
-        }
-
-        ZipEntry zipEntry;
-
-        while ((zipEntry = zipInputStream.getNextEntry()) != null) {
-            if (zipEntry.getName().contains(".git")) { // important as Artemis exercises come in submodules
-                continue;
-            }
-
-            Path zipEntryPath = Paths.get(targetDir.toString(), zipEntry.getName());
-
-            if (isZipFile(zipEntry)) {
-                writeFile(zipEntryPath.toFile(), zipInputStream);
-                String newSubDir;
-                if (StringUtils.endsWithAny(zipEntry.getName(), new String[] { "-exercise.zip", "-solution.zip", "-tests.zip" })) {
-                    newSubDir = Paths.get(targetDir.getParent().toString(), getRepoNameFromZipEntry(zipEntry)).toString();
-                } else {
-                    newSubDir =
-                        Paths.get(targetDir.toString(), zipEntry.getName().substring(0, zipEntry.getName().lastIndexOf("."))).toString();
-                }
-
-                storeZipInTmp(new ZipInputStream(new FileInputStream(zipEntryPath.toFile())), newSubDir);
-                if (Files.deleteIfExists(zipEntryPath)) {
-                    log.warn("Unable to delete temporary Zip file [{}] after extraction. Skipping..", zipEntryPath);
-                }
-            } else if (zipEntry.isDirectory()) {
-                Files.createDirectory(zipEntryPath);
-            } else {
-                writeFile(zipEntryPath.toFile(), zipInputStream);
-            }
-        }
-        cleanUpTmpDirectory(targetDir.toFile());
-        zipInputStream.close();
-    }
-
-    /**
-     * Utility method used to check if a ZipEntry represents a Zip file
-     * @param zipEntry to check
-     * @return true if zipEntry is a Zip file, false otherwise
-     */
-    private boolean isZipFile(ZipEntry zipEntry) {
-        return zipEntry.getName().endsWith(".zip");
-    }
-
-    /**
-     * Cleans up a temporary directory by removing it if empty
-     * or scheduling its deletion with a delay of 60 minutes, otherwise.
-     *
-     * @param tmpDir to clean up
-     */
-    private void cleanUpTmpDirectory(File tmpDir) {
-        try {
-            if (tmpDir.exists() && tmpDir.isDirectory() && fileService.isEmpty(tmpDir.toPath())) {
-                Files.delete(tmpDir.toPath());
-            }
-            fileService.scheduleForDeletion(tmpDir.toPath(), 60);
-        } catch (IOException e) {
-            log.error("An error occurred while cleaning up a temporary directory: ", e);
-        }
-    }
-
-    /**
-     * Utility method used to extract the repository name from Zip files
-     * representing git submodules. (Artemis exercise exports come in
-     * submodules for 'exercise', 'solution' and 'test')
-     * @param zipEntry to get the repository name from
-     * @return the repository name
-     */
-    private String getRepoNameFromZipEntry(ZipEntry zipEntry) {
-        return zipEntry.getName().substring(zipEntry.getName().lastIndexOf("-") + 1, zipEntry.getName().lastIndexOf("."));
-    }
-
-    /**
-     * Method used to write files from a zipInputStream
-     * @param entryFile File to write
-     * @param zipInputStream Byte stream to read from
-     * @throws IOException
-     */
-    @SuppressWarnings("PMD.AssignmentInOperand")
-    private void writeFile(File entryFile, ZipInputStream zipInputStream) throws IOException {
-        byte[] buffer = new byte[1024];
-
-        if (!entryFile.getParentFile().exists()) {
-            Files.createDirectories(entryFile.getParentFile().toPath());
-        }
-
-        try (FileOutputStream fos = new FileOutputStream(entryFile)) {
-            int len;
-            while ((len = zipInputStream.read(buffer)) > 0) {
-                fos.write(buffer, 0, len);
-            }
-        }
-    }
-
-    /**
-     * Utility method used to apply metadata changes to the exercise's 'metadata.yaml' file
-     * @param exerciseInfo containing the metadata values
+     * @param exerciseInfo  containing the metadata values
      * @param exerciseToken of the exercise to update
      * @throws IOException
      */
@@ -351,13 +251,14 @@ public class ExerciseService {
     /**
      * Used to validate apiKeys by checking if they match with
      * one of the registeredConnectors' keys stored in application-(dev|prod).yml
+     *
      * @param apiKey the apikey to validate
      * @return true if equal to the local one, false otherwise
      */
     public boolean validate(String apiKey) {
         for (ApplicationProperties.RegisteredConnector registeredConnector : applicationProperties.getRegisteredConnectors()) {
             if (registeredConnector.getAccessToken().equals(apiKey)) {
-                log.debug("ApiKey validated correctly against [" + registeredConnector.getUrl() + "]");
+                log.debug("ApiKey validated correctly against [{} ]", registeredConnector.getUrl());
                 return true;
             }
         }
@@ -365,8 +266,10 @@ public class ExerciseService {
     }
 
     /**
-     * Method that retrieves the ApiKey of a Registered Connector configured in 'application-dev/prod.yml'
+     * Method that retrieves the ApiKey of a Registered Connector configured in
+     * 'application-dev/prod.yml'
      * given the URL of the connector
+     *
      * @param urlAuthority URL Authority/Domain of the connector
      * @return the ApiKey, or Null if no connector with the given url is found
      */
@@ -409,4 +312,85 @@ public class ExerciseService {
             log.error("Error while renaming file: {}", ioe.getMessage());
         }
     }
+
+    /**
+     * Sanitize the provided input fields for the exercise information, by escaping
+     * all potential tags' brackets
+     *
+     * @param exerciseInfo of the imported exercise
+     */
+    private void sanitizeExerciseInfo(ArtemisExerciseInfo exerciseInfo) {
+        exerciseInfo.setMetadataVersion(exerciseInfo.getMetadataVersion());
+        exerciseInfo.setTitle(replaceTagCharacters(exerciseInfo.getTitle()));
+        exerciseInfo.setDescription(replaceTagCharacters(exerciseInfo.getDescription()));
+        if (exerciseInfo.getKeyword() != null) {
+            exerciseInfo.setKeyword(exerciseInfo.getKeyword().stream().map(this::replaceTagCharacters).collect(Collectors.toList()));
+        }
+        if (exerciseInfo.getCreator() != null) {
+            exerciseInfo.setCreator(exerciseInfo.getCreator().stream().map(creator -> creator.sanitize()).collect(Collectors.toList()));
+        }
+        if (exerciseInfo.getPublisher() != null) {
+            exerciseInfo.setPublisher(
+                exerciseInfo.getPublisher().stream().map(publisher -> publisher.sanitize()).collect(Collectors.toList())
+            );
+        }
+        exerciseInfo.setLicense(replaceTagCharacters(exerciseInfo.getLicense()));
+        if (exerciseInfo.getLanguage() != null) {
+            exerciseInfo.setLanguage(exerciseInfo.getLanguage().stream().map(this::replaceTagCharacters).collect(Collectors.toList()));
+        }
+        if (exerciseInfo.getFormat() != null) {
+            exerciseInfo.setFormat(exerciseInfo.getFormat().stream().map(this::replaceTagCharacters).collect(Collectors.toList()));
+        }
+        exerciseInfo.setLearningResourceType(replaceTagCharacters(exerciseInfo.getLearningResourceType()));
+    }
+
+    /**
+     * Method used to validate the exercise information of the imported exercise
+     *
+     * @param exerciseInfo of the imported exercise
+     * @throws ArtemisImportError if the exercise's information is invalid or
+     *                            incomplete
+     */
+    public void validateExerciseInfo(ArtemisExerciseInfo exerciseInfo) throws ArtemisImportError {
+        if (!exerciseInfo.validateMandatoryFields()) { // Check mandatory fields
+            throw new ArtemisImportError("exercise.import.error.invalidMetadata");
+        }
+        for (String keyword : exerciseInfo.getKeyword()) {
+            if (keyword.isBlank()) {
+                throw new ArtemisImportError("exercise.import.error.invalidKeyword");
+            }
+        }
+        for (String programmingLanguage : exerciseInfo.getProgrammingLanguage()) {
+            if (programmingLanguage.isBlank()) {
+                throw new ArtemisImportError("exercise.import.error.invalidProgrammingLanguage");
+            }
+        }
+        for (MetadataUserDTO creator : exerciseInfo.getCreator()) {
+            if (!creator.isValid()) {
+                throw new ArtemisImportError("exercise.import.error.invalidCreator");
+            }
+        }
+        for (MetadataUserDTO publisher : exerciseInfo.getPublisher()) {
+            if (!publisher.isValid()) {
+                throw new ArtemisImportError("exercise.import.error.invalidPublisher");
+            }
+        }
+    }
+
+    /**
+     * Method responsible for extracting an exercise's token from
+     * its Url.
+     * @param exerciseUrl to extract the token from
+     * @return The exercise's token
+     */
+    public String getTokenFromUrl(String exerciseUrl) {
+        return exerciseUrl.split("/")[exerciseUrl.split("/").length - 1];
+    }
+
+    private String replaceTagCharacters(String input) {
+        if (input != null) {
+            return input.replace("<", "&lt").replace(">", "&gt");
+        }
+        return null;
+    }
 }
diff --git a/src/main/java/at/ac/uibk/gitsearch/service/ZipService.java b/src/main/java/at/ac/uibk/gitsearch/service/ZipService.java
new file mode 100644
index 0000000000000000000000000000000000000000..d88f6093c53fb6a947d77f1134b3b5e46511cd38
--- /dev/null
+++ b/src/main/java/at/ac/uibk/gitsearch/service/ZipService.java
@@ -0,0 +1,169 @@
+package at.ac.uibk.gitsearch.service;
+
+import at.ac.uibk.gitsearch.es.model.ArtemisExerciseInfo;
+import at.ac.uibk.gitsearch.properties.ApplicationProperties;
+import at.ac.uibk.gitsearch.service.dto.MetadataUserDTO;
+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 com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import net.minidev.json.JSONObject;
+import net.minidev.json.JSONStyle;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.jgit.lib.Repository;
+import org.gitlab4j.api.GitLabApiException;
+import org.gitlab4j.api.models.Group;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ZipService {
+
+    private static final String TMP_DIR_PROPERTY = "java.io.tmpdir";
+
+    private final Logger log = LoggerFactory.getLogger(ExerciseImportService.class);
+
+    private final FileService fileService;
+
+    public ZipService(FileService fileService) {
+        this.fileService = fileService;
+    }
+
+    /**
+     * Used to temporary store content of Artemis' exercises Zip-Files into a
+     * tmp-subdirectory
+     *
+     * Recursion is used in case of nested Zip-Files
+     *
+     * @param zipInputStream containing the Zip-File to unzip
+     * @param subDir         name of the tmp-subdirectory
+     */
+    @SuppressWarnings({ "PMD.AssignmentInOperand", "PMD.CognitiveComplexity" })
+    public void storeZipInTmp(ZipInputStream zipInputStream, String subDir) throws IOException {
+        Path targetDir;
+
+        if (subDir.startsWith(System.getProperty(TMP_DIR_PROPERTY))) {
+            targetDir = Files.createDirectories(Paths.get(subDir));
+        } else {
+            targetDir = Files.createDirectories(Paths.get(System.getProperty(TMP_DIR_PROPERTY), subDir));
+        }
+
+        ZipEntry zipEntry;
+
+        while ((zipEntry = zipInputStream.getNextEntry()) != null) {
+            if (zipEntry.getName().contains(".git")) { // important as Artemis exercises come in submodules
+                continue;
+            }
+
+            Path zipEntryPath = Paths.get(targetDir.toString(), zipEntry.getName());
+
+            if (isZipFile(zipEntry)) {
+                writeFile(zipEntryPath.toFile(), zipInputStream);
+                String newSubDir;
+                if (StringUtils.endsWithAny(zipEntry.getName(), new String[] { "-exercise.zip", "-solution.zip", "-tests.zip" })) {
+                    newSubDir = Paths.get(targetDir.getParent().toString(), getRepoNameFromZipEntry(zipEntry)).toString();
+                } else {
+                    newSubDir =
+                        Paths.get(targetDir.toString(), zipEntry.getName().substring(0, zipEntry.getName().lastIndexOf("."))).toString();
+                }
+
+                storeZipInTmp(new ZipInputStream(new FileInputStream(zipEntryPath.toFile())), newSubDir);
+                if (Files.deleteIfExists(zipEntryPath)) {
+                    log.warn("Unable to delete temporary Zip file [{}] after extraction. Skipping..", zipEntryPath);
+                }
+            } else if (zipEntry.isDirectory()) {
+                Files.createDirectory(zipEntryPath);
+            } else {
+                writeFile(zipEntryPath.toFile(), zipInputStream);
+            }
+        }
+        cleanUpTmpDirectory(targetDir.toFile());
+        zipInputStream.close();
+    }
+
+    /**
+     * Utility method used to check if a ZipEntry represents a Zip file
+     *
+     * @param zipEntry to check
+     * @return true if zipEntry is a Zip file, false otherwise
+     */
+    private boolean isZipFile(ZipEntry zipEntry) {
+        return zipEntry.getName().endsWith(".zip");
+    }
+
+    /**
+     * Cleans up a temporary directory by removing it if empty
+     * or scheduling its deletion with a delay of 60 minutes, otherwise.
+     *
+     * @param tmpDir to clean up
+     */
+    private void cleanUpTmpDirectory(File tmpDir) {
+        try {
+            if (tmpDir.exists() && tmpDir.isDirectory() && fileService.isEmpty(tmpDir.toPath())) {
+                Files.delete(tmpDir.toPath());
+            }
+            fileService.scheduleForDeletion(tmpDir.toPath(), 60);
+        } catch (IOException e) {
+            log.error("An error occurred while cleaning up a temporary directory: ", e);
+        }
+    }
+
+    /**
+     * Utility method used to extract the repository name from Zip files
+     * representing git submodules. (Artemis exercise exports come in
+     * submodules for 'exercise', 'solution' and 'test')
+     *
+     * @param zipEntry to get the repository name from
+     * @return the repository name
+     */
+    private String getRepoNameFromZipEntry(ZipEntry zipEntry) {
+        return zipEntry.getName().substring(zipEntry.getName().lastIndexOf("-") + 1, zipEntry.getName().lastIndexOf("."));
+    }
+
+    /**
+     * Method used to write files from a zipInputStream
+     *
+     * @param entryFile      File to write
+     * @param zipInputStream Byte stream to read from
+     * @throws IOException
+     */
+    @SuppressWarnings("PMD.AssignmentInOperand")
+    private void writeFile(File entryFile, ZipInputStream zipInputStream) throws IOException {
+        byte[] buffer = new byte[1024];
+
+        if (!entryFile.getParentFile().exists()) {
+            Files.createDirectories(entryFile.getParentFile().toPath());
+        }
+
+        try (FileOutputStream fos = new FileOutputStream(entryFile)) {
+            int len;
+            while ((len = zipInputStream.read(buffer)) > 0) {
+                fos.write(buffer, 0, len);
+            }
+        }
+    }
+}
diff --git a/src/main/java/at/ac/uibk/gitsearch/service/dto/MetadataUserDTO.java b/src/main/java/at/ac/uibk/gitsearch/service/dto/MetadataUserDTO.java
index fcf92dd2b4f4e92a2a34adf21dd8f50c65a13e80..d9d57086c171b030ad5a91a909bd11b16d3c0f70 100644
--- a/src/main/java/at/ac/uibk/gitsearch/service/dto/MetadataUserDTO.java
+++ b/src/main/java/at/ac/uibk/gitsearch/service/dto/MetadataUserDTO.java
@@ -2,6 +2,7 @@ package at.ac.uibk.gitsearch.service.dto;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import java.util.Objects;
+import java.util.regex.Pattern;
 import javax.validation.constraints.Email;
 import javax.validation.constraints.Size;
 
@@ -59,4 +60,33 @@ public class MetadataUserDTO {
     public String toString() {
         return "MetadataUserDTO{" + "name='" + name + '\'' + ", affiliation='" + affiliation + '\'' + ", email='" + email + '\'' + '}';
     }
+
+    public boolean isValid() {
+        String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$";
+
+        Pattern emailPattern = Pattern.compile(emailRegex);
+
+        return (
+            !this.getName().isBlank() &&
+            !this.getAffiliation().isBlank() &&
+            !this.getEmail().isBlank() &&
+            emailPattern.matcher(this.getEmail()).matches()
+        );
+    }
+
+    /**
+     * Basic sanitization method replacing brackets with its corresponding escaped symbol
+     *
+     * @return the sanitized MetadataUserDTO
+     */
+    public MetadataUserDTO sanitize() {
+        this.affiliation = replaceTagCharacters(this.affiliation);
+        this.name = replaceTagCharacters(this.name);
+        this.email = replaceTagCharacters(this.email);
+        return this;
+    }
+
+    private String replaceTagCharacters(String input) {
+        return input.replace("<", "&lt").replace(">", "&gt");
+    }
 }
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 c9df71446d10e5e23d75ea434407ac71429e922e..7c55d34a47b011800955cda2ea9e83a7d6e90caf 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
@@ -3,7 +3,7 @@ package at.ac.uibk.gitsearch.web.rest;
 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.ExerciseService;
+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.SearchService.ExtractionDepth;
@@ -74,7 +74,7 @@ public class ExerciseResource {
 
     @Autowired
     @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" })
-    private ExerciseService exerciseService;
+    private ExerciseImportService exerciseImportService;
 
     @Autowired
     @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" })
@@ -335,16 +335,14 @@ public class ExerciseResource {
         @RequestParam("apiKey") Optional<String> apiKey,
         HttpServletRequest request
     ) throws ArtemisImportError, MalformedURLException, URISyntaxException {
-        if (apiKey.isPresent() && exerciseService.validate(apiKey.get())) {
-            exerciseService.importExerciseFromArtemis(exerciseUrl);
+        if (apiKey.isPresent() && exerciseImportService.validate(apiKey.get())) {
+            exerciseImportService.importExerciseFromArtemis(exerciseUrl);
         } else {
             return ResponseEntity.status(401).body(null);
         }
 
         String baseUrl = getBaseUrl(request);
-        String exerciseToken = exerciseUrl.split("/")[exerciseUrl.split("/").length - 1];
-
-        return ResponseEntity.ok(new URL(baseUrl + "/import/" + exerciseToken).toURI());
+        return ResponseEntity.ok(new URL(baseUrl + "/import/" + exerciseImportService.getTokenFromUrl(exerciseUrl)).toURI());
     }
 
     /**
@@ -360,7 +358,7 @@ public class ExerciseResource {
         throws IOException {
         if (SecurityUtils.isAuthenticated()) {
             try {
-                return ResponseEntity.ok(exerciseService.getArtemisExerciseInfo(exerciseToken));
+                return ResponseEntity.ok(exerciseImportService.getArtemisExerciseInfo(exerciseToken));
             } catch (NullPointerException npe) {
                 return ResponseEntity.notFound().build();
             }
@@ -387,13 +385,23 @@ public class ExerciseResource {
         @PathVariable("gitlabGroupId") Integer gitlabGroupId
     ) throws GitLabApiException, GitAPIException, IOException, ArtemisImportError {
         if (SecurityUtils.isAuthenticated()) {
-            exerciseService.importExercise(exerciseInfo, exerciseToken, gitlabGroupId);
+            exerciseImportService.importExercise(exerciseInfo, exerciseToken, gitlabGroupId);
             return Response.ok().build();
         } else {
             return Response.status(401).build();
         }
     }
 
+    @PostMapping("/exercise/validate-exercise-info")
+    public ResponseEntity<String> validateExerciseInfo(@RequestBody ArtemisExerciseInfo exerciseInfo) {
+        try {
+            exerciseImportService.validateExerciseInfo(exerciseInfo);
+            return ResponseEntity.ok().build();
+        } catch (ArtemisImportError e) {
+            return ResponseEntity.badRequest().body(e.getMessage());
+        }
+    }
+
     /**
      * 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/webapp/app/exercise/import/exercise-import.component.html b/src/main/webapp/app/exercise/import/exercise-import.component.html
index 2442f92f246d436b1640dbad621b688632a809e3..b25d71086bd213855402b3d5ff8e578932f83533 100644
--- a/src/main/webapp/app/exercise/import/exercise-import.component.html
+++ b/src/main/webapp/app/exercise/import/exercise-import.component.html
@@ -2,6 +2,19 @@
 <jhi-alert-error></jhi-alert-error>
 <h2 jhiTranslate="exercise.import.pageTitle">Import exercise</h2>
 
+<div *ngIf="!exerciseInfo && errorStatus">
+  <div class="w-75 container-fluid align-middle">
+    <div *ngIf="errorStatus" class="row justify-content-center text-center">
+      <span class="display-1 mb-5">{{ errorStatus }}</span>
+    </div>
+    <div class="row justify-content-center text-center">
+      <span class="display-3 mb-5" [jhiTranslate]="occurredError">An error occurred</span>
+    </div>
+    <div class="row justify-content-center text-center">
+      <span class="display-4" jhiTranslate="exercise.import.error.retry">Please retry to import the exercise</span>
+    </div>
+  </div>
+</div>
 <div *ngIf="exerciseInfo != undefined">
   <div class="row justify-content-center">
     <div class="col-8">
diff --git a/src/main/webapp/app/exercise/import/exercise-import.component.spec.ts b/src/main/webapp/app/exercise/import/exercise-import.component.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d4c5f7f9f717dd98c1343326ccf52f49a975ecab
--- /dev/null
+++ b/src/main/webapp/app/exercise/import/exercise-import.component.spec.ts
@@ -0,0 +1,266 @@
+import { HttpClient, HttpHandler } from '@angular/common/http';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ActivatedRoute } from '@angular/router';
+import { ExerciseImportComponent } from 'app/exercise/import/exercise-import.component';
+import { ExerciseService } from 'app/exercise/service/exercise.service';
+import { ArtemisExerciseInfo } from 'app/shared/model/artemis-exercise-info.model';
+import { IInteractivityType } from 'app/shared/model/exercise.model';
+import { GitlabGroup, IVisibility } from 'app/shared/model/gitlab-group.model';
+import { SearchResultDTO } from 'app/shared/model/search/search-result-dto.model';
+import { of, throwError } from 'rxjs';
+import { TranslateModule } from '@ngx-translate/core';
+
+describe('Exercise Import Component', () => {
+  let comp: ExerciseImportComponent;
+  let fixture: ComponentFixture<ExerciseImportComponent>;
+  let service: ExerciseService;
+
+  const exerciseInfoTemplate = new ArtemisExerciseInfo(
+    [],
+    '',
+    [],
+    [],
+    false,
+    '',
+    0,
+    '',
+    '',
+    [],
+    [],
+    [],
+    '',
+    '',
+    '',
+    {} as IInteractivityType,
+    [],
+    [],
+    [],
+    new Date(),
+    '',
+    '',
+    '',
+    {} as SearchResultDTO,
+    ['Java'],
+    '',
+    [],
+    0,
+    [],
+    '',
+    '',
+    [],
+    [],
+    '',
+    'testExTitle',
+    '',
+    '',
+    0,
+    0,
+    0,
+    false
+  );
+
+  const group: GitlabGroup = {
+    id: 1,
+    name: 'testGroup',
+    path: 'testPath',
+    description: '',
+    visibility: IVisibility.PUBLIC,
+    avatarUrl: '',
+    webUrl: '',
+    fullName: '',
+    fullPath: '',
+    parentId: 0,
+  };
+
+  const newPerson = { name: 'newPerson', email: 'person@email', affiliation: 'newAffiliation' };
+  const newPersonCopy = { name: 'newPerson', email: 'person@email', affiliation: 'newAffiliation' };
+  const personToRemove = { name: 'personToRemove', email: 'person@toremove', affiliation: '' };
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({
+      imports: [TranslateModule.forRoot()],
+      declarations: [ExerciseImportComponent],
+      providers: [
+        {
+          provide: ActivatedRoute,
+          useValue: {
+            snapshot: {
+              params: {
+                token: 'exercise-test-token',
+              },
+              queryParams: {
+                callback: 'https://callback-url.test',
+              },
+            },
+          },
+        },
+        ExerciseImportComponent,
+        ExerciseService,
+        HttpClient,
+        HttpHandler,
+      ],
+    })
+      .overrideTemplate(ExerciseImportComponent, '')
+      .compileComponents();
+
+    fixture = TestBed.createComponent(ExerciseImportComponent);
+    comp = fixture.componentInstance;
+    service = TestBed.inject(ExerciseService);
+  });
+
+  describe('init', () => {
+    it('Should load on init', () => {
+      // GIVEN
+      jest.spyOn(service, 'importExerciseInfoFromArtemis').mockImplementation(() => of(exerciseInfoTemplate));
+
+      // WHEN
+      comp.ngOnInit();
+
+      // THEN
+      expect(comp.exerciseInfo?.title).toEqual('testExTitle');
+      expect(comp.exerciseInfo?.programmingLanguage[0]).toEqual('Java');
+    });
+  });
+
+  describe('map metadata', () => {
+    it('Should map metadata values when importing', () => {
+      // GIVEN
+      comp.exerciseToken = 'TestToken';
+      comp.exerciseInfo = exerciseInfoTemplate;
+      comp.exerciseInfo.difficulty = 'ADVANCED';
+      comp.isPublisherSameAsCreator = true;
+
+      jest.spyOn(comp, 'openGitlabPathSelectorModal').mockImplementation(
+        () =>
+          new Promise(resolve => {
+            resolve(group);
+          })
+      );
+      jest.spyOn(service, 'submitExerciseInfoForImport').mockImplementation(() => of({} as ArtemisExerciseInfo));
+
+      // WHEN
+      comp.import();
+
+      // THEN
+      expect(comp.exerciseInfo.difficulty).toEqual(comp.exerciseInfo.difficulty.toLowerCase());
+      expect(comp.exerciseInfo.publisher).toEqual(comp.exerciseInfo.creator);
+    });
+  });
+
+  describe('add and remove creators', () => {
+    it('Should add and remove creators from list', () => {
+      // GIVEN
+      comp.exerciseInfo = exerciseInfoTemplate;
+
+      // WHEN
+      comp.addPerson(newPerson, 'creator');
+      comp.addPerson(personToRemove, 'creator');
+      comp.removePerson(personToRemove, 'creator');
+
+      // THEN
+      expect(comp.exerciseInfo?.creator).toEqual([newPersonCopy]);
+    });
+  });
+
+  describe('add and remove publishers', () => {
+    it('Should add and remove publishers from list', () => {
+      // GIVEN
+      comp.exerciseInfo = exerciseInfoTemplate;
+
+      // WHEN
+      comp.addPerson(newPerson, 'publisher');
+      comp.addPerson(personToRemove, 'publisher');
+      comp.removePerson(personToRemove, 'publisher');
+
+      // THEN
+      expect(comp.exerciseInfo?.publisher).toEqual([newPersonCopy]);
+    });
+  });
+
+  describe('validate button groups', () => {
+    it('Should validate form button groups', () => {
+      // GIVEN
+      comp.exerciseInfo = exerciseInfoTemplate;
+      comp.exerciseInfo.keyword = [];
+      comp.exerciseInfo.language = [];
+      const resultWithErrors = comp.hasValidationErrors();
+
+      comp.exerciseInfo.keyword.push('keyword');
+      comp.setLanguage('english');
+      const resultWithoutErrors = comp.hasValidationErrors();
+
+      // THEN
+      expect(resultWithErrors).toEqual(true);
+      expect(resultWithoutErrors).toEqual(false);
+    });
+  });
+
+  describe('display import errors', () => {
+    it('should set unauthorized error', () => {
+      // GIVEN
+      const error = {
+        status: 401,
+        message: 'Unauthorized',
+      };
+
+      jest.spyOn(service, 'importExerciseInfoFromArtemis').mockReturnValue(throwError(() => error));
+
+      // WHEN
+      comp.ngOnInit();
+
+      // THEN
+      expect(comp.errorStatus).toBeDefined();
+      expect(comp.occurredError).toEqual('exercise.import.error.unauthorized');
+    });
+
+    it('should set not found error', () => {
+      // GIVEN
+      const error = {
+        status: 404,
+        message: 'Not Found',
+      };
+
+      jest.spyOn(service, 'importExerciseInfoFromArtemis').mockReturnValue(throwError(() => error));
+
+      // WHEN
+      comp.ngOnInit();
+
+      // THEN
+      expect(comp.errorStatus).toBeDefined();
+      expect(comp.occurredError).toEqual('exercise.import.error.notFound');
+    });
+
+    it('should set error', () => {
+      // GIVEN
+      const error = {
+        status: 500,
+        message: 'Internal Server Error',
+      };
+
+      jest.spyOn(service, 'importExerciseInfoFromArtemis').mockReturnValue(throwError(() => error));
+
+      // WHEN
+      comp.ngOnInit();
+
+      // THEN
+      expect(comp.errorStatus).toBeDefined();
+      expect(comp.occurredError).toEqual('exercise.import.error.loadingError');
+    });
+  });
+
+  describe('Validate metadata on import', () => {
+    it('Should validate metadata on import', () => {
+      // GIVEN
+      comp.exerciseInfo = exerciseInfoTemplate;
+      comp.exerciseInfo.keyword = [];
+      comp.exerciseInfo.language = [];
+      jest.spyOn(service, 'validateExerciseInfo').mockReturnValue(of('0'));
+
+      // WHEN
+      comp.import();
+
+      // THEN
+      expect(service.validateExerciseInfo).toHaveBeenCalled();
+    });
+  });
+});
diff --git a/src/main/webapp/app/exercise/import/exercise-import.component.ts b/src/main/webapp/app/exercise/import/exercise-import.component.ts
index 22207a08b889713086d3fa14af161a9aea770ffb..5e176d17e9e2aefa723a75ca427ce7dd38929b3b 100644
--- a/src/main/webapp/app/exercise/import/exercise-import.component.ts
+++ b/src/main/webapp/app/exercise/import/exercise-import.component.ts
@@ -10,6 +10,7 @@ import { GitlabPathSelectorComponent } from 'app/shared/gitlab/gitlab-path-selec
 import { GitlabGroup } from 'app/shared/model/gitlab-group.model';
 import { COMMA, ENTER, TAB } from '@angular/cdk/keycodes';
 import { MatChipInputEvent } from '@angular/material/chips';
+import { firstValueFrom } from 'rxjs';
 
 @Component({
   selector: 'jhi-import',
@@ -31,6 +32,9 @@ export class ExerciseImportComponent implements OnInit {
   isLoading = false;
   isImporting = false;
 
+  errorStatus: number | undefined;
+  occurredError = 'exercise.import.error.loadingError';
+
   separatorKeysCodes = [ENTER, COMMA, TAB];
 
   constructor(
@@ -45,12 +49,12 @@ export class ExerciseImportComponent implements OnInit {
     this.isLoading = true;
     this.exerciseToken = this.route.snapshot.params['token'];
     if (this.exerciseToken) {
-      this.exerciseService.importExerciseInfoFromArtemis(this.exerciseToken).subscribe(
-        data => {
+      this.exerciseService.importExerciseInfoFromArtemis(this.exerciseToken).subscribe({
+        next: data => {
           this.exerciseInfo = data;
           this.exerciseInfo.programmingLanguage = data['programmingLanguage'];
           this.exerciseInfo.programmingLanguage = this.exerciseInfo.programmingLanguage.map(programmingLanguage =>
-            this.toTitleCase(programmingLanguage)
+            ExerciseImportComponent.toTitleCase(programmingLanguage)
           );
           this.exerciseInfo.creator = data['creator'];
           if (this.exerciseInfo.language === undefined || this.exerciseInfo.language === null || this.exerciseInfo.language.length === 0) {
@@ -68,10 +72,17 @@ export class ExerciseImportComponent implements OnInit {
             this.exerciseInfo.keyword = ['artemis'];
           }
         },
-        error => {
-          this.jhiAlertService.addAlert({ type: 'danger', message: 'Following error occurred: '.concat(error.error) });
-        }
-      );
+        error: error => {
+          this.errorStatus = error.status;
+          if (error.status == 401) {
+            this.occurredError = 'exercise.import.error.unauthorized';
+          } else if (error.status == 404) {
+            this.occurredError = 'exercise.import.error.notFound';
+          } else {
+            this.occurredError = 'exercise.import.error.loadingError';
+          }
+        },
+      });
     }
     this.isLoading = false;
   }
@@ -80,33 +91,33 @@ export class ExerciseImportComponent implements OnInit {
    * Actual import function submitting import form to server
    */
   import(): void {
-    if (this.exerciseInfo!.difficulty !== null) {
-      this.exerciseInfo!.difficulty = this.exerciseInfo!.difficulty?.toLowerCase();
-    }
-    if (this.isPublisherSameAsCreator) {
-      if (this.exerciseInfo) {
-        this.exerciseInfo.publisher = this.exerciseInfo?.creator;
-      }
-    }
-    this.openGitlabPathSelectorModal().then(gitlabGroup => {
-      if (gitlabGroup !== undefined) {
-        this.isLoading = true;
-        this.isImporting = true;
-        this.exerciseService.submitExerciseInfoForImport(this.exerciseInfo!, this.exerciseToken!, gitlabGroup.id).subscribe(
-          () => {
-            window.location.href = this.route.snapshot.queryParams['callback'];
-          },
-          error => {
-            this.jhiAlertService.addAlert({ type: 'danger', message: error.error.detail });
-            window.scroll(0, 0);
-            this.isLoading = false;
-            this.isImporting = false;
+    this.isLoading = true;
+    this.applyPreprocessingStep();
+    this.validateMetadata().then(res => {
+      if (res && res.length > 0) {
+        window.scroll({ top: 0, behavior: 'smooth' });
+        this.isLoading = false;
+      } else {
+        this.isLoading = false;
+        this.openGitlabPathSelectorModal().then(gitlabGroup => {
+          if (gitlabGroup !== undefined) {
+            this.isLoading = true;
+            this.isImporting = true;
+            this.exerciseService.submitExerciseInfoForImport(this.exerciseInfo!, this.exerciseToken!, gitlabGroup.id).subscribe({
+              next: () => {
+                window.location.href = this.route.snapshot.queryParams['callback'];
+              },
+              error: err => {
+                this.jhiAlertService.addAlert({ type: 'danger', message: err.error.detail });
+                window.scroll(0, 0);
+                this.isLoading = false;
+                this.isImporting = false;
+              },
+            });
           }
-        );
+        });
       }
     });
-    this.isLoading = false;
-    this.isImporting = false;
   }
 
   /**
@@ -126,18 +137,15 @@ export class ExerciseImportComponent implements OnInit {
    * @return true if invalid and false if valid
    */
   hasValidationErrors(): boolean {
-    if (
+    return (
       this.exerciseInfo?.keyword === undefined ||
       this.exerciseInfo?.language === undefined ||
       this.exerciseInfo.creator === undefined ||
-      !(this.exerciseInfo.keyword.length > 0) ||
-      !(this.exerciseInfo.language.length > 0) ||
-      !(this.exerciseInfo.creator.length > 0) ||
-      (!this.isPublisherSameAsCreator && (this.exerciseInfo.publisher === undefined || !(this.exerciseInfo.publisher.length > 0)))
-    ) {
-      return true;
-    }
-    return false;
+      this.exerciseInfo.keyword.length <= 0 ||
+      this.exerciseInfo.language.length <= 0 ||
+      this.exerciseInfo.creator.length <= 0 ||
+      (!this.isPublisherSameAsCreator && (this.exerciseInfo.publisher === undefined || this.exerciseInfo.publisher.length <= 0))
+    );
   }
 
   /**
@@ -167,7 +175,7 @@ export class ExerciseImportComponent implements OnInit {
    * @param personList to add the given person to (ex. 'creators', 'publishers')
    */
   addPerson(person: Person, personList: string): void {
-    if (this.validatePerson(person) && this.exerciseInfo) {
+    if (ExerciseImportComponent.validatePerson(person) && this.exerciseInfo) {
       if (!personList.localeCompare('creator')) {
         if (this.exerciseInfo.creator == null) {
           this.exerciseInfo.creator = [];
@@ -206,22 +214,50 @@ export class ExerciseImportComponent implements OnInit {
    * @param person to check
    * @private
    */
-  private validatePerson(person: Person): boolean {
+  private static validatePerson(person: Person): boolean {
     if (person.name === undefined || person.affiliation === undefined || person.email === undefined) {
       return false;
     }
-    if (person.name === '' || person.affiliation === '' || person.email === '') {
-      return false;
+    return !(person.name === '' || person.affiliation === '' || person.email === '');
+  }
+
+  /**
+   * Validates the provided metadata on server side.
+   * Returns a string representing the specific error,
+   * or and empty string in case of no error
+   * @private
+   */
+  private validateMetadata(): Promise<string> {
+    if (!this.exerciseInfo) {
+      return new Promise<string>(() => 'exercise.import.error.invalidMetadata');
+    }
+    return firstValueFrom(this.exerciseService.validateExerciseInfo(this.exerciseInfo)).catch(err => {
+      return err.error as string;
+    });
+  }
+
+  /**
+   * Function applying default preprocessing changes to the user provided
+   * metadata fields
+   * @private
+   */
+  private applyPreprocessingStep() {
+    if (this.exerciseInfo!.difficulty !== null) {
+      this.exerciseInfo!.difficulty = this.exerciseInfo!.difficulty?.toLowerCase();
+    }
+    if (this.isPublisherSameAsCreator) {
+      if (this.exerciseInfo) {
+        this.exerciseInfo.publisher = this.exerciseInfo?.creator;
+      }
     }
-    return true;
   }
 
   /**
    * Utility function used to turn words into title case
    * @param word the word to turn into title case
    */
-  private toTitleCase(word: string): string {
-    return word[0].toUpperCase() + word.substr(1).toLowerCase();
+  private static toTitleCase(word: string): string {
+    return word[0].toUpperCase() + word.substring(1).toLowerCase();
   }
 
   /**
diff --git a/src/main/webapp/app/exercise/service/exercise.service.ts b/src/main/webapp/app/exercise/service/exercise.service.ts
index 34da3665aefdb9952516f36027d03004bbc3b47c..9a264eceb9689babfb86cb657462b345ed6f7a71 100644
--- a/src/main/webapp/app/exercise/service/exercise.service.ts
+++ b/src/main/webapp/app/exercise/service/exercise.service.ts
@@ -96,6 +96,14 @@ export class ExerciseService {
   ): Observable<ArtemisExerciseInfo> {
     return this.http.post<ArtemisExerciseInfo>(`${this.exerciseUrl}import-exercise/${exerciseToken}/${gitlabGroupId}`, exerciseInfo);
   }
+
+  /**
+   * POST request validating the ExerciseInfo provided on the server side
+   * @param exerciseInfo to validate
+   */
+  public validateExerciseInfo(exerciseInfo: ArtemisExerciseInfo): Observable<string> {
+    return this.http.post<string>(`${this.exerciseUrl}validate-exercise-info`, exerciseInfo);
+  }
 }
 
 /*
diff --git a/src/main/webapp/app/shared/gitlab/gitlab-path-selector/gitlab-path-selector.component.html b/src/main/webapp/app/shared/gitlab/gitlab-path-selector/gitlab-path-selector.component.html
index 7cc4db1adce563c2257921700ecc41fb939a8eda..b3b2f8a8dac7fcfc7f563573515853c6d83f92b6 100644
--- a/src/main/webapp/app/shared/gitlab/gitlab-path-selector/gitlab-path-selector.component.html
+++ b/src/main/webapp/app/shared/gitlab/gitlab-path-selector/gitlab-path-selector.component.html
@@ -50,12 +50,12 @@
         </div>
       </div>
       <div *ngIf="isLoading">
-        <div class="h-100 w-100 d-flex justify-content-center align-items-center m-5">
+        <div class="d-flex justify-content-center align-items-center m-5">
           <div class="spinner"></div>
         </div>
       </div>
       <div *ngIf="errorOccurred">
-        <div class="h-100 w-100 d-flex justify-content-center align-items-center m-5">
+        <div class="d-flex justify-content-center align-items-center m-5">
           <strong jhiTranslate="gitlab.pathSelector.errorOccurred"></strong>
         </div>
       </div>
diff --git a/src/main/webapp/i18n/de/exercise.json b/src/main/webapp/i18n/de/exercise.json
index a0c9755c3b63bad5d5548c9845762c84cc7c54c1..e8e295c0ce10fdd69487c6eaf0e7105a0e2444a9 100644
--- a/src/main/webapp/i18n/de/exercise.json
+++ b/src/main/webapp/i18n/de/exercise.json
@@ -58,6 +58,7 @@
       "medium": "Mittel",
       "difficult": "Schwierig",
       "advanced": "Schwierig",
+      "none": "Keine",
       "educationalLevel": {
         "singular": "Bildungsniveau",
         "plural": "Bildungsniveau"
@@ -146,6 +147,15 @@
         "submit": "Speichern"
       },
       "error": {
+        "unauthorized": "Es ist ein Autorisierungsfehler aufgetreten. Sind Sie eingeloggt?",
+        "notFound": "Diese Aufgabe konnte nicht mehr gefunden werden.",
+        "loadingError": "Beim Laden der importierten Aufgabe ist ein Fehler aufgetreten.",
+        "retry": "Bitte versuchen Sie erneut, die Ãœbung zu importieren",
+        "invalidMetadata": "Ungültige Metadaten. Bitte überprüfen Sie Ihre Eingaben.",
+        "invalidCreator": "Die angegebenen <strong>Erstellerinformationen</strong> sind ungültig",
+        "invalidPublisher": "Die angegebenen <strong>Herausgeber</strong> Daten sind ungültig",
+        "invalidKeyword": "Eines oder mehrere der angegebenen <strong>Schlüsselwörter</strong> sind ungültig",
+        "invalidProgrammingLanguage": "Eines oder mehrere der angegebenen <strong>Programmiersprachen</strong> sind ungültig",
         "creationFailed": "Beim Erstellen des neuen Gitlab-Projekts ist ein Fehler aufgetreten. Bitte stellen Sie sicher, dass Sie über ausreichende Berechtigungen für die gewählte Gruppe verfügen.",
         "checkoutFailed": "Das Git-Repository konnte nicht ausgecheckt werden. Bitte stellen Sie sicher, dass Sie über ausreichende Berechtigungen für die gewählte Gruppe verfügen.",
         "pushFailed": "Beim Committing und Pushing der Daten in das neue Git-Repository ist ein Fehler aufgetreten. Bitte stellen Sie sicher, dass Sie über ausreichende Berechtigungen für die gewählte Gruppe verfügen."
diff --git a/src/main/webapp/i18n/en/exercise.json b/src/main/webapp/i18n/en/exercise.json
index 019cddd2774f8ea17427c77411f8e7ca0049f16d..2c635b7066889c396fb1db43babcb57f11f4ed63 100644
--- a/src/main/webapp/i18n/en/exercise.json
+++ b/src/main/webapp/i18n/en/exercise.json
@@ -58,6 +58,7 @@
       "medium": "Medium",
       "difficult": "Difficult",
       "advanced": "Difficult",
+      "none": "None",
       "educationalLevel": {
         "singular": "Educational Level",
         "plural": "Educational Level"
@@ -146,6 +147,15 @@
         "submit": "Submit"
       },
       "error": {
+        "unauthorized": "An authorization error occurred. Are you logged in?",
+        "notFound": "This exercise couldn't be found anymore.",
+        "loadingError": "An error occurred while loading the imported exercise",
+        "retry": "Please retry to import the exercise",
+        "invalidMetadata": "Invalid metadata. Please check your inputs.",
+        "invalidCreator": "The provided <strong>creator's information</strong> is invalid",
+        "invalidPublisher": "The provided <strong>publisher's information</strong> is invalid",
+        "invalidKeyword": "One or more of the provided <strong>keywords</strong> are invalid",
+        "invalidProgrammingLanguage": "One or more of the provided <strong>programming languages</strong> are invalid",
         "creationFailed": "An error occurred while creating the new Gitlab Project. Please make sure to have sufficient permissions for the chosen group.",
         "checkoutFailed": "Unable to checkout the Git repository. Please make sure to have sufficient permissions for the chosen group.",
         "pushFailed": "An error occurred while committing and pushing the data to the new Git repository. Please make sure to have sufficient permissions for the chosen group."
diff --git a/src/test/java/at/ac/uibk/gitsearch/service/ExerciseImportServiceIT.java b/src/test/java/at/ac/uibk/gitsearch/service/ExerciseImportServiceIT.java
new file mode 100644
index 0000000000000000000000000000000000000000..15383c40b372c62dce0893bb9a6d450e08c3c848
--- /dev/null
+++ b/src/test/java/at/ac/uibk/gitsearch/service/ExerciseImportServiceIT.java
@@ -0,0 +1,300 @@
+package at.ac.uibk.gitsearch.service;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+import at.ac.uibk.gitsearch.GitsearchApp;
+import at.ac.uibk.gitsearch.es.model.ArtemisExerciseInfo;
+import at.ac.uibk.gitsearch.service.dto.MetadataUserDTO;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+import javax.ws.rs.client.Client;
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.gitlab4j.api.models.Group;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InjectMocks;
+import org.mockito.MockitoAnnotations;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.security.test.context.support.WithMockUser;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@SpringBootTest(classes = GitsearchApp.class)
+@RunWith(SpringJUnit4ClassRunner.class)
+@WithMockUser(value = "test", authorities = "sharing")
+public class ExerciseImportServiceIT {
+
+    private final Logger log = LoggerFactory.getLogger(ExerciseImportServiceIT.class);
+
+    @InjectMocks
+    @Autowired
+    ExerciseImportService exerciseImportService;
+
+    @MockBean
+    GitlabService gitlabService;
+
+    String token = "testToken";
+
+    String TMP_DIR_PROPERTY = "java.io.tmpdir";
+
+    Path testZipPath = Paths.get(System.getProperty(TMP_DIR_PROPERTY), "testZip.zip");
+
+    ArtemisExerciseInfo preliminaryArtemisExerciseInfo = new ArtemisExerciseInfo();
+
+    ArtemisExerciseInfo fullArtemisExerciseInfo = new ArtemisExerciseInfo();
+
+    @Before
+    public void init() throws IOException {
+        MockitoAnnotations.openMocks(this);
+
+        preliminaryArtemisExerciseInfo.setMetadataVersion("1");
+        preliminaryArtemisExerciseInfo.setTitle("TestTitle");
+        preliminaryArtemisExerciseInfo.setLicense("testLicense");
+        preliminaryArtemisExerciseInfo.setLearningResourceType("artemis");
+        preliminaryArtemisExerciseInfo.setStructure("atomic");
+        preliminaryArtemisExerciseInfo.setFormat(List.of("artemis"));
+
+        MetadataUserDTO metadataUser = new MetadataUserDTO();
+        metadataUser.setName("Test User");
+        metadataUser.setAffiliation("Test Affiliation");
+        metadataUser.setEmail("test@user");
+
+        fullArtemisExerciseInfo.setMetadataVersion("1");
+        fullArtemisExerciseInfo.setTitle("TestTitle");
+        fullArtemisExerciseInfo.setLicense("testLicense");
+        fullArtemisExerciseInfo.setLearningResourceType("artemis");
+        fullArtemisExerciseInfo.setStructure("atomic");
+        fullArtemisExerciseInfo.setFormat(List.of("artemis"));
+        fullArtemisExerciseInfo.setKeyword(List.of("keyword"));
+        fullArtemisExerciseInfo.setDescription("TestDescription");
+        fullArtemisExerciseInfo.setLanguage(List.of("en"));
+        fullArtemisExerciseInfo.setCreator(List.of(metadataUser));
+        fullArtemisExerciseInfo.setPublisher(List.of(metadataUser));
+        fullArtemisExerciseInfo.setIdentifier("https://test:8080/test/path/url");
+        fullArtemisExerciseInfo.setProgrammingLanguage(List.of("C"));
+    }
+
+    @After
+    public void cleanUp() throws IOException {
+        if (new File(System.getProperty("java.io.tmpdir"), token).exists()) {
+            FileUtils.deleteDirectory(new File(System.getProperty("java.io.tmpdir") + token));
+        }
+        try {
+            Files.deleteIfExists(testZipPath);
+        } catch (FileSystemException fse) {
+            log.warn("Couldn't delete {}, if the problem persists, please remove this file manually", testZipPath.toAbsolutePath());
+        }
+    }
+
+    @Test
+    public void testImportExercise() throws Exception {
+        int gitlabGroup = 1;
+
+        File metadata = Paths.get(System.getProperty(TMP_DIR_PROPERTY), token, "Exercise-Details-something.json").toFile();
+        File problemStatement = Paths.get(System.getProperty(TMP_DIR_PROPERTY), token, "Problem-Statement-something.md").toFile();
+
+        metadata.getParentFile().mkdirs();
+        metadata.createNewFile();
+        problemStatement.createNewFile();
+        Group group = new Group();
+        Repository jgitRepo = FileRepositoryBuilder.create(Paths.get(System.getProperty("java.io.tmpdir"), token, ".git").toFile());
+
+        // mocks
+        doNothing().when(gitlabService).setJGitDefaultAuth();
+        doReturn(group).when(gitlabService).getGroupById(gitlabGroup);
+        doReturn("repoUrl").when(gitlabService).createRepository(group, fullArtemisExerciseInfo.getTitle());
+        doReturn(jgitRepo).when(gitlabService).checkoutRepo("repoUrl");
+        doNothing().when(gitlabService).commitAndPushToRepo(jgitRepo, new File(System.getProperty("java.io.tmpdir"), token));
+
+        exerciseImportService.importExercise(fullArtemisExerciseInfo, token, gitlabGroup);
+
+        // asserts
+        verify(gitlabService, times(1)).createRepository(group, preliminaryArtemisExerciseInfo.getTitle());
+        verify(gitlabService, times(1)).commitAndPushToRepo(jgitRepo, new File(System.getProperty("java.io.tmpdir"), token));
+    }
+
+    @Test
+    public void testApplyExerciseInfoChanges() throws Exception {
+        File metadata = Paths.get(System.getProperty("java.io.tmpdir"), token, "metadata.yaml").toFile();
+        metadata.getParentFile().mkdirs();
+        metadata.createNewFile();
+
+        exerciseImportService.applyExerciseInfoChanges(fullArtemisExerciseInfo, token);
+
+        Assert.assertTrue(new String(Files.readAllBytes(metadata.toPath())).contains("learningResourceType: \"artemis\""));
+        Assert.assertTrue(new String(Files.readAllBytes(metadata.toPath())).contains("license: \"testLicense\""));
+        Assert.assertTrue(new String(Files.readAllBytes(metadata.toPath())).contains("metadataVersion: \"1\""));
+        Assert.assertTrue(new String(Files.readAllBytes(metadata.toPath())).contains("title: \"TestTitle\""));
+        Assert.assertTrue(new String(Files.readAllBytes(metadata.toPath())).contains("description: \"TestDescription\""));
+        Assert.assertTrue(new String(Files.readAllBytes(metadata.toPath())).contains("language:\n - \"en\""));
+        Assert.assertTrue(new String(Files.readAllBytes(metadata.toPath())).contains("creator:\n -\n  name: \"Test User\""));
+    }
+
+    @Test
+    public void testGetArtemisExerciseInfo() throws Exception {
+        File metadata = Paths.get(System.getProperty("java.io.tmpdir"), token, "Exercise-Details-something.json").toFile();
+        metadata.getParentFile().mkdirs();
+        metadata.createNewFile();
+        FileWriter writer = new FileWriter(metadata, StandardCharsets.UTF_8);
+        writer.write(
+            "{\n" +
+            "  \"metadataVersion\": \"1\",\n" +
+            "  \"type\": \"programming exercise\",\n" +
+            "  \"title\": \"TestTitle\",\n" +
+            "  \"license\": \"testLicense\",\n" +
+            "  \"maxPoints\": 100,\n" +
+            "  \"bonusPoints\": 100,\n" +
+            "  \"mode\": \"INDIVIDUAL\",\n" +
+            "  \"staticCodeAnalysis\": false,\n" +
+            "  \"allowOfflineIDE\": false,\n" +
+            "  \"allowOnlineEditor\": false,\n" +
+            "  \"showTestNamesToStudents\": false,\n" +
+            "  \"sequentialTestRuns\": false\n" +
+            "}"
+        );
+        writer.close();
+
+        preliminaryArtemisExerciseInfo.setMaxPoints(100f);
+        preliminaryArtemisExerciseInfo.setBonusPoints(100f);
+        preliminaryArtemisExerciseInfo.setMode(ArtemisExerciseInfo.ExerciseMode.INDIVIDUAL);
+        preliminaryArtemisExerciseInfo.setStaticCodeAnalysis(false);
+        preliminaryArtemisExerciseInfo.setAllowOfflineIDE(false);
+        preliminaryArtemisExerciseInfo.setAllowOnlineEditor(false);
+        preliminaryArtemisExerciseInfo.setShowTestNamesToStudents(false);
+        preliminaryArtemisExerciseInfo.setSequentialTestRuns(false);
+        preliminaryArtemisExerciseInfo.setStructure("atomic");
+
+        Assert.assertEquals(preliminaryArtemisExerciseInfo, exerciseImportService.getArtemisExerciseInfo(token));
+    }
+
+    @Test
+    @SuppressWarnings("PMD")
+    public void testGetArtemisExerciseInfo_onlyMetadata() throws Exception {
+        File metadata = Paths.get(System.getProperty("java.io.tmpdir"), token, "Exercise-Details-something.json").toFile();
+        metadata.getParentFile().mkdirs();
+        metadata.createNewFile();
+        FileWriter writer = new FileWriter(metadata, StandardCharsets.UTF_8);
+        writer.write(
+            "{\n" +
+            "  \"metadataVersion\": \"1\",\n" +
+            "  \"type\": \"programming exercise\",\n" +
+            "  \"title\": \"TestTitle\",\n" +
+            "  \"license\": \"testLicense\"\n" +
+            "}"
+        );
+        writer.close();
+
+        Assert.assertEquals(preliminaryArtemisExerciseInfo, exerciseImportService.getArtemisExerciseInfo(token));
+    }
+
+    @Test
+    public void testValidate() throws Exception {
+        String testApiKeyCorrect = "2c8845a4-b3df-414b-a682-36e2313dc1c0";
+        String testApiKeyIncorrect = "wrong-apikey";
+
+        Assert.assertTrue(exerciseImportService.validate(testApiKeyCorrect));
+        Assert.assertFalse(exerciseImportService.validate(testApiKeyIncorrect));
+    }
+
+    @Test
+    public void testValidateExerciseInfo_correct() throws Exception {
+        // Should not throw any Exception
+        exerciseImportService.validateExerciseInfo(fullArtemisExerciseInfo);
+    }
+
+    @Test(expected = ArtemisImportError.class)
+    public void testValidateExerciseInfo_missing() throws Exception {
+        exerciseImportService.validateExerciseInfo(preliminaryArtemisExerciseInfo);
+    }
+
+    @Test
+    public void testSanitizeExerciseInfo() throws Exception {
+        int gitlabGroup = 1;
+
+        File metadata = Paths.get(System.getProperty("java.io.tmpdir"), token, "Exercise-Details-something.json").toFile();
+        metadata.getParentFile().mkdirs();
+        metadata.createNewFile();
+        Group group = new Group();
+        Repository jgitRepo = FileRepositoryBuilder.create(Paths.get(System.getProperty("java.io.tmpdir"), token, ".git").toFile());
+
+        // mocks
+        MetadataUserDTO user = new MetadataUserDTO();
+        user.setName("<script>Name");
+        user.setEmail("user@<script>test");
+        user.setAffiliation("test<script>affiliation");
+
+        fullArtemisExerciseInfo.setMetadataVersion("<script>");
+        fullArtemisExerciseInfo.setTitle(fullArtemisExerciseInfo.getTitle() + "<script>");
+        fullArtemisExerciseInfo.setDescription(fullArtemisExerciseInfo.getDescription() + "<script>");
+        fullArtemisExerciseInfo.setKeyword(List.of("Key<script>word"));
+        fullArtemisExerciseInfo.setCreator(List.of(user));
+        fullArtemisExerciseInfo.setPublisher(List.of(user));
+        fullArtemisExerciseInfo.setLicense(fullArtemisExerciseInfo.getLicense() + "<script>");
+        fullArtemisExerciseInfo.setFormat(List.of("<script>"));
+        fullArtemisExerciseInfo.setLearningResourceType(fullArtemisExerciseInfo.getLearningResourceType() + "<script>");
+
+        doNothing().when(gitlabService).setJGitDefaultAuth();
+        doReturn(group).when(gitlabService).getGroupById(gitlabGroup);
+        doReturn("repoUrl").when(gitlabService).createRepository(any(), anyString());
+        doReturn(jgitRepo).when(gitlabService).checkoutRepo("repoUrl");
+        doNothing().when(gitlabService).commitAndPushToRepo(jgitRepo, new File(System.getProperty("java.io.tmpdir"), token));
+
+        exerciseImportService.importExercise(fullArtemisExerciseInfo, token, gitlabGroup);
+
+        Assert.assertFalse(fullArtemisExerciseInfo.getMetadataVersion().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getTitle().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getDescription().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getKeyword().get(0).contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getCreator().get(0).getName().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getCreator().get(0).getEmail().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getCreator().get(0).getAffiliation().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getPublisher().get(0).getName().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getPublisher().get(0).getEmail().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getPublisher().get(0).getAffiliation().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getLicense().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getFormat().contains("<script"));
+        Assert.assertFalse(fullArtemisExerciseInfo.getLearningResourceType().contains("<script"));
+    }
+
+    @Test
+    public void testImportExerciseFromArtemis() throws Exception {
+        ExerciseImportService mockExerciseImportService = spy(exerciseImportService);
+
+        ZipOutputStream out = new ZipOutputStream(new FileOutputStream(testZipPath.toFile()));
+        out.putNextEntry(new ZipEntry("testFile.yml"));
+        byte[] buffer = new byte[1024];
+
+        int length;
+        FileInputStream fis = new FileInputStream(testZipPath.toFile());
+        while ((length = fis.read(buffer)) >= 0) {
+            out.write(buffer, 0, length);
+        }
+        out.closeEntry();
+        fis.close();
+
+        doReturn(new ZipInputStream(new FileInputStream(testZipPath.toFile())))
+            .when(mockExerciseImportService)
+            .getZipInputStreamFromUrl(anyString());
+        mockExerciseImportService.importExerciseFromArtemis(token);
+
+        Assert.assertTrue(Paths.get(System.getProperty(TMP_DIR_PROPERTY), token).toFile().exists());
+    }
+}
diff --git a/src/test/java/at/ac/uibk/gitsearch/service/ExerciseServiceIT.java b/src/test/java/at/ac/uibk/gitsearch/service/ExerciseServiceIT.java
deleted file mode 100644
index e3941b38149d74c1f37bb1e26655e39d6bd6386f..0000000000000000000000000000000000000000
--- a/src/test/java/at/ac/uibk/gitsearch/service/ExerciseServiceIT.java
+++ /dev/null
@@ -1,182 +0,0 @@
-package at.ac.uibk.gitsearch.service;
-
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import at.ac.uibk.gitsearch.GitsearchApp;
-import at.ac.uibk.gitsearch.es.model.ArtemisExerciseInfo;
-import java.io.File;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import org.apache.commons.io.FileUtils;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
-import org.gitlab4j.api.models.Group;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InjectMocks;
-import org.mockito.MockitoAnnotations;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.security.test.context.support.WithMockUser;
-import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
-
-@SpringBootTest(classes = GitsearchApp.class)
-@RunWith(SpringJUnit4ClassRunner.class)
-@WithMockUser(value = "test", authorities = "sharing")
-public class ExerciseServiceIT {
-
-    @InjectMocks
-    @Autowired
-    ExerciseService exerciseService;
-
-    @MockBean
-    GitlabService gitlabService;
-
-    String token = "testToken";
-
-    ArtemisExerciseInfo artemisExerciseInfo = new ArtemisExerciseInfo();
-
-    @Before
-    public void init() throws IOException {
-        MockitoAnnotations.openMocks(this);
-        artemisExerciseInfo.setMetadataVersion("1");
-        artemisExerciseInfo.setLearningResourceType("Artemis");
-        artemisExerciseInfo.setTitle("TestTitle");
-        artemisExerciseInfo.setLicense("testLicense");
-    }
-
-    @After
-    public void cleanUp() throws IOException {
-        if (new File(System.getProperty("java.io.tmpdir"), token).exists()) {
-            FileUtils.deleteDirectory(new File(System.getProperty("java.io.tmpdir") + token));
-        }
-    }
-
-    @Test
-    public void testImportExercise() throws Exception {
-        int gitlabGroup = 1;
-
-        File metadata = Paths.get(System.getProperty("java.io.tmpdir"), token, "Exercise-Details-something.json").toFile();
-        metadata.getParentFile().mkdirs();
-        metadata.createNewFile();
-        Group group = new Group();
-        Repository jgitRepo = FileRepositoryBuilder.create(Paths.get(System.getProperty("java.io.tmpdir"), token, ".git").toFile());
-
-        // mocks
-        doNothing().when(gitlabService).setJGitDefaultAuth();
-        doReturn(group).when(gitlabService).getGroupById(gitlabGroup);
-        doReturn("repoUrl").when(gitlabService).createRepository(group, artemisExerciseInfo.getTitle());
-        doReturn(jgitRepo).when(gitlabService).checkoutRepo("repoUrl");
-        doNothing().when(gitlabService).commitAndPushToRepo(jgitRepo, new File(System.getProperty("java.io.tmpdir"), token));
-
-        exerciseService.importExercise(artemisExerciseInfo, token, gitlabGroup);
-
-        // asserts
-        verify(gitlabService, times(1)).createRepository(group, artemisExerciseInfo.getTitle());
-        verify(gitlabService, times(1)).commitAndPushToRepo(jgitRepo, new File(System.getProperty("java.io.tmpdir"), token));
-    }
-
-    @Test
-    public void testApplyExerciseInfoChanges() throws Exception {
-        File metadata = Paths.get(System.getProperty("java.io.tmpdir"), token, "metadata.yaml").toFile();
-        metadata.getParentFile().mkdirs();
-        metadata.createNewFile();
-
-        exerciseService.applyExerciseInfoChanges(artemisExerciseInfo, token);
-
-        Assert.assertEquals(
-            "learningResourceType: \"Artemis\"\n" + "license: \"testLicense\"\n" + "metadataVersion: \"1\"\n" + "title: \"TestTitle\"",
-            new String(Files.readAllBytes(metadata.toPath())).strip()
-        );
-    }
-
-    @Test
-    public void testGetArtemisExerciseInfo() throws Exception {
-        File metadata = Paths.get(System.getProperty("java.io.tmpdir"), token, "Exercise-Details-something.json").toFile();
-        metadata.getParentFile().mkdirs();
-        metadata.createNewFile();
-        FileWriter writer = new FileWriter(metadata, StandardCharsets.UTF_8);
-        writer.write(
-            "{\n" +
-            "  \"metadataVersion\": \"1\",\n" +
-            "  \"type\": \"programming exercise\",\n" +
-            "  \"title\": \"TestTitle\",\n" +
-            "  \"license\": \"testLicense\",\n" +
-            "  \"maxPoints\": 100,\n" +
-            "  \"bonusPoints\": 100,\n" +
-            "  \"mode\": \"INDIVIDUAL\",\n" +
-            "  \"staticCodeAnalysis\": false,\n" +
-            "  \"allowOfflineIDE\": false,\n" +
-            "  \"allowOnlineEditor\": false,\n" +
-            "  \"showTestNamesToStudents\": false,\n" +
-            "  \"sequentialTestRuns\": false\n" +
-            "}"
-        );
-        writer.close();
-
-        artemisExerciseInfo.setMaxPoints(100f);
-        artemisExerciseInfo.setBonusPoints(100f);
-        artemisExerciseInfo.setMode(ArtemisExerciseInfo.ExerciseMode.INDIVIDUAL);
-        artemisExerciseInfo.setStaticCodeAnalysis(false);
-        artemisExerciseInfo.setAllowOfflineIDE(false);
-        artemisExerciseInfo.setAllowOnlineEditor(false);
-        artemisExerciseInfo.setShowTestNamesToStudents(false);
-        artemisExerciseInfo.setSequentialTestRuns(false);
-        artemisExerciseInfo.setLearningResourceType(null);
-        artemisExerciseInfo.setStructure("atomic");
-        List<String> str = new ArrayList<>();
-        str.add("artemis");
-
-        artemisExerciseInfo.setFormat(str);
-
-        Assert.assertEquals(artemisExerciseInfo, exerciseService.getArtemisExerciseInfo(token));
-    }
-
-    @Test
-    @SuppressWarnings("PMD")
-    public void testGetArtemisExerciseInfo_onlyMetadata() throws Exception {
-        File metadata = Paths.get(System.getProperty("java.io.tmpdir"), token, "Exercise-Details-something.json").toFile();
-        metadata.getParentFile().mkdirs();
-        metadata.createNewFile();
-        FileWriter writer = new FileWriter(metadata, StandardCharsets.UTF_8);
-        writer.write(
-            "{\n" +
-            "  \"metadataVersion\": \"1\",\n" +
-            "  \"type\": \"programming exercise\",\n" +
-            "  \"title\": \"TestTitle\",\n" +
-            "  \"license\": \"testLicense\"\n" +
-            "}"
-        );
-        writer.close();
-
-        artemisExerciseInfo.setLearningResourceType(null);
-        artemisExerciseInfo.setStructure("atomic");
-        List<String> str = new ArrayList<>();
-        str.add("artemis");
-
-        artemisExerciseInfo.setFormat(str);
-
-        Assert.assertEquals(artemisExerciseInfo, exerciseService.getArtemisExerciseInfo(token));
-    }
-
-    @Test
-    public void testValidate() throws Exception {
-        String testApiKeyCorrect = "2c8845a4-b3df-414b-a682-36e2313dc1c0";
-        String testApiKeyIncorrect = "wrong-apikey";
-
-        Assert.assertTrue(exerciseService.validate(testApiKeyCorrect));
-        Assert.assertFalse(exerciseService.validate(testApiKeyIncorrect));
-    }
-}
diff --git a/src/test/javascript/spec/app/exercise/import/exercise-import.component.spec.ts b/src/test/javascript/spec/app/exercise/import/exercise-import.component.spec.ts
deleted file mode 100644
index d6b7b461006f6f5a02649720043e55dc093e7f30..0000000000000000000000000000000000000000
--- a/src/test/javascript/spec/app/exercise/import/exercise-import.component.spec.ts
+++ /dev/null
@@ -1,173 +0,0 @@
-import { HttpClient, HttpHandler } from '@angular/common/http';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { ActivatedRoute } from '@angular/router';
-import { ExerciseImportComponent } from 'app/exercise/import/exercise-import.component';
-import { ExerciseService } from 'app/exercise/service/exercise.service';
-import { ArtemisExerciseInfo } from 'app/shared/model/artemis-exercise-info.model';
-import { IInteractivityType } from 'app/shared/model/exercise.model';
-import { GitlabGroup, IVisibility } from 'app/shared/model/gitlab-group.model';
-import { SearchResultDTO } from 'app/shared/model/search/search-result-dto.model';
-import { of } from 'rxjs';
-
-describe('Component Tests', () => {
-  describe('Exercise Import Component', () => {
-    let comp: ExerciseImportComponent;
-    let fixture: ComponentFixture<ExerciseImportComponent>;
-    let service: ExerciseService;
-
-    const exerciseInfoTemplate = new ArtemisExerciseInfo(
-      [],
-      '',
-      [],
-      [],
-      false,
-      '',
-      0,
-      '',
-      '',
-      [],
-      [],
-      [],
-      '',
-      '',
-      '',
-      {} as IInteractivityType,
-      [],
-      [],
-      [],
-      new Date(),
-      '',
-      '',
-      '',
-      {} as SearchResultDTO,
-      ['Java'],
-      '',
-      [],
-      0,
-      [],
-      '',
-      '',
-      [],
-      [],
-      '',
-      'testExTitle',
-      '',
-      '',
-      0,
-      0,
-      0,
-      false
-    );
-
-    beforeEach(() => {
-      TestBed.configureTestingModule({
-        imports: [],
-        declarations: [ExerciseImportComponent],
-        providers: [
-          {
-            provide: ActivatedRoute,
-            useValue: {
-              snapshot: {
-                params: {
-                  token: 'exercise-test-token',
-                },
-                queryParams: {
-                  callback: 'https://callback-url.test',
-                },
-              },
-            },
-          },
-          ExerciseImportComponent,
-          ExerciseService,
-          HttpClient,
-          HttpHandler,
-        ],
-      })
-        .overrideTemplate(ExerciseImportComponent, '')
-        .compileComponents();
-
-      fixture = TestBed.createComponent(ExerciseImportComponent);
-      comp = fixture.componentInstance;
-      service = TestBed.get(ExerciseService);
-    });
-
-    it('Should load on init', () => {
-      // GIVEN
-      spyOn(service, 'importExerciseInfoFromArtemis').and.returnValue(of(exerciseInfoTemplate));
-
-      // WHEN
-      comp.ngOnInit();
-
-      // THEN
-      expect(comp.exerciseInfo?.title).toEqual('testExTitle');
-      expect(comp.exerciseInfo?.programmingLanguage[0]).toEqual('Java');
-    });
-
-    it('Should map metadata values when importing', () => {
-      // GIVEN
-      comp.exerciseToken = 'TestToken';
-      comp.exerciseInfo = exerciseInfoTemplate;
-      comp.exerciseInfo.difficulty = 'ADVANCED';
-      comp.isPublisherSameAsCreator = true;
-
-      const group: GitlabGroup = {
-        id: 1,
-        name: 'testGroup',
-        path: 'testPath',
-        description: '',
-        visibility: IVisibility.PUBLIC,
-        avatarUrl: '',
-        webUrl: '',
-        fullName: '',
-        fullPath: '',
-        parentId: 0,
-      };
-      spyOn(comp, 'openGitlabPathSelectorModal').and.returnValue(
-        new Promise(resolve => {
-          resolve(group);
-        })
-      );
-      spyOn(service, 'submitExerciseInfoForImport').and.returnValue(of({}));
-
-      // WHEN
-      comp.import();
-
-      // THEN
-      expect(comp.exerciseInfo.difficulty).toEqual(comp.exerciseInfo.difficulty.toLowerCase());
-      expect(comp.exerciseInfo.publisher).toEqual(comp.exerciseInfo.creator);
-    });
-
-    it('Should add and remove creators from list', () => {
-      // GIVEN
-      comp.exerciseInfo = exerciseInfoTemplate;
-
-      const newPerson = { name: 'newPerson', email: 'person@email', affiliation: 'newAffiliation' };
-      const newPersonCopy = { name: 'newPerson', email: 'person@email', affiliation: 'newAffiliation' };
-      const personToRemove = { name: 'personToRemove', email: 'person@toremove', affiliation: '' };
-
-      // WHEN
-      comp.addPerson(newPerson, 'creator');
-      comp.addPerson(personToRemove, 'creator');
-      comp.removePerson(personToRemove, 'creator');
-
-      // THEN
-      expect(comp.exerciseInfo?.creator).toEqual([newPersonCopy]);
-    });
-
-    it('Should validate form button groups', () => {
-      // GIVEN
-      comp.exerciseInfo = exerciseInfoTemplate;
-      comp.exerciseInfo.keyword = [];
-      comp.exerciseInfo.language = [];
-      const resultWithErrors = comp.hasValidationErrors();
-
-      comp.exerciseInfo.keyword.push('keyword');
-      comp.setLanguage('english');
-      const resultWithoutErrors = comp.hasValidationErrors();
-
-      // THEN
-      expect(resultWithErrors).toEqual(true);
-      expect(resultWithoutErrors).toEqual(false);
-    });
-  });
-});