CanvasService.java
package edu.ucsb.cs156.frontiers.services;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import edu.ucsb.cs156.frontiers.entities.Course;
import edu.ucsb.cs156.frontiers.entities.RosterStudent;
import edu.ucsb.cs156.frontiers.models.CanvasGroup;
import edu.ucsb.cs156.frontiers.models.CanvasGroupSet;
import edu.ucsb.cs156.frontiers.models.CanvasStudent;
import edu.ucsb.cs156.frontiers.utilities.CanonicalFormConverter;
import edu.ucsb.cs156.frontiers.validators.HasLinkedCanvasCourse;
import java.util.ArrayList;
import java.util.List;
import org.springframework.graphql.client.HttpSyncGraphQlClient;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.client.RestClient;
/**
* Service for interacting with the Canvas API.
*
* <p>Note that the Canvas API uses a GraphQL endpoint, which allows for more flexible queries
* compared to traditional REST APIs.
*
* <p>For more information on the Canvas API, visit the official documentation at
* https://canvas.instructure.com/doc/api/.
*
* <p>You can typically interact with Canvas API GraphQL endpoints interactively by appending
* /graphiql to the URL of the Canvas instance.
*
* <p>For example, for UCSB Canvas, use: https://ucsb.instructure.com/graphiql
*/
@Service
@Validated
public class CanvasService {
private HttpSyncGraphQlClient graphQlClient;
private ObjectMapper mapper;
private static final String CANVAS_GRAPHQL_URL = "https://ucsb.instructure.com/api/graphql";
public CanvasService(ObjectMapper mapper, RestClient.Builder builder) {
this.graphQlClient =
HttpSyncGraphQlClient.builder(builder.baseUrl(CANVAS_GRAPHQL_URL).build()).build();
this.mapper = mapper;
this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public List<CanvasGroupSet> getCanvasGroupSets(@HasLinkedCanvasCourse Course course) {
String query =
"""
query GetGroupSets($courseId: ID!) {
course(id: $courseId) {
groupSets {
_id
name
id
}
}
}
""";
HttpSyncGraphQlClient authedClient =
graphQlClient
.mutate()
.header("Authorization", "Bearer " + course.getCanvasApiToken())
.build();
List<CanvasGroupSet> groupSets =
authedClient
.document(query)
.variable("courseId", course.getCanvasCourseId())
.retrieveSync("course.groupSets")
.toEntityList(CanvasGroupSet.class);
return groupSets;
}
/**
* Fetches the roster of students from Canvas for the given course.
*
* @param course the Course entity containing canvasApiToken and canvasCourseId
* @return list of RosterStudent objects from Canvas
*/
public List<RosterStudent> getCanvasRoster(@HasLinkedCanvasCourse Course course) {
String query =
"""
query GetRoster($courseId: ID!) {
course(id: $courseId) {
usersConnection(filter: {enrollmentTypes: StudentEnrollment}) {
edges {
node {
firstName
lastName
sisId
email
integrationId
}
}
}
}
}
""";
HttpSyncGraphQlClient authedClient =
graphQlClient
.mutate()
.header("Authorization", "Bearer " + course.getCanvasApiToken())
.build();
List<CanvasStudent> students =
authedClient
.document(query)
.variable("courseId", course.getCanvasCourseId())
.retrieveSync("course.usersConnection.edges")
.toEntityList(JsonNode.class)
.stream()
.map(node -> mapper.convertValue(node.get("node"), CanvasStudent.class))
.toList();
return students.stream()
.map(
student ->
RosterStudent.builder()
.firstName(student.getFirstName())
.lastName(student.getLastName())
.studentId(student.getStudentId())
.email(student.getEmail())
.build())
.toList();
}
public List<CanvasGroup> getCanvasGroups(
@HasLinkedCanvasCourse Course course, String groupSetId) {
String query =
"""
query GetTeams($groupId: ID!) {
node(id: $groupId) {
... on GroupSet {
id
name
groups {
name
_id
membersConnection {
edges {
node {
user {
email
}
}
}
}
}
}
}
}
""";
HttpSyncGraphQlClient authedClient =
graphQlClient
.mutate()
.header("Authorization", "Bearer " + course.getCanvasApiToken())
.build();
List<JsonNode> groups =
authedClient
.document(query)
.variable("groupId", groupSetId)
.retrieveSync("node.groups")
.toEntityList(JsonNode.class);
List<CanvasGroup> parsedGroups =
groups.stream()
.map(
group -> {
CanvasGroup canvasGroup =
CanvasGroup.builder()
.name(group.get("name").asText())
.id(group.get("_id").asInt())
.members(new ArrayList<>())
.build();
group
.get("membersConnection")
.get("edges")
.forEach(
edge -> {
canvasGroup
.getMembers()
.add(
CanonicalFormConverter.convertToValidEmail(
edge.path("node").path("user").get("email").asText()));
});
return canvasGroup;
})
.toList();
return parsedGroups;
}
}