BrightspaceClientImpl.java

/*
 * Licensed to The Apereo Foundation under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 *
 * The Apereo Foundation licenses this file to you under the Educational
 * Community License, Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of the License
 * at:
 *
 *   http://opensource.org/licenses/ecl2.txt
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package org.opencastproject.userdirectory.brightspace.client;

import org.opencastproject.userdirectory.brightspace.client.api.BrightspaceUser;
import org.opencastproject.userdirectory.brightspace.client.api.OrgUnitItem;
import org.opencastproject.userdirectory.brightspace.client.api.OrgUnitResponse;
import org.opencastproject.userdirectory.brightspace.client.api.PagingInfo;
import org.opencastproject.userdirectory.brightspace.client.api.UsersResponse;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.net.ssl.HttpsURLConnection;

import be.ugent.brightspace.idkeyauth.AuthenticationSecurityFactory;
import be.ugent.brightspace.idkeyauth.ID2LAppContext;
import be.ugent.brightspace.idkeyauth.ID2LUserContext;

public class BrightspaceClientImpl implements BrightspaceClient {

  private static final Logger logger = LoggerFactory.getLogger(BrightspaceClientImpl.class);

  private static final String GET_USER_BY_USERNAME = "/d2l/api/lp/1.31/users/?UserName=";
  private static final String GET_ALL_USERS = "/d2l/api/lp/1.31/users/";
  private static final String GET_COURSES_BY_BRIGHTSPACE_USER_ID
      = "/d2l/api/lp/1.31/enrollments/users/{brightspace-userid}/orgUnits/?orgUnitTypeId=3";
  private static final String UNEXPECTED_JSON_RESPONSE = "The brightspace API returned a unexpected json response";
  private static final String SUPER_ADMIN = "Super Administrator";
  private static final String LTI_LEARNER_ROLE = "Learner";
  private static final String LTI_INSTRUCTOR_ROLE = "Instructor";

  private final String url;
  private final String applicationId;
  private final String applicationKey;
  private final String systemUserId;
  private final String systemUserKey;
  private final ID2LUserContext userContext;

  private final ObjectMapper objectMapper = new ObjectMapper();

  public BrightspaceClientImpl(String url, String applicationId, String applicationKey, String systemUserId,
          String systemUserKey) {
    this.url = url;
    this.applicationId = applicationId;
    this.applicationKey = applicationKey;
    this.systemUserId = systemUserId;
    this.systemUserKey = systemUserKey;
    userContext = createUserContext();
  }

  public BrightspaceUser findUser(String userName) throws BrightspaceClientException {
    String request = GET_USER_BY_USERNAME + userName;

    try {
      String response = httpGetRequest(request);
      logger.debug(response);

      BrightspaceUser brightspaceUser = objectMapper
              .readerFor(BrightspaceUser.class)
              .readValue(response);
      return brightspaceUser;
    } catch (BrightspaceNotFoundException nfe) {
      return null;
    } catch (IOException e) {
      logger.debug(e.toString());
      throw new BrightspaceClientException(UNEXPECTED_JSON_RESPONSE, e);
    }
  }

  @Override public List<String> getRolesFromBrightspace(String userid, Set<String> instructorRoles)
                            throws BrightspaceClientException {
    logger.debug("Retrieving subscribed courses for user: {}", userid);

    boolean hasMoreItems;
    String bookmark = null;
    List<String> roleList = new ArrayList<>();
    try {
      do {
        String request = composePagedUrl(bookmark, userid);
        OrgUnitResponse orgUnitPage = findOrgUnitPage(request);
        if (SUPER_ADMIN.equals(orgUnitPage.getItems().get(0).getRole().getName())) {
          roleList.add("BRIGHTSPACE_ADMIN");
          return roleList;
        }

        for (OrgUnitItem course: orgUnitPage.getItems()) {
          String brightspaceRole = course.getRole().getName();
          String ltiRole = instructorRoles.contains(brightspaceRole) ? LTI_INSTRUCTOR_ROLE : LTI_LEARNER_ROLE;
          String opencastRole = String.format("%s_%s", course.getOrgUnit().getId(), ltiRole);
          roleList.add(opencastRole);
        }

        PagingInfo pagingInfo = orgUnitPage.getPagingInfo();
        hasMoreItems = pagingInfo.hasMoreItems();
        bookmark = pagingInfo.getBookmark();

      } while (hasMoreItems);

      return roleList;
    } catch (BrightspaceClientException e) {
      logger.warn("Exception getting site/role membership for brightspace user {}", userid, e);
      throw new BrightspaceClientException(UNEXPECTED_JSON_RESPONSE, e);
    }

  }

  @Override
  public List<BrightspaceUser> findAllUsers() throws BrightspaceClientException {
    try {
      String response = httpGetRequest(GET_ALL_USERS);
      UsersResponse usersResponse = objectMapper.readValue(response, UsersResponse.class);
      return usersResponse.getItems();
    } catch (IOException e) {
      throw new BrightspaceClientException(UNEXPECTED_JSON_RESPONSE, e);
    } catch (BrightspaceNotFoundException nfe) {
      throw new BrightspaceClientException(UNEXPECTED_JSON_RESPONSE, nfe);
    }

  }

  public String getURL() {
    return this.url;
  }

  private String httpGetRequest(String request) throws BrightspaceClientException, BrightspaceNotFoundException {
    URL url = createUrl(request);

    try {
      HttpsURLConnection urlConnection = (HttpsURLConnection) getURLConnection(url);

      if (urlConnection.getResponseCode() == 404) {
        logger.debug("Not found, 404 response");
        throw new BrightspaceNotFoundException("not found");
      }

      InputStream inputStream = urlConnection.getInputStream();
      return readInputStream(inputStream);
    } catch (IOException io) {
      logger.warn("error in brightspace data fetching", io);
      throw new BrightspaceClientException("could not read response");
    }
  }

  private URL createUrl(String request) throws BrightspaceClientException {
    URI uri = userContext.createAuthenticatedUri(request, "GET");
    URL url;
    try {
      url = uri.toURL();
    } catch (MalformedURLException mue) {
      throw new BrightspaceClientException("url was malformed", mue);
    }
    logger.debug("about to make GET request to : {}", uri);
    return url;
  }

  private ID2LUserContext createUserContext() {
    ID2LAppContext securityContext = AuthenticationSecurityFactory
            .createSecurityContext(applicationId, applicationKey, url);
    return securityContext.createUserContext(systemUserId, systemUserKey);
  }

  private URLConnection getURLConnection(URL url) throws BrightspaceClientException {
    try {
      HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
      urlConnection.setRequestMethod("GET");
      urlConnection.setRequestProperty("Content-Type", "application/json");
      urlConnection.setRequestProperty("Accept-Charset", "utf-8");
      urlConnection.setDoInput(true);
      urlConnection.setDoOutput(true);
      return urlConnection;
    } catch (IOException ioe) {
      throw new BrightspaceClientException("Brightspace api unreachable", ioe);
    }
  }

  private String readInputStream(InputStream inputStream) throws BrightspaceClientException {
    StringBuilder content = new StringBuilder();
    try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
      String inputline;
      while ((inputline = bufferedReader.readLine()) != null) {
        content.append(inputline);
      }
      logger.debug("call to brightspace api: {}", content);
      return content.toString();
    } catch (IOException io) {
      throw new BrightspaceClientException("Could not read response", io);
    }

  }

  private OrgUnitResponse findOrgUnitPage(String request) throws BrightspaceClientException {
    try {
      String response = httpGetRequest(request);
      return objectMapper.readValue(response, OrgUnitResponse.class);
    } catch (IOException e) {
      logger.error(UNEXPECTED_JSON_RESPONSE);
      throw new BrightspaceClientException(UNEXPECTED_JSON_RESPONSE, e);
    } catch (BrightspaceNotFoundException nfe) {
      logger.error(UNEXPECTED_JSON_RESPONSE);
      throw new BrightspaceClientException(UNEXPECTED_JSON_RESPONSE, nfe);
    }
  }

  private String composePagedUrl(String bookmark, String brightspaceUserId) {
    String request = GET_COURSES_BY_BRIGHTSPACE_USER_ID.replaceAll("\\{\\S+}", brightspaceUserId);
    if (bookmark != null) {
      request += "&bookmark=" + bookmark;
    }
    return request;
  }
}