44
55import java .net .URLEncoder ;
66import java .nio .charset .StandardCharsets ;
7- import java .util .ArrayList ;
8- import java .util .Arrays ;
97import java .util .List ;
108import java .util .Map ;
119import java .util .Optional ;
3432
3533/**
3634 * Service for handling LTI deep linking functionality.
35+ * This includes building and returning appropriate LTI launch URLs
36+ * for various Artemis content types such as exercises, lectures, competencies, etc.
3737 */
3838@ Service
3939@ Profile (PROFILE_LTI )
@@ -78,6 +78,7 @@ public String performDeepLinking(OidcIdToken ltiIdToken, String clientRegistrati
7878 case EXERCISE -> populateExerciseContentItems (String .valueOf (courseId ), unitIds );
7979 case GROUPED_EXERCISE -> List .of (populateGroupedExerciseContentItem (String .valueOf (courseId ), unitIds ));
8080 case LECTURE -> populateLectureContentItems (String .valueOf (courseId ), unitIds );
81+ case GROUPED_LECTURE -> List .of (populateGroupedLectureContentItems (String .valueOf (courseId ), unitIds ));
8182 case COMPETENCY -> populateCompetencyContentItems (String .valueOf (courseId ));
8283 case IRIS -> populateIrisContentItems (String .valueOf (courseId ));
8384 case LEARNING_PATH -> populateLearningPathsContentItems (String .valueOf (courseId ));
@@ -90,9 +91,11 @@ public String performDeepLinking(OidcIdToken ltiIdToken, String clientRegistrati
9091 }
9192
9293 /**
93- * Build an LTI deep linking response URL.
94+ * Creates the deep linking launch URL that includes encoded JWT and parameters required by the LTI platform .
9495 *
95- * @return The LTI deep link response URL.
96+ * @param clientRegistrationId Registration ID of the LTI client.
97+ * @param lti13DeepLinkingResponse Object holding the LTI claims and return URL.
98+ * @return Final URL to be sent back to the LTI platform for launching.
9699 */
97100 private String buildLtiDeepLinkResponse (String clientRegistrationId , Lti13DeepLinkingResponse lti13DeepLinkingResponse ) {
98101 UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder .fromUriString (this .artemisServerUrl + "/lti/select-content" );
@@ -110,28 +113,39 @@ private String buildLtiDeepLinkResponse(String clientRegistrationId, Lti13DeepLi
110113 }
111114
112115 /**
113- * Populate content items for deep linking response with exercises .
116+ * Maps each exercise ID to an individual LTI content item .
114117 */
115118 private List <LtiContentItem > populateExerciseContentItems (String courseId , Set <Long > exerciseIds ) {
116119 validateUnitIds (exerciseIds , DeepLinkingType .EXERCISE );
117120 return exerciseIds .stream ().map (exerciseId -> setExerciseContentItem (courseId , String .valueOf (exerciseId ))).toList ();
118121 }
119122
123+ /**
124+ * Groups a set of exercises into one content item.
125+ */
120126 private LtiContentItem populateGroupedExerciseContentItem (String courseId , Set <Long > exerciseIds ) {
121127 validateUnitIds (exerciseIds , DeepLinkingType .GROUPED_EXERCISE );
122128 return setGroupedExerciseContentItem (courseId , exerciseIds );
123129 }
124130
125131 /**
126- * Populate content items for deep linking response with lectures .
132+ * Maps each lecture ID to an individual LTI content item .
127133 */
128134 private List <LtiContentItem > populateLectureContentItems (String courseId , Set <Long > lectureIds ) {
129135 validateUnitIds (lectureIds , DeepLinkingType .LECTURE );
130136 return lectureIds .stream ().map (lectureId -> setLectureContentItem (courseId , String .valueOf (lectureId ))).toList ();
131137 }
132138
133139 /**
134- * Populate content items for deep linking response with competencies.
140+ * Groups a set of lectures into one content item.
141+ */
142+ private LtiContentItem populateGroupedLectureContentItems (String courseId , Set <Long > lectureIds ) {
143+ validateUnitIds (lectureIds , DeepLinkingType .GROUPED_LECTURE );
144+ return setGroupedLectureContentItem (courseId , lectureIds );
145+ }
146+
147+ /**
148+ * Prepares a content item pointing to the first available competency in the course.
135149 */
136150 private List <LtiContentItem > populateCompetencyContentItems (String courseId ) {
137151 Optional <Competency > competencyOpt = courseRepository .findWithEagerCompetenciesAndPrerequisitesById (Long .parseLong (courseId ))
@@ -142,7 +156,7 @@ private List<LtiContentItem> populateCompetencyContentItems(String courseId) {
142156 }
143157
144158 /**
145- * Populate content items for deep linking response with Iris .
159+ * Prepares a content item for launching the Iris analytics dashboard .
146160 */
147161 private List <LtiContentItem > populateIrisContentItems (String courseId ) {
148162 Optional <Course > courseOpt = courseRepository .findById (Long .parseLong (courseId ));
@@ -156,7 +170,7 @@ private List<LtiContentItem> populateIrisContentItems(String courseId) {
156170 }
157171
158172 /**
159- * Populate content items for deep linking response with learning paths .
173+ * Prepares a content item pointing to the learning path of the course .
160174 */
161175 private List <LtiContentItem > populateLearningPathsContentItems (String courseId ) {
162176 boolean hasLearningPaths = courseRepository .findWithEagerLearningPathsAndLearningPathCompetenciesByIdElseThrow (Long .parseLong (courseId )).getLearningPathsEnabled ();
@@ -170,7 +184,7 @@ private List<LtiContentItem> populateLearningPathsContentItems(String courseId)
170184 }
171185
172186 /**
173- * Set a content item for an exercise.
187+ * Create a content item for a specific exercise.
174188 */
175189 private LtiContentItem setExerciseContentItem (String courseId , String exerciseId ) {
176190 Optional <Exercise > exerciseOpt = exerciseRepository .findById (Long .valueOf (exerciseId ));
@@ -179,22 +193,22 @@ private LtiContentItem setExerciseContentItem(String courseId, String exerciseId
179193 .orElseThrow (() -> new BadRequestAlertException ("Exercise not found." , "LTI" , "exerciseNotFound" ));
180194 }
181195
196+ /**
197+ * Create a content item for a group of exercises.
198+ */
182199 private LtiContentItem setGroupedExerciseContentItem (String courseId , Set <Long > exerciseIds ) {
183- List <Exercise > exercises = new ArrayList <>();
184200
185- for (Long exerciseId : exerciseIds ) {
186- Optional <Exercise > exerciseOpt = exerciseRepository .findById (exerciseId );
187- exerciseOpt .ifPresent (exercises ::add );
188- }
201+ List <Exercise > exercises = exerciseRepository .findAllById (exerciseIds );
202+
189203 if (exercises .isEmpty ()) {
190204 throw new BadRequestAlertException ("No exercises found." , "LTI" , "exercisesNotFound" );
191205 }
192- String launchUrl = buildContentUrl (courseId , "groupedExercises" , exercises .stream ().map (Exercise ::getId ).map ( String :: valueOf ). collect (Collectors .joining ( "," )) );
206+ String launchUrl = buildGroupedResourceUrl (courseId , exercises .stream ().map (Exercise ::getId ).collect (Collectors .toSet ()), "exercises" , "exerciseIDs" , "noExerciseIds" );
193207 return createGroupedExerciseContentItem (exercises , launchUrl );
194208 }
195209
196210 /**
197- * Set a content item for a lecture.
211+ * Create a content item for a specific lecture.
198212 */
199213 private LtiContentItem setLectureContentItem (String courseId , String lectureId ) {
200214 String launchUrl = buildContentUrl (courseId , "lectures" , lectureId );
@@ -203,8 +217,19 @@ private LtiContentItem setLectureContentItem(String courseId, String lectureId)
203217 }
204218
205219 /**
206- * Create a content item for an exercise .
220+ * Create a content item for a group of lectures .
207221 */
222+ private LtiContentItem setGroupedLectureContentItem (String courseId , Set <Long > lectureIds ) {
223+
224+ List <Lecture > lectures = lectureRepository .findAllById (lectureIds );
225+
226+ if (lectures .isEmpty ()) {
227+ throw new BadRequestAlertException ("No lectures found." , "LTI" , "lecturesNotFound" );
228+ }
229+ String launchUrl = buildGroupedResourceUrl (courseId , lectures .stream ().map (Lecture ::getId ).collect (Collectors .toSet ()), "lectures" , "lectureIDs" , "noLectureIds" );
230+ return createGroupedLectureContentItem (launchUrl );
231+ }
232+
208233 private LtiContentItem createExerciseContentItem (Exercise exercise , String url ) {
209234 LineItem lineItem = exercise .getIncludedInOverallScore () != IncludedInOverallScore .NOT_INCLUDED ? new LineItem (DEFAULT_SCORE_MAXIMUM ) : null ;
210235 return new LtiContentItem ("ltiResourceLink" , exercise .getTitle (), url , lineItem );
@@ -217,13 +242,14 @@ private LtiContentItem createGroupedExerciseContentItem(List<Exercise> exercises
217242 return new LtiContentItem ("ltiResourceLink" , "Grouped Exercises" , url , lineItem );
218243 }
219244
220- /**
221- * Create a content item for a lecture.
222- */
223245 private LtiContentItem createLectureContentItem (Lecture lecture , String url ) {
224246 return new LtiContentItem ("ltiResourceLink" , lecture .getTitle (), url , null );
225247 }
226248
249+ private LtiContentItem createGroupedLectureContentItem (String url ) {
250+ return new LtiContentItem ("ltiResourceLink" , "Grouped Lectures" , url , null );
251+ }
252+
227253 /**
228254 * Create a content item for a single unit (e.g., competency, learning path, Iris).
229255 */
@@ -244,28 +270,22 @@ private void validateUnitIds(Set<Long> unitIds, DeepLinkingType type) {
244270 * Build a content URL for deep linking.
245271 */
246272 private String buildContentUrl (String courseId , String resourceType , String resourceId ) {
247- if ("groupedExercises" .equals (resourceType )) {
248- List <Long > exerciseIds = Arrays .stream (resourceId .split ("," )).map (String ::trim ).map (Long ::valueOf ).toList ();
249-
250- // Take the smallest exercise ID for the base URL to establish it as "starting" exercise for LTI view
251- Long smallestExerciseId = exerciseIds .stream ().min (Long ::compareTo ).orElseThrow (() -> new BadRequestAlertException ("No exercise IDs provided" , "LTI" , "noExerciseIds" ));
252-
253- String baseUrl = String .format ("%s/courses/%s/exercises/%d" , artemisServerUrl , courseId , smallestExerciseId );
254-
255- // Include all exercise IDs in the query parameter for sidebar content
256- UriComponentsBuilder uriBuilder = UriComponentsBuilder .fromUriString (baseUrl ).queryParam ("isMultiLaunch" , true ).queryParam ("exerciseIDs" , resourceId );
257-
258- return uriBuilder .toUriString ();
259- }
260- else {
261- return String .format ("%s/courses/%s/%s/%s" , artemisServerUrl , courseId , resourceType , resourceId );
262- }
273+ return String .format ("%s/courses/%s/%s/%s" , artemisServerUrl , courseId , resourceType , resourceId );
263274 }
264275
265276 private String buildContentUrl (String courseId , String resourceType ) {
266277 return String .format ("%s/courses/%s/%s" , artemisServerUrl , courseId , resourceType );
267278 }
268279
280+ private String buildGroupedResourceUrl (String courseId , Set <Long > ids , String pathSegment , String queryParamKey , String alertKey ) {
281+ Long smallestId = ids .stream ().min (Long ::compareTo ).orElseThrow (() -> new BadRequestAlertException ("No IDs provided" , "LTI" , alertKey ));
282+
283+ String baseUrl = String .format ("%s/courses/%s/%s/%d" , artemisServerUrl , courseId , pathSegment , smallestId );
284+ String joinedIds = ids .stream ().map (String ::valueOf ).collect (Collectors .joining ("," ));
285+
286+ return UriComponentsBuilder .fromUriString (baseUrl ).queryParam ("isMultiLaunch" , true ).queryParam (queryParamKey , joinedIds ).toUriString ();
287+ }
288+
269289 /**
270290 * Validate deep linking response settings.
271291 */
0 commit comments