| 1 | package edu.ucsb.cs156.frontiers.services; | |
| 2 | ||
| 3 | import com.fasterxml.jackson.databind.DeserializationFeature; | |
| 4 | import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | import edu.ucsb.cs156.frontiers.entities.Course; | |
| 7 | import edu.ucsb.cs156.frontiers.entities.RosterStudent; | |
| 8 | import edu.ucsb.cs156.frontiers.models.CanvasGroup; | |
| 9 | import edu.ucsb.cs156.frontiers.models.CanvasGroupSet; | |
| 10 | import edu.ucsb.cs156.frontiers.models.CanvasStudent; | |
| 11 | import edu.ucsb.cs156.frontiers.utilities.CanonicalFormConverter; | |
| 12 | import edu.ucsb.cs156.frontiers.validators.HasLinkedCanvasCourse; | |
| 13 | import java.util.ArrayList; | |
| 14 | import java.util.List; | |
| 15 | import org.springframework.graphql.client.HttpSyncGraphQlClient; | |
| 16 | import org.springframework.stereotype.Service; | |
| 17 | import org.springframework.validation.annotation.Validated; | |
| 18 | import org.springframework.web.client.RestClient; | |
| 19 | ||
| 20 | /** | |
| 21 | * Service for interacting with the Canvas API. | |
| 22 | * | |
| 23 | * <p>Note that the Canvas API uses a GraphQL endpoint, which allows for more flexible queries | |
| 24 | * compared to traditional REST APIs. | |
| 25 | * | |
| 26 | * <p>For more information on the Canvas API, visit the official documentation at <a | |
| 27 | * href="https://canvas.instructure.com/doc/api/">...</a>. | |
| 28 | * | |
| 29 | * <p>You can typically interact with Canvas API GraphQL endpoints interactively by appending | |
| 30 | * /graphiql to the URL of the Canvas instance. | |
| 31 | * | |
| 32 | * <p>For example, for UCSB Canvas, use: <a href="https://ucsb.instructure.com/graphiql">...</a> | |
| 33 | */ | |
| 34 | @Service | |
| 35 | @Validated | |
| 36 | public class CanvasService { | |
| 37 | ||
| 38 | private HttpSyncGraphQlClient graphQlClient; | |
| 39 | private ObjectMapper mapper; | |
| 40 | private TokenEncryptionService tokenEncryptionService; | |
| 41 | ||
| 42 | private static final String CANVAS_GRAPHQL_URL = "https://ucsb.instructure.com/api/graphql"; | |
| 43 | ||
| 44 | public CanvasService( | |
| 45 | ObjectMapper mapper, | |
| 46 | RestClient.Builder builder, | |
| 47 | TokenEncryptionService tokenEncryptionService) { | |
| 48 | this.graphQlClient = | |
| 49 | HttpSyncGraphQlClient.builder(builder.baseUrl(CANVAS_GRAPHQL_URL).build()).build(); | |
| 50 | this.mapper = mapper; | |
| 51 | this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |
| 52 | this.tokenEncryptionService = tokenEncryptionService; | |
| 53 | } | |
| 54 | ||
| 55 | public List<CanvasGroupSet> getCanvasGroupSets(@HasLinkedCanvasCourse Course course) { | |
| 56 | // language=GraphQL | |
| 57 | String query = | |
| 58 | """ | |
| 59 | query GetGroupSets($courseId: ID!) { | |
| 60 | course(id: $courseId) { | |
| 61 | groupSets { | |
| 62 | _id | |
| 63 | name | |
| 64 | id | |
| 65 | } | |
| 66 | } | |
| 67 | } | |
| 68 | """; | |
| 69 | ||
| 70 | HttpSyncGraphQlClient authedClient = | |
| 71 | graphQlClient | |
| 72 | .mutate() | |
| 73 | .header( | |
| 74 | "Authorization", | |
| 75 | "Bearer " + tokenEncryptionService.decryptToken(course.getCanvasApiToken())) | |
| 76 | .build(); | |
| 77 | ||
| 78 | List<CanvasGroupSet> groupSets = | |
| 79 | authedClient | |
| 80 | .document(query) | |
| 81 | .variable("courseId", course.getCanvasCourseId()) | |
| 82 | .retrieveSync("course.groupSets") | |
| 83 | .toEntityList(CanvasGroupSet.class); | |
| 84 |
1
1. getCanvasGroupSets : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroupSets → KILLED |
return groupSets; |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Fetches the roster of students from Canvas for the given course. | |
| 89 | * | |
| 90 | * @param course the Course entity containing canvasApiToken and canvasCourseId | |
| 91 | * @return list of RosterStudent objects from Canvas | |
| 92 | */ | |
| 93 | public List<RosterStudent> getCanvasRoster(@HasLinkedCanvasCourse Course course) { | |
| 94 | ||
| 95 | // language=GraphQL | |
| 96 | String query = | |
| 97 | """ | |
| 98 | query GetRoster($courseId: ID!) { | |
| 99 | course(id: $courseId) { | |
| 100 | usersConnection(filter: {enrollmentTypes: StudentEnrollment}) { | |
| 101 | edges { | |
| 102 | node { | |
| 103 | firstName | |
| 104 | lastName | |
| 105 | sisId | |
| 106 | email | |
| 107 | integrationId | |
| 108 | } | |
| 109 | } | |
| 110 | } | |
| 111 | } | |
| 112 | } | |
| 113 | """; | |
| 114 | ||
| 115 | HttpSyncGraphQlClient authedClient = | |
| 116 | graphQlClient | |
| 117 | .mutate() | |
| 118 | .header( | |
| 119 | "Authorization", | |
| 120 | "Bearer " + tokenEncryptionService.decryptToken(course.getCanvasApiToken())) | |
| 121 | .build(); | |
| 122 | ||
| 123 | List<CanvasStudent> students = | |
| 124 | authedClient | |
| 125 | .document(query) | |
| 126 | .variable("courseId", course.getCanvasCourseId()) | |
| 127 | .retrieveSync("course.usersConnection.edges") | |
| 128 | .toEntityList(JsonNode.class) | |
| 129 | .stream() | |
| 130 |
1
1. lambda$getCanvasRoster$0 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$0 → KILLED |
.map(node -> mapper.convertValue(node.get("node"), CanvasStudent.class)) |
| 131 | .toList(); | |
| 132 | ||
| 133 |
1
1. getCanvasRoster : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasRoster → KILLED |
return students.stream() |
| 134 | .map( | |
| 135 | student -> | |
| 136 |
1
1. lambda$getCanvasRoster$1 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasRoster$1 → KILLED |
RosterStudent.builder() |
| 137 | .firstName(student.getFirstName()) | |
| 138 | .lastName(student.getLastName()) | |
| 139 | .studentId(student.getStudentId()) | |
| 140 | .email(student.getEmail()) | |
| 141 | .build()) | |
| 142 | .toList(); | |
| 143 | } | |
| 144 | ||
| 145 | public List<CanvasGroup> getCanvasGroups( | |
| 146 | @HasLinkedCanvasCourse Course course, String groupSetId) { | |
| 147 | // language=GraphQL | |
| 148 | String query = | |
| 149 | """ | |
| 150 | query GetTeams($groupId: ID!) { | |
| 151 | node(id: $groupId) { | |
| 152 | ... on GroupSet { | |
| 153 | id | |
| 154 | name | |
| 155 | groups { | |
| 156 | name | |
| 157 | _id | |
| 158 | membersConnection { | |
| 159 | edges { | |
| 160 | node { | |
| 161 | user { | |
| 162 | email | |
| 163 | } | |
| 164 | } | |
| 165 | } | |
| 166 | } | |
| 167 | } | |
| 168 | } | |
| 169 | } | |
| 170 | } | |
| 171 | """; | |
| 172 | ||
| 173 | HttpSyncGraphQlClient authedClient = | |
| 174 | graphQlClient | |
| 175 | .mutate() | |
| 176 | .header( | |
| 177 | "Authorization", | |
| 178 | "Bearer " + tokenEncryptionService.decryptToken(course.getCanvasApiToken())) | |
| 179 | .build(); | |
| 180 | ||
| 181 | List<JsonNode> groups = | |
| 182 | authedClient | |
| 183 | .document(query) | |
| 184 | .variable("groupId", groupSetId) | |
| 185 | .retrieveSync("node.groups") | |
| 186 | .toEntityList(JsonNode.class); | |
| 187 | ||
| 188 | List<CanvasGroup> parsedGroups = | |
| 189 | groups.stream() | |
| 190 | .map( | |
| 191 | group -> { | |
| 192 | CanvasGroup canvasGroup = | |
| 193 | CanvasGroup.builder() | |
| 194 | .name(group.get("name").asText()) | |
| 195 | .id(group.get("_id").asInt()) | |
| 196 | .members(new ArrayList<>()) | |
| 197 | .build(); | |
| 198 | group | |
| 199 | .get("membersConnection") | |
| 200 | .get("edges") | |
| 201 |
1
1. lambda$getCanvasGroups$3 : removed call to com/fasterxml/jackson/databind/JsonNode::forEach → KILLED |
.forEach( |
| 202 | edge -> { | |
| 203 | canvasGroup | |
| 204 | .getMembers() | |
| 205 | .add( | |
| 206 | CanonicalFormConverter.convertToValidEmail( | |
| 207 | edge.path("node").path("user").get("email").asText())); | |
| 208 | }); | |
| 209 |
1
1. lambda$getCanvasGroups$3 : replaced return value with null for edu/ucsb/cs156/frontiers/services/CanvasService::lambda$getCanvasGroups$3 → KILLED |
return canvasGroup; |
| 210 | }) | |
| 211 | .toList(); | |
| 212 | ||
| 213 |
1
1. getCanvasGroups : replaced return value with Collections.emptyList for edu/ucsb/cs156/frontiers/services/CanvasService::getCanvasGroups → KILLED |
return parsedGroups; |
| 214 | } | |
| 215 | } | |
Mutations | ||
| 84 |
1.1 |
|
| 130 |
1.1 |
|
| 133 |
1.1 |
|
| 136 |
1.1 |
|
| 201 |
1.1 |
|
| 209 |
1.1 |
|
| 213 |
1.1 |