private final void updateSiteDetailsIfChanged(
      Site site, String context_title, String context_label) {

    boolean changed = false;

    if (BasicLTIUtil.isNotBlank(context_title) && !context_title.equals(site.getTitle())) {
      site.setTitle(context_title);
      changed = true;
    }

    if (BasicLTIUtil.isNotBlank(context_label)
        && !context_label.equals(site.getShortDescription())) {
      site.setShortDescription(context_label);
      changed = true;
    }

    if (changed) {
      try {
        SiteService.save(site);
        M_log.info(
            "Updated  site="
                + site.getId()
                + " title="
                + context_title
                + " label="
                + context_label);
      } catch (Exception e) {
        M_log.warn("Failed to update site title and/or label");
      }
    }
  }
  protected void validate(Map payload, boolean isTrustedConsumer) throws LTIException {
    // check parameters
    String lti_message_type = (String) payload.get(BasicLTIConstants.LTI_MESSAGE_TYPE);
    String lti_version = (String) payload.get(BasicLTIConstants.LTI_VERSION);
    String oauth_consumer_key = (String) payload.get("oauth_consumer_key");
    String resource_link_id = (String) payload.get(BasicLTIConstants.RESOURCE_LINK_ID);
    String user_id = (String) payload.get(BasicLTIConstants.USER_ID);
    String context_id = (String) payload.get(BasicLTIConstants.CONTEXT_ID);

    boolean launch = true;
    if (BasicLTIUtil.equals(lti_message_type, "basic-lti-launch-request")) {
      launch = true;
    } else if (BasicLTIUtil.equals(lti_message_type, "ContentItemSelectionRequest")) {
      launch = false;
    } else {
      throw new LTIException("launch.invalid", "lti_message_type=" + lti_message_type, null);
    }

    if (!BasicLTIUtil.equals(lti_version, "LTI-1p0")) {
      throw new LTIException("launch.invalid", "lti_version=" + lti_version, null);
    }

    if (BasicLTIUtil.isBlank(oauth_consumer_key)) {
      throw new LTIException("launch.missing", "oauth_consumer_key", null);
    }

    if (launch && BasicLTIUtil.isBlank(resource_link_id)) {
      throw new LTIException("launch.missing", "resource_link_id", null);
    }

    if (BasicLTIUtil.isBlank(user_id)) {
      throw new LTIException("launch.missing", "user_id", null);
    }
    if (M_log.isDebugEnabled()) {
      M_log.debug("user_id=" + user_id);
    }

    // check tool_id
    String tool_id = (String) payload.get("tool_id");
    if (tool_id == null) {
      throw new LTIException("launch.tool_id.required", null, null);
    }

    // Trim off the leading slash and any trailing space
    tool_id = tool_id.substring(1).trim();
    if (M_log.isDebugEnabled()) {
      M_log.debug("tool_id=" + tool_id);
    }
    // store modified tool_id back in payload
    payload.put("tool_id", tool_id);
    final String allowedToolsConfig =
        ServerConfigurationService.getString("basiclti.provider.allowedtools", "");

    final String[] allowedTools = allowedToolsConfig.split(":");
    final List<String> allowedToolsList = Arrays.asList(allowedTools);

    if (launch && allowedTools != null && !allowedToolsList.contains(tool_id)) {
      throw new LTIException("launch.tool.notallowed", tool_id, null);
    }
    final Tool toolCheck = ToolManager.getTool(tool_id);
    if (launch && toolCheck == null) {
      throw new LTIException("launch.tool.notfound", tool_id, null);
    }

    // Check for the ext_sakai_provider_eid param. If set, this will contain the eid that we are to
    // use
    // in place of using the user_id parameter
    // WE still need that parameter though, so translate it from the given eid.
    boolean useProvidedEid = false;
    String ext_sakai_provider_eid = (String) payload.get(BasicLTIConstants.EXT_SAKAI_PROVIDER_EID);
    if (BasicLTIUtil.isNotBlank(ext_sakai_provider_eid)) {
      useProvidedEid = true;
      try {
        user_id = UserDirectoryService.getUserId(ext_sakai_provider_eid);
      } catch (Exception e) {
        M_log.error(e.getLocalizedMessage(), e);
        throw new LTIException(
            "launch.provided.eid.invalid", "ext_sakai_provider_eid=" + ext_sakai_provider_eid, e);
      }
    }

    if (M_log.isDebugEnabled()) {
      M_log.debug("ext_sakai_provider_eid=" + ext_sakai_provider_eid);
    }

    // Contextualize the context_id with the OAuth consumer key
    // Also use the resource_link_id for the context_id if we did not get a context_id
    // BLTI-31: if trusted, context_id is required and use the param without modification
    if (BasicLTIUtil.isBlank(context_id)) {
      if (isTrustedConsumer) {
        throw new LTIException("launch.missing", context_id, null);
      } else {
        context_id = "res:" + resource_link_id;
        payload.put(BasicLTIConstants.CONTEXT_ID, context_id);
      }
    }

    // Check if context_id is simply a ~. If so, get the id of that user's My Workspace site
    // and use that to construct the full context_id
    if (BasicLTIUtil.equals(context_id, "~")) {
      if (useProvidedEid) {
        String userSiteId = null;
        try {
          userSiteId = SiteService.getUserSiteId(user_id);
        } catch (Exception e) {
          M_log.warn("Failed to get My Workspace site for user_id:" + user_id);
          M_log.error(e.getLocalizedMessage(), e);
          throw new LTIException("launch.user.site.unknown", "user_id=" + user_id, e);
        }
        context_id = userSiteId;
        payload.put(BasicLTIConstants.CONTEXT_ID, context_id);
      }
    }

    if (M_log.isDebugEnabled()) {
      M_log.debug("context_id=" + context_id);
    }

    // Lookup the secret
    final String configPrefix = "basiclti.provider." + oauth_consumer_key + ".";
    final String oauth_secret = ServerConfigurationService.getString(configPrefix + "secret", null);
    if (oauth_secret == null) {
      throw new LTIException("launch.key.notfound", oauth_consumer_key, null);
    }
    final OAuthMessage oam = (OAuthMessage) payload.get("oauth_message");

    final String forcedURIScheme =
        ServerConfigurationService.getString("basiclti.provider.forcedurischeme", null);

    if (forcedURIScheme != null) {
      try {
        URI testURI = new URI(oam.URL);
        URI newURI = new URI(forcedURIScheme, testURI.getSchemeSpecificPart(), null);
        oam.URL = newURI.toString();
      } catch (URISyntaxException use) {
      }
    }
    final OAuthValidator oav = new SimpleOAuthValidator();
    final OAuthConsumer cons =
        new OAuthConsumer(
            "about:blank#OAuth+CallBack+NotUsed", oauth_consumer_key, oauth_secret, null);

    final OAuthAccessor acc = new OAuthAccessor(cons);

    String base_string = null;
    try {
      base_string = OAuthSignatureMethod.getBaseString(oam);
    } catch (Exception e) {
      M_log.error(e.getLocalizedMessage(), e);
      base_string = null;
    }

    try {
      oav.validateMessage(oam, acc);
    } catch (Exception e) {
      M_log.warn("Provider failed to validate message");
      M_log.warn(e.getLocalizedMessage(), e);
      if (base_string != null) {
        M_log.warn(base_string);
      }
      throw new LTIException("launch.no.validate", context_id, e);
    }

    final Session sess = SessionManager.getCurrentSession();

    if (sess == null) {
      throw new LTIException("launch.no.session", context_id, null);
    }
  }
  protected Site findOrCreateSite(Map payload, boolean trustedConsumer) throws LTIException {

    String context_id = (String) payload.get(BasicLTIConstants.CONTEXT_ID);
    String oauth_consumer_key = (String) payload.get("oauth_consumer_key");
    String siteId = null;

    if (trustedConsumer) {
      siteId = context_id;
    } else {
      siteId = LegacyShaUtil.sha1Hash(oauth_consumer_key + ":" + context_id);
    }
    if (M_log.isDebugEnabled()) {
      M_log.debug("siteId=" + siteId);
    }

    final String context_title = (String) payload.get(BasicLTIConstants.CONTEXT_TITLE);
    final String context_label = (String) payload.get(BasicLTIConstants.CONTEXT_LABEL);

    Site site = null;

    // Get the site if it exists
    if (ServerConfigurationService.getBoolean(
        "basiclti.provider.lookupSitesByLTIContextIdProperty", false)) {
      try {
        site = findSiteByLTIContextId(context_id);
        if (site != null) {
          updateSiteDetailsIfChanged(site, context_title, context_label);
          return site;
        }
      } catch (Exception e) {
        if (M_log.isDebugEnabled()) {
          M_log.debug(e.getLocalizedMessage(), e);
        }
      }
    } else {
      try {
        site = SiteService.getSite(siteId);
        updateSiteDetailsIfChanged(site, context_title, context_label);
        return site;
      } catch (Exception e) {
        if (M_log.isDebugEnabled()) {
          M_log.debug(e.getLocalizedMessage(), e);
        }
      }
    }

    // If trusted and site does not exist, error, otherwise, create the site
    if (trustedConsumer) {
      throw new LTIException("launch.site.invalid", "siteId=" + siteId, null);
    } else {

      pushAdvisor();
      try {
        String sakai_type = "project";

        // BLTI-154. If an autocreation site template has been specced in sakai.properties, use it.
        String autoSiteTemplateId =
            ServerConfigurationService.getString("basiclti.provider.autositetemplate", null);

        boolean templateSiteExists = SiteService.siteExists(autoSiteTemplateId);

        if (!templateSiteExists) {
          M_log.warn(
              "A template site id was specced ("
                  + autoSiteTemplateId
                  + ") but no site with this id exists. A default lti site will be created instead.");
        }

        if (autoSiteTemplateId == null || !templateSiteExists) {
          // BLTI-151 If the new site type has been specified in sakai.properties, use it.
          sakai_type = ServerConfigurationService.getString("basiclti.provider.newsitetype", null);
          if (BasicLTIUtil.isBlank(sakai_type)) {
            // It wasn't specced in the props. Test for the ims course context type.
            final String context_type = (String) payload.get(BasicLTIConstants.CONTEXT_TYPE);
            if (BasicLTIUtil.equalsIgnoreCase(context_type, "course")) {
              sakai_type = "course";
            } else {
              sakai_type = BasicLTIConstants.NEW_SITE_TYPE;
            }
          }
          site = SiteService.addSite(siteId, sakai_type);
          site.setType(sakai_type);
        } else {
          Site autoSiteTemplate = SiteService.getSite(autoSiteTemplateId);
          site = SiteService.addSite(siteId, autoSiteTemplate);
        }

        if (BasicLTIUtil.isNotBlank(context_title)) {
          site.setTitle(context_title);
        }
        if (BasicLTIUtil.isNotBlank(context_label)) {
          site.setShortDescription(context_label);
        }
        site.setJoinable(false);
        site.setPublished(true);
        site.setPubView(false);
        // record the original context_id to a site property
        site.getPropertiesEdit().addProperty(LTI_CONTEXT_ID, context_id);

        try {
          SiteService.save(site);
          M_log.info(
              "Created  site="
                  + siteId
                  + " label="
                  + context_label
                  + " type="
                  + sakai_type
                  + " title="
                  + context_title);

        } catch (Exception e) {
          throw new LTIException("launch.site.save", "siteId=" + siteId, e);
        }

      } catch (Exception e) {
        throw new LTIException("launch.create.site", "siteId=" + siteId, e);
      } finally {
        popAdvisor();
      }
    }

    try {
      return SiteService.getSite(site.getId());
    } catch (IdUnusedException e) {
      throw new LTIException("launch.site.invalid", "siteId=" + siteId, e);
    }
  }
  private void syncSiteMembershipsOnceThenSchedule(
      Map payload, Site site, boolean isTrustedConsumer, boolean isEmailTrustedConsumer)
      throws LTIException {

    if (isTrustedConsumer) return;

    M_log.debug("synchSiteMembershipsOnceThenSchedule");

    if (!ServerConfigurationService.getBoolean(SakaiBLTIUtil.INCOMING_ROSTER_ENABLED, false)) {
      M_log.info("LTI Memberships synchronization disabled.");
      return;
    }

    final String membershipsUrl = (String) payload.get("ext_ims_lis_memberships_url");

    if (!BasicLTIUtil.isNotBlank(membershipsUrl)) {
      M_log.info("LTI Memberships extension is not supported.");
      return;
    }

    if (M_log.isDebugEnabled()) M_log.debug("Memberships URL: " + membershipsUrl);

    final String membershipsId = (String) payload.get("ext_ims_lis_memberships_id");

    if (!BasicLTIUtil.isNotBlank(membershipsId)) {
      M_log.info("No memberships id supplied. Memberships will NOT be synchronized.");
      return;
    }

    final String siteId = site.getId();

    // If this site has already been scheduled, then we do nothing.
    if (ltiService.getMembershipsJob(siteId) != null) {
      if (M_log.isDebugEnabled()) {
        M_log.debug(
            "Site '" + siteId + "' already scheduled for memberships sync. Doing nothing ...");
      }
      return;
    }

    final String oauth_consumer_key = (String) payload.get(OAuth.OAUTH_CONSUMER_KEY);

    // This is non standard. Moodle's core LTI plugin does not currently do memberships and
    // a fix for this has been proposed at https://tracker.moodle.org/browse/MDL-41724. I don't
    // think this will ever become core and the first time memberships will appear in core lti
    // is with LTI2. At that point this code will be replaced with standard LTI2 JSON type stuff.

    String lms = (String) payload.get("ext_lms");
    final String callbackType =
        (BasicLTIUtil.isNotBlank(lms) && lms.equals("moodle-2"))
            ? "ext-moodle-2"
            : (String) payload.get(BasicLTIConstants.LTI_VERSION);

    (new Thread(
            new Runnable() {

              public void run() {

                long then = 0L;

                if (M_log.isDebugEnabled()) {
                  M_log.debug("Starting memberships sync.");
                  then = (new Date()).getTime();
                }

                siteMembershipsSynchroniser.synchroniseSiteMemberships(
                    siteId,
                    membershipsId,
                    membershipsUrl,
                    oauth_consumer_key,
                    isEmailTrustedConsumer,
                    callbackType);

                if (M_log.isDebugEnabled()) {
                  long now = (new Date()).getTime();
                  M_log.debug(
                      "Memberships sync finished. It took " + ((now - then) / 1000) + " seconds.");
                }
              }
            },
            "org.sakaiproject.blti.ProviderServlet.MembershipsSync"))
        .start();

    ltiService.insertMembershipsJob(
        siteId, membershipsId, membershipsUrl, oauth_consumer_key, callbackType);
  }