From 11f401b13ce6d61141965cf563238243443f88e9 Mon Sep 17 00:00:00 2001
From: Gero Lueckemeyer <gero.lueckemeyer@hft-stuttgart.de>
Date: Sat, 13 Jul 2024 22:42:07 +0200
Subject: [PATCH] added recommendations

---
 pom.xml                                       |   8 ++
 .../models/ExerciseCompetencyProfile.java     |  20 +++
 .../dtabackend/models/ICompetencyProfile.java |  35 +++++
 .../dtabackend/models/Recommendation.java     |  23 +++
 .../dtabackend/models/ResultSummary.java      |  29 ++--
 .../models/TestCompetencyProfile.java         |  67 +++------
 .../dtabackend/rest/v1/task/TaskUpload.java   |   8 +-
 .../rest/v1/unittest/UnitTestUpload.java      |   8 +-
 .../utils/CompetencyAssessmentUtil.java       |  62 +++++++--
 .../dtabackend/utils/ExecuteTestUtil.java     | 131 ++++++++++++++++--
 10 files changed, 296 insertions(+), 95 deletions(-)
 create mode 100644 src/main/java/de/hftstuttgart/dtabackend/models/ExerciseCompetencyProfile.java
 create mode 100644 src/main/java/de/hftstuttgart/dtabackend/models/ICompetencyProfile.java
 create mode 100644 src/main/java/de/hftstuttgart/dtabackend/models/Recommendation.java

diff --git a/pom.xml b/pom.xml
index 067e53e..015b92f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -163,6 +163,14 @@
             <version>4.11.0</version>
         </dependency>
         
+        <!-- https://mvnrepository.com/artifact/org.tmatesoft.svnkit/svnkit -->
+		<dependency>
+    		<groupId>org.tmatesoft.svnkit</groupId>
+    		<artifactId>svnkit</artifactId>
+    		<version>1.10.11</version>
+		</dependency>
+
+        
 	</dependencies>
 
 	<build>
