Java tutorial
/* * This file is part of Moodi application. * * Moodi application is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Moodi application is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Moodi application. If not, see <http://www.gnu.org/licenses/>. */ package fi.helsinki.moodi.integration.moodle; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import fi.helsinki.moodi.exception.IntegrationConnectionException; import fi.helsinki.moodi.exception.MoodiException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestTemplate; import java.io.IOException; import java.util.*; import java.util.function.Function; import static fi.helsinki.moodi.integration.moodle.MoodleClient.ResponseBodyEvaluator.Action.*; import static org.slf4j.LoggerFactory.getLogger; public class MoodleClient { private static final Logger logger = getLogger(MoodleClient.class); private final String baseUrl; private final RestTemplate restTemplate; private final RestTemplate readOnlyRestTemplate; private final ObjectMapper objectMapper; private final String wstoken; private static final String ENROLMENTS = "enrolments"; private static final String COURSEID = "courseid"; private static final String ROLEID = "roleid"; private static final String USERID = "userid"; private static final String SUSPEND = "suspend"; private static final String COURSES = "courses"; private static final String USERS = "users"; public MoodleClient(String baseUrl, String wstoken, ObjectMapper objectMapper, RestTemplate restTemplate, RestTemplate readOnlyRestTemplate) { this.baseUrl = baseUrl; this.wstoken = wstoken; this.objectMapper = objectMapper; this.restTemplate = restTemplate; this.readOnlyRestTemplate = readOnlyRestTemplate; } public List<MoodleFullCourse> getCourses(List<Long> ids) { final MultiValueMap<String, String> params = createParametersForFunction("core_course_get_courses"); setListParameters(params, "options[ids][%s]", ids, String::valueOf); try { return execute(params, new TypeReference<List<MoodleFullCourse>>() { }, DEFAULT_EVALUATION, true); } catch (Exception e) { return handleException("Error executing method: core_course_get_courses", e); } } private <T> void setListParameters(final MultiValueMap<String, String> params, final String paramTemplate, final Collection<T> values, final Function<T, String> toStringConverter) { final List<T> list = new ArrayList<>(values); for (int i = 0; i < list.size(); i++) { final String name = String.format(paramTemplate, i); final T value = list.get(i); params.set(name, toStringConverter.apply(value)); } } public long createCourse(final MoodleCourse course) { final MultiValueMap<String, String> params = createParametersForFunction("core_course_create_courses"); params.set(createParamName(COURSES, "idnumber", 0), course.idnumber); params.set(createParamName(COURSES, "fullname", 0), course.fullName); params.set(createParamName(COURSES, "shortname", 0), course.shortName); params.set(createParamName(COURSES, "categoryid", 0), course.categoryId); params.set(createParamName(COURSES, "summary", 0), course.summary); params.set(createParamName(COURSES, "visible", 0), booleanToIntString(course.visible)); params.set(createParamName(COURSES, "courseformatoptions", 0) + "[0][name]", "numsections"); params.set(createParamName(COURSES, "courseformatoptions", 0) + "[0][value]", String.valueOf(course.numberOfSections)); try { return execute(params, new TypeReference<List<MoodleCourseData>>() { }, DEFAULT_EVALUATION, false).stream().findFirst().map(s -> s.id).orElse(null); } catch (Exception e) { return handleException("Error executing method: importCourse", e); } } public void addEnrollments(final List<MoodleEnrollment> moodleEnrollments) { final MultiValueMap<String, String> params = createParametersForFunction("enrol_manual_enrol_users"); for (int i = 0; i < moodleEnrollments.size(); i++) { final MoodleEnrollment moodleEnrollment = moodleEnrollments.get(i); params.set(createParamName(ENROLMENTS, COURSEID, i), String.valueOf(moodleEnrollment.moodleCourseId)); params.set(createParamName(ENROLMENTS, ROLEID, i), String.valueOf(moodleEnrollment.moodleRoleId)); params.set(createParamName(ENROLMENTS, USERID, i), String.valueOf(moodleEnrollment.moodleUserId)); } try { execute(params, new TypeReference<Void>() { }, EMPTY_OK_RESPONSE_EVALUATION, false); } catch (Exception e) { handleException("Error executing method: addEnrollments", e); } } public void suspendEnrollments(final List<MoodleEnrollment> moodleEnrollments) { final MultiValueMap<String, String> params = createParametersForFunction("enrol_manual_enrol_users"); for (int i = 0; i < moodleEnrollments.size(); i++) { final MoodleEnrollment moodleEnrollment = moodleEnrollments.get(i); params.set(createParamName(ENROLMENTS, COURSEID, i), String.valueOf(moodleEnrollment.moodleCourseId)); params.set(createParamName(ENROLMENTS, ROLEID, i), String.valueOf(moodleEnrollment.moodleRoleId)); params.set(createParamName(ENROLMENTS, USERID, i), String.valueOf(moodleEnrollment.moodleUserId)); params.set(createParamName(ENROLMENTS, SUSPEND, i), "1"); } try { execute(params, new TypeReference<Void>() { }, EMPTY_OK_RESPONSE_EVALUATION, false); } catch (Exception e) { handleException("Error executing method: suspendEnrollments", e); } } @Cacheable(value = "moodle-client.moodle-user-by-username", unless = "#result == null") public MoodleUser getUser(final List<String> username) { final MultiValueMap<String, String> params = createParametersForFunction("core_user_get_users_by_field"); params.set("field", "username"); setListParameters(params, "values[%s]", username, String::valueOf); try { return execute(params, new TypeReference<List<MoodleUser>>() { }, DEFAULT_EVALUATION, true).stream().findFirst().orElse(null); } catch (Exception e) { return handleException("Error executing method: getUsers", e); } } // For testing purposes public long createUser(final String username, final String firstName, final String lastName, final String email, final String password, final String idNumber) { final MultiValueMap<String, String> params = createParametersForFunction("core_user_create_users"); params.set(createParamName(USERS, "username", 0), username); params.set(createParamName(USERS, "password", 0), password); params.set(createParamName(USERS, "firstname", 0), firstName); params.set(createParamName(USERS, "lastname", 0), lastName); params.set(createParamName(USERS, "email", 0), email); params.set(createParamName(USERS, "idnumber", 0), idNumber); try { return Long.valueOf(execute(params, new TypeReference<List<Map<String, String>>>() { }, DEFAULT_EVALUATION, false).get(0).get("id")); } catch (IOException e) { return handleException("Error executing method: createUser", e); } } public void deleteUser(final long moodleId) { final MultiValueMap<String, String> params = createParametersForFunction("core_user_delete_users"); params.set("userids[0]", String.valueOf(moodleId)); try { execute(params, new TypeReference<Void>() { }, DEFAULT_EVALUATION, false); } catch (IOException e) { handleException("Error executing method: deleteUser", e); } } public List<MoodleUserEnrollments> getEnrolledUsers(final long courseId) { final MultiValueMap<String, String> params = createParametersForFunction("core_enrol_get_enrolled_users"); params.set(COURSEID, String.valueOf(courseId)); try { return execute(params, new TypeReference<List<MoodleUserEnrollments>>() { }, DEFAULT_EVALUATION, true); } catch (Exception e) { return handleException("Error executing method: getUsers", e); } } public void addRoles(final List<MoodleEnrollment> moodleEnrollments) { assignRoles(moodleEnrollments, true); } public void removeRoles(final List<MoodleEnrollment> moodleEnrollments) { assignRoles(moodleEnrollments, false); } private void assignRoles(final List<MoodleEnrollment> moodleEnrollments, final boolean addition) { final String function = addition ? "core_role_assign_roles" : "core_role_unassign_roles"; final String array = addition ? "assignments" : "unassignments"; final MultiValueMap<String, String> params = createParametersForFunction(function); for (int i = 0; i < moodleEnrollments.size(); i++) { final MoodleEnrollment moodleEnrollment = moodleEnrollments.get(i); params.set(createParamName(array, USERID, i), String.valueOf(moodleEnrollment.moodleUserId)); params.set(createParamName(array, ROLEID, i), String.valueOf(moodleEnrollment.moodleRoleId)); params.set(createParamName(array, "instanceid", i), String.valueOf(moodleEnrollment.moodleCourseId)); params.set(createParamName(array, "contextlevel", i), "course"); } try { execute(params, null, EMPTY_OK_RESPONSE_EVALUATION, false); } catch (Exception e) { handleException("Error executing method: assignRoles", e); } } private <T> T handleException(final String message, final Exception e) { if (e instanceof MoodiException) { throw (MoodiException) e; } else { throw new MoodleClientException(message, e); } } private String booleanToIntString(final boolean b) { return b ? "1" : "0"; } private MultiValueMap<String, String> createParametersForFunction(final String function) { final MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.set("wstoken", wstoken); params.set("wsfunction", function); params.set("moodlewsrestformat", "json"); return params; } private String createParamName(String param1, String param2, int i) { // "enrolments", "courseid", 0 -> "enrolments[0][courseid]" return String.format("%s[%d][%s]", param1, i, param2); } private HttpHeaders createHeaders() { final HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); return headers; } private RestTemplate getRestTemplate(final boolean readOnly) { return readOnly ? readOnlyRestTemplate : restTemplate; } private <T> T execute(final MultiValueMap<String, String> params, final TypeReference<T> typeReference, final ResponseBodyEvaluator responseBodyEvaluator, final boolean readOnly) throws IOException { logger.info("Invoke url: {} with params: {}", baseUrl, paramsToString(params)); final String body = getRestTemplate(readOnly).postForObject(baseUrl, new HttpEntity<>(params, createHeaders()), String.class); logger.debug("Got response body:\n{}", body); switch (responseBodyEvaluator.evaluate(body)) { case CONTINUE: break; case ERROR: throw createMoodleClientException(body); case RETURN_NULL: return null; case RETURN_BODY: return (T) body; default: } try { return objectMapper.readValue(body, typeReference); } catch (ResourceAccessException e) { throw new IntegrationConnectionException("Moodle connection failure", e); } catch (Exception e) { throw createMoodleClientException(body); } } private MoodleClientException createMoodleClientException(final String body) throws IOException { logger.error("Got unexpected response body " + body); final Map<String, String> map = objectMapper.readValue(body, Map.class); return new MoodleClientException(map.get("message"), map.get("exception"), map.get("errorcode")); } @FunctionalInterface protected interface ResponseBodyEvaluator { enum Action { CONTINUE, RETURN_BODY, RETURN_NULL, ERROR } Action evaluate(String responseBody); } private static ResponseBodyEvaluator DEFAULT_EVALUATION = s -> CONTINUE; private static ResponseBodyEvaluator EMPTY_OK_RESPONSE_EVALUATION = s -> StringUtils.isEmpty(s) || "null".equals(s) ? RETURN_NULL : ERROR; private static String paramsToString(final MultiValueMap<String, String> params) { final StringBuilder sb = new StringBuilder(); for (final String name : params.keySet()) { sb.append(name).append(": ").append(params.get(name)); } return sb.toString(); } }