/**
   * Fills the request with required AuthNContext according to selected options.
   *
   * @param request request to fill
   * @param options options driving generation of the element
   */
  protected void buildAuthnContext(AuthnRequest request, WebSSOProfileOptions options) {

    Collection<String> contexts = options.getAuthnContexts();
    if (contexts != null && contexts.size() > 0) {

      SAMLObjectBuilder<RequestedAuthnContext> builder =
          (SAMLObjectBuilder<RequestedAuthnContext>)
              builderFactory.getBuilder(RequestedAuthnContext.DEFAULT_ELEMENT_NAME);
      RequestedAuthnContext authnContext = builder.buildObject();
      authnContext.setComparison(options.getAuthnContextComparison());

      for (String context : contexts) {

        SAMLObjectBuilder<AuthnContextClassRef> contextRefBuilder =
            (SAMLObjectBuilder<AuthnContextClassRef>)
                builderFactory.getBuilder(AuthnContextClassRef.DEFAULT_ELEMENT_NAME);
        AuthnContextClassRef authnContextClassRef = contextRefBuilder.buildObject();
        authnContextClassRef.setAuthnContextClassRef(context);
        authnContext.getAuthnContextClassRefs().add(authnContextClassRef);
      }

      request.setRequestedAuthnContext(authnContext);
      log.debug("ajoute RequestedAuthnContext=" + request.toString());
    }
  }
  /**
   * Initializes SSO by creating AuthnRequest assertion and sending it to the IDP using the default
   * binding. Default IDP is used to send the request.
   *
   * @param options values specified by caller to customize format of sent request
   * @throws SAMLException error initializing SSO
   * @throws SAMLRuntimeException in case context doesn't contain required entities or contains
   *     invalid data
   * @throws MetadataProviderException error retrieving needed metadata
   * @throws MessageEncodingException error forming SAML message
   */
  public void sendAuthenticationRequest(SAMLMessageContext context, WebSSOProfileOptions options)
      throws SAMLException, MetadataProviderException, MessageEncodingException {

    // Verify we deal with a local SP
    if (!SPSSODescriptor.DEFAULT_ELEMENT_NAME.equals(context.getLocalEntityRole())) {
      throw new SAMLException(
          "WebSSO can only be initialized for local SP, but localEntityRole is: "
              + context.getLocalEntityRole());
    }

    // Load the entities from the context
    SPSSODescriptor spDescriptor = (SPSSODescriptor) context.getLocalEntityRoleMetadata();
    IDPSSODescriptor idpssoDescriptor = (IDPSSODescriptor) context.getPeerEntityRoleMetadata();
    ExtendedMetadata idpExtendedMetadata = context.getPeerExtendedMetadata();

    if (spDescriptor == null || idpssoDescriptor == null || idpExtendedMetadata == null) {
      throw new SAMLException(
          "SPSSODescriptor, IDPSSODescriptor or IDPExtendedMetadata are not present in the SAMLContext");
    }
    log.debug(
        "idpExtendedMetadata.getSigningAlgorithm=" + idpExtendedMetadata.getSigningAlgorithm());
    idpExtendedMetadata.setSigningAlgorithm("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512");

    SingleSignOnService ssoService =
        getSingleSignOnService(options, idpssoDescriptor, spDescriptor);
    AssertionConsumerService consumerService =
        getAssertionConsumerService(options, idpssoDescriptor, spDescriptor);

    AuthnRequest authRequest = getAuthnRequest(context, options, consumerService, ssoService);
    if (authRequest == null) {
      throw new SAMLException("Erreur dans getAuthnRequest null");
    }
    // authRequest.setForceAuthn(Boolean.TRUE);
    log.debug("getAuthnRequest.providerName=" + authRequest.getProviderName());
    // TODO optionally implement support for conditions, subject

    context.setCommunicationProfileId(getProfileIdentifier());
    context.setOutboundMessage(authRequest);
    context.setOutboundSAMLMessage(authRequest);
    context.setPeerEntityEndpoint(ssoService);
    context.setPeerEntityRoleMetadata(idpssoDescriptor);
    context.setPeerExtendedMetadata(idpExtendedMetadata);

    if (options.getRelayState() != null) {
      context.setRelayState(options.getRelayState());
    }

    boolean sign =
        spDescriptor.isAuthnRequestsSigned() || idpssoDescriptor.getWantAuthnRequestsSigned();
    log.debug("signature?" + sign + " avec http://www.w3.org/2001/04/xmldsig-more#rsa-sha512");
    context
        .getLocalExtendedMetadata()
        .setSigningAlgorithm("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512");
    sendMessage(context, sign);

    SAMLMessageStorage messageStorage = context.getMessageStorage();
    if (messageStorage != null) {
      messageStorage.storeMessage(authRequest.getID(), authRequest);
    }
  }
  /**
   * Fills the request with required AuthNContext according to selected options.
   *
   * @param request request to fill
   * @param options options driving generation of the element
   */
  protected void builNameIDPolicy(AuthnRequest request, WebSSOProfileOptions options) {

    if (options.getNameID() != null) {
      SAMLObjectBuilder<NameIDPolicy> builder =
          (SAMLObjectBuilder<NameIDPolicy>)
              builderFactory.getBuilder(NameIDPolicy.DEFAULT_ELEMENT_NAME);
      NameIDPolicy nameIDPolicy = builder.buildObject();
      nameIDPolicy.setFormat(options.getNameID());
      nameIDPolicy.setAllowCreate(options.isAllowCreate());
      nameIDPolicy.setSPNameQualifier(getSPNameQualifier());
      request.setNameIDPolicy(nameIDPolicy);
    }
  }
  /**
   * Determines endpoint where should the identity provider return the SAML message. Endpoint also
   * implies the used binding. In case assertionConsumerIndex in the WebSSOProfileOptions is
   * specified the endpoint with the given ID is used. Otherwise assertionConsumerService marked as
   * default is used when present, otherwise first found supported assertionConsumerService is used.
   *
   * <p>In case endpoint determined by the webSSOProfileOptions index is not supported by the
   * profile an exception is raised.
   *
   * @param options user supplied preferences
   * @param idpSSODescriptor idp, can be null when no IDP is known in advance
   * @param spDescriptor sp
   * @return consumer service or null
   * @throws MetadataProviderException in case index supplied in options is invalid or unsupported
   *     or no supported consumer service can be found
   */
  protected AssertionConsumerService getAssertionConsumerService(
      WebSSOProfileOptions options, IDPSSODescriptor idpSSODescriptor, SPSSODescriptor spDescriptor)
      throws MetadataProviderException {

    List<AssertionConsumerService> services = spDescriptor.getAssertionConsumerServices();

    // Use user preference
    if (options.getAssertionConsumerIndex() != null) {
      for (AssertionConsumerService service : services) {
        if (options.getAssertionConsumerIndex().equals(service.getIndex())) {
          if (!isEndpointSupported(service)) {
            throw new MetadataProviderException(
                "Endpoint designated by the value in the WebSSOProfileOptions is not supported by this profile");
          } else {
            log.debug(
                "Using consumer service determined by user preference with binding {}",
                service.getBinding());
            return service;
          }
        }
      }
      throw new MetadataProviderException(
          "AssertionConsumerIndex "
              + options.getAssertionConsumerIndex()
              + " not found for spDescriptor "
              + spDescriptor);
    }

    // Use default
    if (spDescriptor.getDefaultAssertionConsumerService() != null
        && isEndpointSupported(spDescriptor.getDefaultAssertionConsumerService())) {
      AssertionConsumerService service = spDescriptor.getDefaultAssertionConsumerService();
      log.debug("Using default consumer service with binding {}", service.getBinding());
      log.debug("Default ACS url=" + service.getLocation());
      return service;
    }

    // Iterate and find first match
    if (services.size() > 0) {
      for (AssertionConsumerService service : services) {
        if (isEndpointSupported(service)) {
          log.debug("Using first available consumer service with binding {}", service.getBinding());
          return service;
        }
      }
    }

    throw new MetadataProviderException(
        "Service provider has no assertion consumer service available for the selected profile "
            + spDescriptor);
  }
  /**
   * Fills the request with information about scoping, including IDP in the scope IDP List.
   *
   * @param request request to fill
   * @param serviceURI destination to send the request to
   * @param options options driving generation of the element, contains list of allowed IDPs
   */
  protected void buildScoping(
      AuthnRequest request, SingleSignOnService serviceURI, WebSSOProfileOptions options) {

    if (options.isIncludeScoping() != null && options.isIncludeScoping()) {

      Set<String> idpEntityNames = options.getAllowedIDPs();
      IDPList idpList = buildIDPList(idpEntityNames, serviceURI);
      SAMLObjectBuilder<Scoping> scopingBuilder =
          (SAMLObjectBuilder<Scoping>) builderFactory.getBuilder(Scoping.DEFAULT_ELEMENT_NAME);
      Scoping scoping = scopingBuilder.buildObject();
      scoping.setIDPList(idpList);
      scoping.setProxyCount(options.getProxyCount());
      request.setScoping(scoping);
    }
  }
  /**
   * Method determines SingleSignOn service (and thus binding) to be used to deliver AuthnRequest to
   * the IDP. When binding is specified in the WebSSOProfileOptions it is honored. Otherwise first
   * suitable binding is used.
   *
   * @param options user supplied preferences, binding attribute is used
   * @param idpssoDescriptor idp
   * @param spDescriptor sp
   * @return service to send message to
   * @throws MetadataProviderException in case binding from the options is invalid or not found or
   *     when no default service can be found
   */
  protected SingleSignOnService getSingleSignOnService(
      WebSSOProfileOptions options, IDPSSODescriptor idpssoDescriptor, SPSSODescriptor spDescriptor)
      throws MetadataProviderException {

    // User specified value
    String userBinding = options.getBinding();

    // Find the endpoint
    List<SingleSignOnService> services = idpssoDescriptor.getSingleSignOnServices();
    for (SingleSignOnService service : services) {
      if (isEndpointSupported(service)) {
        if (userBinding != null) {
          if (isEndpointMatching(service, userBinding)) {
            log.debug("Found user specified binding {}", userBinding);
            return service;
          }
        } else {
          // Use as a default
          return service;
        }
      }
    }

    // No value found
    if (userBinding != null) {
      throw new MetadataProviderException(
          "User specified binding "
              + userBinding
              + " is not supported by the IDP using profile "
              + getProfileIdentifier());
    } else {
      throw new MetadataProviderException(
          "No supported binding "
              + userBinding
              + " was found for profile "
              + getProfileIdentifier());
    }
  }
  /**
   * Returns AuthnRequest SAML message to be used to demand authentication from an IDP described
   * using idpEntityDescriptor, with an expected response to the assertionConsumer address.
   *
   * @param context message context
   * @param options preferences of message creation
   * @param assertionConsumer assertion consumer where the IDP should respond
   * @param bindingService service used to deliver the request
   * @return authnRequest ready to be sent to IDP
   * @throws SAMLException error creating the message
   * @throws MetadataProviderException error retreiving metadata
   */
  protected AuthnRequest getAuthnRequest(
      SAMLMessageContext context,
      WebSSOProfileOptions options,
      AssertionConsumerService assertionConsumer,
      SingleSignOnService bindingService)
      throws SAMLException, MetadataProviderException {

    SAMLObjectBuilder<AuthnRequest> builder =
        (SAMLObjectBuilder<AuthnRequest>)
            builderFactory.getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME);
    AuthnRequest request = builder.buildObject();

    request.setIsPassive(options.getPassive());
    request.setForceAuthn(options.getForceAuthN());
    request.setProviderName(options.getProviderName());
    request.setVersion(SAMLVersion.VERSION_20);
    if (options.getIncludeEidas()) {
      // ne sert à rien
      //            request.setAssertionConsumerServiceURL(options.getIssuer());
      //            IssuerBuilder issuerBuilder = new IssuerBuilder();
      //            Issuer issuer = issuerBuilder.buildObject();
      //            issuer.setFormat(NAME_ISSUER_FORMAT_EIDAS);
      //            log.debug("issuer="+options.getIssuer());
      //            issuer.setValue(options.getIssuer());
      //            request.setIssuer(issuer);
      NameIDPolicy nameIDPolicy = new NameIDPolicyBuilder().buildObject();
      nameIDPolicy.setFormat(NAME_POLICY_FORMAT_EIDAS);
      nameIDPolicy.setAllowCreate(true);
      request.setNameIDPolicy(nameIDPolicy);
      QName eidas = new QName("xmlns:eidas", "http://eidas.europa.eu/saml-extensions");
      request.getNamespaceManager().registerAttributeName(eidas);
      Extensions extEidas =
          new ExtensionsBuilder()
              .buildObject("urn:oasis:names:tc:SAML:2.0:protocol", "Extensions", "saml2p");
      // Extensions extEidas = new EidasExtensions();
      Collection<String> colAttr = options.getEidasAttributes();
      // XSAnyBuilder raBuild = new XSAnyBuilder();
      // <eidas:SPType>public</eidas:SPType>
      SPType pub =
          new SPTypeBuilder()
              .buildObject("http://eidas.europa.eu/saml-extensions", "SPType", "eidas");
      // pub.setTextContent(EIDAS_PUBLIC);
      pub.setSPType(EIDAS_PUBLIC);
      // XSAny attrs = new XSAnyBuilder().buildObject("http://eidas.europa.eu/saml-extensions",
      // "RequestedAttributes", "eidas");
      extEidas.getUnknownXMLObjects().add(pub);
      // XSAnyBuilder anyBuilder = (XSAnyBuilder)
      // Configuration.getBuilderFactory().getBuilder(XSAny.TYPE_NAME);
      String resAttrs =
          "<eidas:RequestedAttributes xmlns:eidas=\"http://eidas.europa.eu/saml-extensions\">";
      for (String attr : colAttr) {
        resAttrs += oneAttribute(attr);
      }
      resAttrs += "</eidas:RequestedAttributes>";
      log.debug("resAttrs=" + resAttrs);
      EidasExtensionConfiguration eidasExt = new EidasExtensionConfiguration();
      eidasExt.configureExtension();
      SAMLSchemaBuilder.addExtensionSchema("/schema/saml_eidas_extension.xsd");
      BasicParserPool ppMgr = new BasicParserPool();
      ppMgr.setNamespaceAware(true);
      try {
        ppMgr.setSchema(SAMLSchemaBuilder.getSAML11Schema());
      } catch (SAXException ex) {
        log.error("Erreur schema=" + ex);
        return null;
      }
      InputStream is = new ByteArrayInputStream(resAttrs.getBytes());
      Document domAttrsRaq = null;
      try {
        domAttrsRaq = ppMgr.parse(is);
      } catch (XMLParserException e) {
        log.error("Erreur dom=" + e);
        return null;
      }
      if (domAttrsRaq == null) {
        log.error("Erreur dom vide");
        return null;
      }
      RequestedAttributesUnmarshaller unMars = new RequestedAttributesUnmarshaller();
      XMLObject attrs = null;
      try {
        attrs = unMars.unmarshall(domAttrsRaq.getDocumentElement());
      } catch (UnmarshallingException e) {
        System.err.println("Erreur unMarsh error=" + e);
      }

      extEidas.getUnknownXMLObjects().add(attrs);
      request.setExtensions(extEidas);
    }
    buildCommonAttributes(context.getLocalEntityId(), request, bindingService);

    buildScoping(request, bindingService, options);
    builNameIDPolicy(request, options);
    buildAuthnContext(request, options);
    buildReturnAddress(request, assertionConsumer);

    return request;
  }