diff --git a/src/main/java/de/hftstuttgart/dtabackend/models/ExerciseCompetencyProfile.java b/src/main/java/de/hftstuttgart/dtabackend/models/ExerciseCompetencyProfile.java
new file mode 100644
index 0000000..1b16afa
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/dtabackend/models/ExerciseCompetencyProfile.java
@@ -0,0 +1,20 @@
+package de.hftstuttgart.dtabackend.models;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+public class ExerciseCompetencyProfile implements ICompetencyProfile {
+
+	public String exerciseTopicName;
+    public String exerciseName;
+    public String exerciseURL;
+    
+    public float difficulty;
+
+	public float[] competencyAssessments=new float[MAX_COMPETENCY_DIMENSIONS];
+	
+    @Override
+    public boolean equals(Object other) {
+    	return other instanceof ExerciseCompetencyProfile && Objects.equals(exerciseURL, ((ExerciseCompetencyProfile) other).exerciseURL); 
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/dtabackend/models/ICompetencyProfile.java b/src/main/java/de/hftstuttgart/dtabackend/models/ICompetencyProfile.java
new file mode 100644
index 0000000..6f741a8
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/dtabackend/models/ICompetencyProfile.java
@@ -0,0 +1,35 @@
+package de.hftstuttgart.dtabackend.models;
+
+public interface ICompetencyProfile {
+
+	int MAX_COMPETENCY_DIMENSIONS = 16;
+	String COMPETENCY_SEPARATOR = ";";
+
+	public static float[] competencyProjection(float[] cp, float[] cp2) {
+	    float z[] = new float[MAX_COMPETENCY_DIMENSIONS];
+
+	    for (int i = 0; i < MAX_COMPETENCY_DIMENSIONS; i++) {
+	        z[i] = cp[i] * cp2[i];
+	    }
+	   return z;
+	}
+	
+	public static float[] competencyShare(float[] cp, float[] cpTotal) {
+	    float z[] = new float[MAX_COMPETENCY_DIMENSIONS];
+
+	    for (int i = 0; i < MAX_COMPETENCY_DIMENSIONS; i++) {
+	        z[i] = cp[i] / cpTotal[i];
+	    }
+	   return z;
+	}
+
+	public static float[] competencySum(float[] cp, float[] cp2) {
+	    float z[] = new float[MAX_COMPETENCY_DIMENSIONS];
+
+	    for (int i = 0; i < MAX_COMPETENCY_DIMENSIONS; i++) {
+	        z[i] = cp[i] + cp2[i];
+	    }
+	   return z;
+	}
+
+}
diff --git a/src/main/java/de/hftstuttgart/dtabackend/models/Recommendation.java b/src/main/java/de/hftstuttgart/dtabackend/models/Recommendation.java
new file mode 100644
index 0000000..f85d2f6
--- /dev/null
+++ b/src/main/java/de/hftstuttgart/dtabackend/models/Recommendation.java
@@ -0,0 +1,23 @@
+package de.hftstuttgart.dtabackend.models;
+
+import java.util.Comparator;
+
+public class Recommendation {
+	public String topic;
+    public String exerciseName;
+    public String url;
+    
+    public float difficulty;
+    
+    public float score;
+    
+    public static final Comparator<Recommendation> COMPARE_BY_SCORE = Comparator.comparingDouble(other -> other.score);
+    
+    public Recommendation(String topic, String exerciseName, String url, float difficulty, float score) {
+        this.topic = topic;
+        this.exerciseName = exerciseName;
+        this.url = url; 
+        this.difficulty = difficulty;
+        this.score = score;
+    }
+}
diff --git a/src/main/java/de/hftstuttgart/dtabackend/models/ResultSummary.java b/src/main/java/de/hftstuttgart/dtabackend/models/ResultSummary.java
index b9a11ef..80c8458 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/models/ResultSummary.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/models/ResultSummary.java
@@ -1,13 +1,16 @@
-package de.hftstuttgart.dtabackend.models;
-
-import java.util.HashSet;
-import java.util.Set;
-
-public class ResultSummary
-{
-    public long timestamp               = System.currentTimeMillis() / 1000;
-    public String globalStacktrace      = null;
-    public String successfulTestCompetencyProfile;
-    public String overallTestCompetencyProfile;
-    public Set<Result> results   = new HashSet<>();
-}
+package de.hftstuttgart.dtabackend.models;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class ResultSummary
+{
+    public long timestamp               = System.currentTimeMillis() / 1000;
+    public String globalStacktrace      = null;
+    public String successfulTestCompetencyProfile;
+    public String overallTestCompetencyProfile;
+    public Set<Result> results   = new HashSet<>();
+    public List<Recommendation> recommendations = new ArrayList<>();
+}
diff --git a/src/main/java/de/hftstuttgart/dtabackend/models/TestCompetencyProfile.java b/src/main/java/de/hftstuttgart/dtabackend/models/TestCompetencyProfile.java
index a6a5e3a..f4bee50 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/models/TestCompetencyProfile.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/models/TestCompetencyProfile.java
@@ -1,49 +1,18 @@
-package de.hftstuttgart.dtabackend.models;
-
-public class TestCompetencyProfile {
-	public static final int MAX_COMPETENCY_DIMENSIONS = 16;
-	
-	public static final String COMPETENCY_SEPARATOR=";";
-	
-	public String testPackageName;
-	public String testClassName;
-	public String testName;
-	
-	public float[] competencyAssessments=new float[MAX_COMPETENCY_DIMENSIONS]; 
-	
-	public static float[] competencyProjection(float[] cp, float[] cp2) {
-	    float z[] = new float[MAX_COMPETENCY_DIMENSIONS];
-
-	    for (int i = 0; i < MAX_COMPETENCY_DIMENSIONS; i++) {
-	        z[i] = cp[i] * cp2[i];
-	    }
-	   return z;
-	}
-	
-	public static float[] competencyShare(float[] cp, float[] cpTotal) {
-	    float z[] = new float[MAX_COMPETENCY_DIMENSIONS];
-
-	    for (int i = 0; i < MAX_COMPETENCY_DIMENSIONS; i++) {
-	        z[i] = cp[i] / cpTotal[i];
-	    }
-	   return z;
-	}
-
-	public static float[] competencySum(float[] cp, float[] cp2) {
-	    float z[] = new float[MAX_COMPETENCY_DIMENSIONS];
-
-	    for (int i = 0; i < MAX_COMPETENCY_DIMENSIONS; i++) {
-	        z[i] = cp[i] + cp2[i];
-	    }
-	   return z;
-	}
-	
-	@Override
-	public boolean equals(Object other) {
-		return other instanceof TestCompetencyProfile && 
-				testPackageName.equals(((TestCompetencyProfile)other).testPackageName) &&
-				testClassName.equals(((TestCompetencyProfile)other).testClassName) &&
-				testName.equals(((TestCompetencyProfile)other).testName);
-	}
-
-}
+package de.hftstuttgart.dtabackend.models;
+
+public class TestCompetencyProfile implements ICompetencyProfile {
+	public String testPackageName;
+	public String testClassName;
+	public String testName;
+	
+	public float[] competencyAssessments=new float[MAX_COMPETENCY_DIMENSIONS]; 
+	
+	@Override
+	public boolean equals(Object other) {
+		return other instanceof TestCompetencyProfile && 
+				testPackageName.equals(((TestCompetencyProfile)other).testPackageName) &&
+				testClassName.equals(((TestCompetencyProfile)other).testClassName) &&
+				testName.equals(((TestCompetencyProfile)other).testName);
+	}
+
+}
diff --git a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/task/TaskUpload.java b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/task/TaskUpload.java
index 1b4c4ca..7b14090 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/task/TaskUpload.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/task/TaskUpload.java
@@ -31,16 +31,16 @@ import jakarta.servlet.annotation.MultipartConfig;
 public class TaskUpload {
     private static final Logger LOG = LogManager.getLogger(TaskUpload.class);
 
-    private final RepoUtil jGitUtil;
+    private final RepoUtil repoUtil;
     private final Path testTmpPath;
     private final ExecuteTestUtil executeTestUtil;
 
     public TaskUpload(
         Environment env,
-        RepoUtil jGitUtil,
+        RepoUtil repoUtil,
         ExecuteTestUtil executeTestUtil
     ) {
-        this.jGitUtil = jGitUtil;
+        this.repoUtil = repoUtil;
         this.executeTestUtil = executeTestUtil;
 
         // set path of temporary directory on host and inside our container
@@ -78,7 +78,7 @@ public class TaskUpload {
                 	subDir=config.group(4);
                 }
                 LOG.debug("calling repo clone");
-                jGitUtil.cloneRepository(config, srcPath.toAbsolutePath().toString(), subDir);
+                repoUtil.cloneRepository(config, srcPath.toAbsolutePath().toString(), subDir);
                 break;
 
             case "application/zip":
diff --git a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java
index ce8b59a..79cc5ed 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/rest/v1/unittest/UnitTestUpload.java
@@ -30,11 +30,11 @@ import java.util.regex.Pattern;
 public class UnitTestUpload {
 
     private static final Logger LOG = LogManager.getLogger(UnitTestUpload.class);
-    private final RepoUtil jGitUtil;
+    private final RepoUtil repoUtil;
     private final String assignmentBasePath;
 
-    public UnitTestUpload(Environment env, RepoUtil jGitUtil) {
-        this.jGitUtil = jGitUtil;
+    public UnitTestUpload(Environment env, RepoUtil repoUtil) {
+        this.repoUtil = repoUtil;
 
         Path p = Paths.get(env.getProperty("data.dir"), env.getProperty("data.dir.test.folder.name"));
         this.assignmentBasePath = p.toAbsolutePath().toString();
@@ -78,7 +78,7 @@ public class UnitTestUpload {
         }
         LOG.debug("calling test repo clone");
         // cloning assignment repo to persistent space
-        jGitUtil.cloneRepository(config, Paths.get(this.assignmentBasePath, assignmentId).toAbsolutePath().toString(), subDir);
+        repoUtil.cloneRepository(config, Paths.get(this.assignmentBasePath, assignmentId).toAbsolutePath().toString(), subDir);
 
         LOG.info(String.format("stored new assignment: %s", file.getAbsolutePath()));
     }
diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java
index 40284d0..cdc3b43 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/utils/CompetencyAssessmentUtil.java
@@ -16,8 +16,9 @@ import org.apache.logging.log4j.Logger;
 
 import com.fasterxml.jackson.core.exc.StreamReadException;
 import com.fasterxml.jackson.databind.DatabindException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 
+import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile;
+import de.hftstuttgart.dtabackend.models.ICompetencyProfile;
 import de.hftstuttgart.dtabackend.models.Result;
 import de.hftstuttgart.dtabackend.models.ResultSummary;
 import de.hftstuttgart.dtabackend.models.TestCompetencyProfile;
@@ -28,31 +29,33 @@ public class CompetencyAssessmentUtil {
 	private static final Logger LOG = LogManager.getLogger(CompetencyAssessmentUtil.class);
 			
 	public static String TEST_COMPETENCY_MANIFEST_FILE_NAME="competency-tests.mft";
+	public static String EXERCISE_COMPETENCY_MANIFEST_FILE_NAME="exercise-tests.mft";
 	
-	public static void main(String[] args) throws StreamReadException, DatabindException, MalformedURLException, IOException {
-		ResultSummary summary=ExecuteTestUtil.generateResult(Path.of(args[0]), Path.of(args[1]));
+	/*public static void main(String[] args) throws StreamReadException, DatabindException, MalformedURLException, IOException {
+		ResultSummary summary=ExecuteTestUtil.generateResult("1", Path.of(args[0]), Path.of(args[1]));
         System.out.println(summary.successfulTestCompetencyProfile);
 	}
-	
+	*/
 	public static float[] sumTestCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles) {
-		float[] tcpTotalProfile=new float[TestCompetencyProfile.MAX_COMPETENCY_DIMENSIONS];
+		float[] tcpTotalProfile=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS];
 		for(TestCompetencyProfile currentProfile: testCompetencyProfiles) {
-			tcpTotalProfile=TestCompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments);
+			tcpTotalProfile=ICompetencyProfile.competencySum(tcpTotalProfile, currentProfile.competencyAssessments);
 		}
 		return tcpTotalProfile;
 	}
 	
-	public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary) {
-		float[] sumSuccessful=new float[TestCompetencyProfile.MAX_COMPETENCY_DIMENSIONS];
+	public static float[] sumSuccessfulCompetencyProfiles(List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary, boolean success) {
+		float[] sumSuccessful=new float[ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS];
 		for(Result currentResult: resultSummary.results) {
-			if(currentResult.state==Result.State.SUCCESS.ordinal()) {
+			boolean isSuccess = Integer.valueOf(currentResult.state).equals(Result.State.SUCCESS.ordinal());
+	        if (isSuccess == success) {
 				TestCompetencyProfile currentProfile=new TestCompetencyProfile();
 				currentProfile.testPackageName=(currentResult.packageName!=null)?currentResult.packageName:"";
 				currentProfile.testClassName=(currentResult.className!=null)?currentResult.className:"";
 				currentProfile.testName=(currentResult.name!=null)?currentResult.name:"";
 				int testIndex=testCompetencyProfiles.indexOf(currentProfile);
 				if(testIndex!=-1) {
-					sumSuccessful=TestCompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments);
+					sumSuccessful=ICompetencyProfile.competencySum(sumSuccessful, testCompetencyProfiles.get(testIndex).competencyAssessments);
 				}
 			}
 		}
@@ -66,12 +69,12 @@ public class CompetencyAssessmentUtil {
 			String testEntry=testCompetencyManifest.readLine();
 			while(testEntry!=null)
 			{
-				String[] testEntyComponents=testEntry.split(TestCompetencyProfile.COMPETENCY_SEPARATOR);
+				String[] testEntyComponents=testEntry.split(ICompetencyProfile.COMPETENCY_SEPARATOR);
 				TestCompetencyProfile currentProfile=new TestCompetencyProfile();
 				currentProfile.testPackageName=testEntyComponents[0];
 				currentProfile.testClassName=testEntyComponents[1];
 				currentProfile.testName=testEntyComponents[2];
-				for(int competencyIndex=0; competencyIndex<TestCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) {
+				for(int competencyIndex=0; competencyIndex<ICompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) {
 					currentProfile.competencyAssessments[competencyIndex]=Float.valueOf(testEntyComponents[competencyIndex+3]);
 				}
 				testCompetencyProfiles.add(currentProfile);
@@ -88,6 +91,41 @@ public class CompetencyAssessmentUtil {
 		} 
 		return testCompetencyProfiles;
 	}
+	
+	public static List<ExerciseCompetencyProfile> readExerciseCompetencyProfiles(Path exercisePath, String fileName) {
+	    List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = new ArrayList<>();
+
+	    try (BufferedReader exerciseCompetencyManifest = new BufferedReader(new FileReader(new File(exercisePath.toFile(), fileName)))) {
+	        String exerciseEntry = exerciseCompetencyManifest.readLine();
+
+	        while (exerciseEntry != null) {
+	            String[] exerciseEntyComponents = exerciseEntry.split(ExerciseCompetencyProfile.COMPETENCY_SEPARATOR);
+	            ExerciseCompetencyProfile currentProfile = new ExerciseCompetencyProfile();
+
+	            currentProfile.exerciseTopicName = exerciseEntyComponents[0];
+	            currentProfile.exerciseName = exerciseEntyComponents[1];
+	            currentProfile.exerciseURL = exerciseEntyComponents[2];
+	            
+	            for (int competencyIndex = 0; competencyIndex < ExerciseCompetencyProfile.MAX_COMPETENCY_DIMENSIONS; competencyIndex++) {
+	                currentProfile.competencyAssessments[competencyIndex] = Float.valueOf(exerciseEntyComponents[competencyIndex+3]);
+	            }
+	            
+	            currentProfile.difficulty = Float.parseFloat(exerciseEntyComponents[19]);
+
+	            exerciseCompetencyProfiles.add(currentProfile);
+	            
+	            exerciseEntry = exerciseCompetencyManifest.readLine();
+	        }
+	        exerciseCompetencyManifest.close();
+	        LOG.info("Added " + exerciseCompetencyProfiles.size() + " test competency profiles from exercise competency manifest.");
+	    } catch (FileNotFoundException e) {
+	        LOG.info("Exercise competency manifest file not found.");
+	    } catch (IOException e) {
+	        LOG.info("Exercise competency manifest file unreadable.");
+	    }
+
+	    return exerciseCompetencyProfiles;
+	}
 
 	public static String packFloats(float[] array) {
 		return IntStream.range(0, array.length)
diff --git a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
index 6ba00f8..333b177 100644
--- a/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
+++ b/src/main/java/de/hftstuttgart/dtabackend/utils/ExecuteTestUtil.java
@@ -5,6 +5,10 @@ import com.fasterxml.jackson.databind.DatabindException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.github.dockerjava.api.model.Bind;
 import com.github.dockerjava.api.model.Volume;
+
+import de.hftstuttgart.dtabackend.models.ExerciseCompetencyProfile;
+import de.hftstuttgart.dtabackend.models.ICompetencyProfile;
+import de.hftstuttgart.dtabackend.models.Recommendation;
 import de.hftstuttgart.dtabackend.models.ResultSummary;
 import de.hftstuttgart.dtabackend.models.TestCompetencyProfile;
 
@@ -18,26 +22,26 @@ import java.net.MalformedURLException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
 @Component
 public class ExecuteTestUtil  {
     private static final Logger LOG = LogManager.getLogger(ExecuteTestUtil.class);
 
-    private final RepoUtil jGitUtil;
     private final DockerUtil dockerUtil;
     private final String assignmentBasePath;
     private final Path testTmpPathHost;
-    private final Path testTmpPath;
 
     public ExecuteTestUtil(
         Environment env,
-        RepoUtil jGitUtil,
         DockerUtil dockerUtil
     ) {
-        this.jGitUtil = jGitUtil;
         this.dockerUtil = dockerUtil;
 
         // set base path for assignments to be stored
@@ -46,9 +50,8 @@ public class ExecuteTestUtil  {
             env.getProperty("data.dir.test.folder.name"));
         this.assignmentBasePath = p.toAbsolutePath().toString();
 
-        // set path of temporary directory on host and inside our container
+        // set path of temporary directory on host _and_ inside our container, _must_ be identical 
         this.testTmpPathHost = Paths.get(env.getProperty("host.tests.tmp.dir"));
-        this.testTmpPath = Paths.get(env.getProperty("tests.tmp.dir"));
     }
 
     public ResultSummary runTests(String assignmentId, Path workDirectory) throws IOException, InterruptedException {
@@ -114,12 +117,12 @@ public class ExecuteTestUtil  {
             new Bind(resultPathHost.toAbsolutePath().toString(), new Volume("/data/result"))
         );
 
-        ResultSummary resultSummary = generateResult(resultPath, testPathHost);
+        ResultSummary resultSummary = generateResult(assignmentId, resultPath, testPathHost);
         
         return resultSummary;
     }
 
-	static ResultSummary generateResult(Path resultPath, Path testPathHost)
+	private ResultSummary generateResult(String assignmentId, Path resultPath, Path testPathHost)
 			throws IOException, StreamReadException, DatabindException, MalformedURLException {
 		// define expected result file
         File resultFile = Paths.get(resultPath.toAbsolutePath().toString(), "result.json").toFile();
@@ -132,9 +135,7 @@ public class ExecuteTestUtil  {
 
         LOG.debug("parse results json");
         ObjectMapper objectMapper = new ObjectMapper();
-        ResultSummary resultSummary = objectMapper.readValue(
-            resultFile.toURI().toURL(),
-            ResultSummary.class);
+        ResultSummary resultSummary = objectMapper.readValue(resultFile.toURI().toURL(), ResultSummary.class);
         LOG.debug("result json returned time "+ resultSummary.timestamp + " with "+resultSummary.results.size()+ " test results.");
         
         LOG.info("Checking for optional test competency profile information for paedagogical agent functionality...");
@@ -142,8 +143,112 @@ public class ExecuteTestUtil  {
         if(testCompetencyProfiles!=null) {
         	LOG.info("Found optional test competency profiles, generating agent profile data...");
         	resultSummary.overallTestCompetencyProfile=CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumTestCompetencyProfiles(testCompetencyProfiles));
-        	resultSummary.successfulTestCompetencyProfile=CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary));
+        	resultSummary.successfulTestCompetencyProfile=CompetencyAssessmentUtil.packFloats(CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, true));
+        	
+        	LOG.info("Checking for optional exercise competency profile information for paedagogical agent exercise recommendation functionality...");
+        	//testPathHost or assignmentBasePath
+        	Path exerciseManifestFile = Paths.get(testPathHost.toAbsolutePath().toString(), CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
+			if (Files.exists(exerciseManifestFile)) {
+				LOG.info("Found optional exercise competency profiles, generating recommendations...");
+				resultSummary.recommendations = recommendNextExercises(assignmentId, testPathHost, testCompetencyProfiles, resultSummary);
+			}
         }
 		return resultSummary;
 	}
-}
+	
+	/*
+	 * exercise recommendation part
+	 */
+	public List<Recommendation> recommendNextExercises(String assignmentId, Path testPathHost, List<TestCompetencyProfile> testCompetencyProfiles, ResultSummary resultSummary)
+			throws FileNotFoundException {
+		// fetch repo url from original test upload
+		Pattern pattern = Pattern.compile(RegexUtil.DTA_TESTCONFIGREGEX);
+		File file = Paths.get(assignmentBasePath, assignmentId + ".txt").toFile();
+		FileInputStream configFileStream = new FileInputStream(file);
+		Matcher config = RegexUtil.extractConfig(configFileStream, pattern);
+		String testRepoURL = config.group(1);
+
+		List<ExerciseCompetencyProfile> exerciseCompetencyProfiles = CompetencyAssessmentUtil.readExerciseCompetencyProfiles(testPathHost, CompetencyAssessmentUtil.EXERCISE_COMPETENCY_MANIFEST_FILE_NAME);
+
+		int currentTopicIndex=0;
+		float currentDifficulty=0.0f;
+
+		//build course topic order
+		Map<String, Integer> topicOrder = new HashMap<>();
+		int order = 1;
+		for (ExerciseCompetencyProfile e : exerciseCompetencyProfiles) {
+			if (!topicOrder.containsKey(e.exerciseTopicName)) {
+				topicOrder.put(e.exerciseTopicName, order++);
+			}
+			if (e.exerciseURL.equals(testRepoURL)) {
+				currentTopicIndex = order;
+				currentDifficulty = e.difficulty;
+			}
+		}
+		
+		//filter exercises according to success
+		float[] unsuccessful = CompetencyAssessmentUtil.sumSuccessfulCompetencyProfiles(testCompetencyProfiles, resultSummary, false);
+		List<ExerciseCompetencyProfile> filteredExercises = filterExercisesByTopicsAndDifficulty(
+				exerciseCompetencyProfiles, topicOrder, currentTopicIndex, testRepoURL,
+				currentDifficulty, unsuccessful, resultSummary);
+		
+		//compute recommendations
+		List<Recommendation> recommendedExercises = new ArrayList<>();
+		for (ExerciseCompetencyProfile exerciseProfile : filteredExercises) {
+			Recommendation recommendation = new Recommendation(exerciseProfile.exerciseTopicName, exerciseProfile.exerciseURL, exerciseProfile.exerciseName,
+					exerciseProfile.difficulty, calculateScore(exerciseProfile, unsuccessful, topicOrder, currentDifficulty));
+			recommendedExercises.add(recommendation);
+			LOG.info("Recommending exercise "+recommendation.topic+"/"+recommendation.exerciseName+" with score "+recommendation.score);
+		}
+		//sort the recommendations for successful or resilient learners, otherwise reverse in display
+		recommendedExercises.stream().sorted(Recommendation.COMPARE_BY_SCORE).collect(Collectors.toList());
+		
+		return recommendedExercises;
+	}
+
+	public static List<ExerciseCompetencyProfile> filterExercisesByTopicsAndDifficulty(List<ExerciseCompetencyProfile> exerciseCompetencyProfiles,
+			Map<String, Integer> topicOrder, int currentTopicIndex, String testRepoURL, float currentDifficulty, float[] unsuccessful, 
+			ResultSummary resultSummary) {
+		//filter out all advanced topics in any case
+		//option for later: include next topic if fullsuccess and current difficulty == max difficulty
+		List<ExerciseCompetencyProfile> filteredExercises = exerciseCompetencyProfiles.stream()
+		        .filter(testProfile -> topicOrder.get(testProfile.exerciseTopicName) <= currentTopicIndex)
+		        .collect(Collectors.toList());
+
+		//filter by difficulty according to success
+		if (isFullSuccess(unsuccessful)) {
+			filteredExercises = filteredExercises.stream().filter(profile -> profile.difficulty >= currentDifficulty && !testRepoURL.equals(profile.exerciseURL)).collect(Collectors.toList());
+		} else {
+			filteredExercises = filteredExercises.stream().filter(profile -> profile.difficulty <= currentDifficulty).collect(Collectors.toList());
+		}
+
+		return filteredExercises;
+	}
+	
+	public static boolean isFullSuccess(float[] unsuccessful) {
+		for (float value : unsuccessful) {
+			if (value != 0.0f) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	public static float calculateScore(ExerciseCompetencyProfile exerciseProfile, float[] unsuccessful, Map<String, Integer> topicOrder, float currentDifficulty) {
+		//ensure factor 1 for full success not to blank out score, thus offset the base
+		float score = 1.0f;
+		//competency profile difference to not fully achieved competencies component
+		for (int i = 0; i < exerciseProfile.competencyAssessments.length-1; i++) {
+			score += exerciseProfile.competencyAssessments[i] * unsuccessful[i];
+		}
+		
+		//difficulty component
+		score = score * (exerciseProfile.difficulty*(0.5f+Math.abs(currentDifficulty-exerciseProfile.difficulty)));
+		
+		//topic component
+		score *= topicOrder.get(exerciseProfile.exerciseTopicName);
+
+		score = Math.round(score * 10.0f) / 10.0f;
+		return score;
+	}
+}
\ No newline at end of file
-- 
GitLab