/**
   * Create a new client
   *
   * @param json
   * @param m
   * @param principal
   * @return
   */
  @PreAuthorize("hasRole('ROLE_ADMIN')")
  @RequestMapping(
      method = RequestMethod.POST,
      consumes = "application/json",
      produces = "application/json")
  public String apiAddClient(@RequestBody String jsonString, Model m, Authentication auth) {

    JsonObject json = null;
    ClientDetailsEntity client = null;

    try {
      json = parser.parse(jsonString).getAsJsonObject();
      client = gson.fromJson(json, ClientDetailsEntity.class);
    } catch (JsonSyntaxException e) {
      logger.error("apiAddClient failed due to JsonSyntaxException", e);
      m.addAttribute("code", HttpStatus.BAD_REQUEST);
      m.addAttribute(
          "errorMessage",
          "Could not save new client. The server encountered a JSON syntax exception. Contact a system administrator for assistance.");
      return "jsonErrorView";
    } catch (IllegalStateException e) {
      logger.error("apiAddClient failed due to IllegalStateException", e);
      m.addAttribute("code", HttpStatus.BAD_REQUEST);
      m.addAttribute(
          "errorMessage",
          "Could not save new client. The server encountered an IllegalStateException. Refresh and try again - if the problem persists, contact a system administrator for assistance.");
      return "jsonErrorView";
    }

    // if they leave the client identifier empty, force it to be generated
    if (Strings.isNullOrEmpty(client.getClientId())) {
      client = clientService.generateClientId(client);
    }

    // if they've asked for us to generate a client secret, do so here
    if (json.has("generateClientSecret") && json.get("generateClientSecret").getAsBoolean()) {
      client = clientService.generateClientSecret(client);
    }

    // set owners as current logged in user
    // try to look up a user based on the principal's name
    if (client.getContacts() == null || client.getContacts().isEmpty()) {
      UserInfo user = userInfoService.getByUsername(auth.getName());
      if (user != null && user.getEmail() != null) {
        client.setContacts(Sets.newHashSet(user.getEmail()));
      }
    }

    client.setDynamicallyRegistered(false);

    ClientDetailsEntity newClient = clientService.saveNewClient(client);
    m.addAttribute("entity", newClient);

    if (isAdmin(auth)) {
      return "clientEntityViewAdmins";
    } else {
      return "clientEntityViewUsers";
    }
  }
  /**
   * Create a new Client, issue a client ID, and create a registration access token.
   *
   * @param jsonString
   * @param m
   * @param p
   * @return
   */
  @RequestMapping(
      method = RequestMethod.POST,
      consumes = "application/json",
      produces = "application/json")
  public String registerNewClient(@RequestBody String jsonString, Model m) {

    ClientDetailsEntity newClient = ClientDetailsEntityJsonProcessor.parse(jsonString);

    if (newClient != null) {
      // it parsed!

      //
      // Now do some post-processing consistency checks on it
      //

      // clear out any spurious id/secret (clients don't get to pick)
      newClient.setClientId(null);
      newClient.setClientSecret(null);

      // set of scopes that are OK for clients to dynamically register for
      Set<SystemScope> dynScopes = scopeService.getDynReg();

      // scopes that the client is asking for
      Set<SystemScope> requestedScopes = scopeService.fromStrings(newClient.getScope());

      // the scopes that the client can have must be a subset of the dynamically allowed scopes
      Set<SystemScope> allowedScopes = Sets.intersection(dynScopes, requestedScopes);

      // if the client didn't ask for any, give them the defaults
      if (allowedScopes == null || allowedScopes.isEmpty()) {
        allowedScopes = scopeService.getDefaults();
      }

      newClient.setScope(scopeService.toStrings(allowedScopes));

      // set default grant types if needed
      if (newClient.getGrantTypes() == null || newClient.getGrantTypes().isEmpty()) {
        if (newClient.getScope().contains("offline_access")) { // client asked for offline access
          newClient.setGrantTypes(
              Sets.newHashSet(
                  "authorization_code",
                  "refresh_token")); // allow authorization code and refresh token grant types by
          // default
        } else {
          newClient.setGrantTypes(
              Sets.newHashSet(
                  "authorization_code")); // allow authorization code grant type by default
        }
      }

      // set default response types if needed
      // TODO: these aren't checked by SECOAUTH
      // TODO: the consistency between the response_type and grant_type needs to be checked by the
      // client service, most likely
      if (newClient.getResponseTypes() == null || newClient.getResponseTypes().isEmpty()) {
        newClient.setResponseTypes(
            Sets.newHashSet("code")); // default to allowing only the auth code flow
      }

      if (newClient.getTokenEndpointAuthMethod() == null) {
        newClient.setTokenEndpointAuthMethod(AuthMethod.SECRET_BASIC);
      }

      if (newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_BASIC
          || newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_JWT
          || newClient.getTokenEndpointAuthMethod() == AuthMethod.SECRET_POST) {

        // we need to generate a secret
        newClient = clientService.generateClientSecret(newClient);
      }

      // set some defaults for token timeouts
      newClient.setAccessTokenValiditySeconds(
          (int) TimeUnit.HOURS.toSeconds(1)); // access tokens good for 1hr
      newClient.setIdTokenValiditySeconds(
          (int) TimeUnit.MINUTES.toSeconds(10)); // id tokens good for 10min
      newClient.setRefreshTokenValiditySeconds(null); // refresh tokens good until revoked

      // this client has been dynamically registered (obviously)
      newClient.setDynamicallyRegistered(true);

      // TODO: check and enforce the sector URI if it's not null (#504)

      // now save it
      try {
        ClientDetailsEntity savedClient = clientService.saveNewClient(newClient);

        // generate the registration access token
        OAuth2AccessTokenEntity token =
            connectTokenService.createRegistrationAccessToken(savedClient);
        tokenService.saveAccessToken(token);

        // send it all out to the view

        // TODO: urlencode the client id for safety?
        RegisteredClient registered =
            new RegisteredClient(
                savedClient,
                token.getValue(),
                config.getIssuer() + "register/" + savedClient.getClientId());

        m.addAttribute("client", registered);
        m.addAttribute("code", HttpStatus.CREATED); // http 201

        return "clientInformationResponseView";
      } catch (IllegalArgumentException e) {
        logger.error("Couldn't save client", e);
        m.addAttribute("code", HttpStatus.BAD_REQUEST);

        return "httpCodeView";
      }
    } else {
      // didn't parse, this is a bad request
      logger.error("registerNewClient failed; submitted JSON is malformed");
      m.addAttribute("code", HttpStatus.BAD_REQUEST); // http 400

      return "httpCodeView";
    }
  }
  /**
   * Create a new Client, issue a client ID, and create a registration access token.
   *
   * @param jsonString
   * @param m
   * @param p
   * @return
   */
  @RequestMapping(
      method = RequestMethod.POST,
      consumes = MediaType.APPLICATION_JSON_VALUE,
      produces = MediaType.APPLICATION_JSON_VALUE)
  public String registerNewProtectedResource(@RequestBody String jsonString, Model m) {

    ClientDetailsEntity newClient = null;
    try {
      newClient = ClientDetailsEntityJsonProcessor.parse(jsonString);
    } catch (JsonSyntaxException e) {
      // bad parse
      // didn't parse, this is a bad request
      logger.error("registerNewProtectedResource failed; submitted JSON is malformed");
      m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400
      return HttpCodeView.VIEWNAME;
    }

    if (newClient != null) {
      // it parsed!

      //
      // Now do some post-processing consistency checks on it
      //

      // clear out any spurious id/secret (clients don't get to pick)
      newClient.setClientId(null);
      newClient.setClientSecret(null);

      // do validation on the fields
      try {
        newClient = validateScopes(newClient);
        newClient = validateAuth(newClient);
      } catch (ValidationException ve) {
        // validation failed, return an error
        m.addAttribute(JsonErrorView.ERROR, ve.getError());
        m.addAttribute(JsonErrorView.ERROR_MESSAGE, ve.getErrorDescription());
        m.addAttribute(HttpCodeView.CODE, ve.getStatus());
        return JsonErrorView.VIEWNAME;
      }

      // no grant types are allowed
      newClient.setGrantTypes(new HashSet<String>());
      newClient.setResponseTypes(new HashSet<String>());
      newClient.setRedirectUris(new HashSet<String>());

      // don't issue tokens to this client
      newClient.setAccessTokenValiditySeconds(0);
      newClient.setIdTokenValiditySeconds(0);
      newClient.setRefreshTokenValiditySeconds(0);

      // clear out unused fields
      newClient.setDefaultACRvalues(new HashSet<String>());
      newClient.setDefaultMaxAge(null);
      newClient.setIdTokenEncryptedResponseAlg(null);
      newClient.setIdTokenEncryptedResponseEnc(null);
      newClient.setIdTokenSignedResponseAlg(null);
      newClient.setInitiateLoginUri(null);
      newClient.setPostLogoutRedirectUris(null);
      newClient.setRequestObjectSigningAlg(null);
      newClient.setRequireAuthTime(null);
      newClient.setReuseRefreshToken(false);
      newClient.setSectorIdentifierUri(null);
      newClient.setSubjectType(null);
      newClient.setUserInfoEncryptedResponseAlg(null);
      newClient.setUserInfoEncryptedResponseEnc(null);
      newClient.setUserInfoSignedResponseAlg(null);

      // this client has been dynamically registered (obviously)
      newClient.setDynamicallyRegistered(true);

      // this client has access to the introspection endpoint
      newClient.setAllowIntrospection(true);

      // now save it
      try {
        ClientDetailsEntity savedClient = clientService.saveNewClient(newClient);

        // generate the registration access token
        OAuth2AccessTokenEntity token = connectTokenService.createResourceAccessToken(savedClient);
        tokenService.saveAccessToken(token);

        // send it all out to the view

        RegisteredClient registered =
            new RegisteredClient(
                savedClient,
                token.getValue(),
                config.getIssuer()
                    + "resource/"
                    + UriUtils.encodePathSegment(savedClient.getClientId(), "UTF-8"));
        m.addAttribute("client", registered);
        m.addAttribute(HttpCodeView.CODE, HttpStatus.CREATED); // http 201

        return ClientInformationResponseView.VIEWNAME;
      } catch (UnsupportedEncodingException e) {
        logger.error("Unsupported encoding", e);
        m.addAttribute(HttpCodeView.CODE, HttpStatus.INTERNAL_SERVER_ERROR);
        return HttpCodeView.VIEWNAME;
      } catch (IllegalArgumentException e) {
        logger.error("Couldn't save client", e);

        m.addAttribute(JsonErrorView.ERROR, "invalid_client_metadata");
        m.addAttribute(
            JsonErrorView.ERROR_MESSAGE,
            "Unable to save client due to invalid or inconsistent metadata.");
        m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400

        return JsonErrorView.VIEWNAME;
      }
    } else {
      // didn't parse, this is a bad request
      logger.error("registerNewClient failed; submitted JSON is malformed");
      m.addAttribute(HttpCodeView.CODE, HttpStatus.BAD_REQUEST); // http 400

      return HttpCodeView.VIEWNAME;
    }
  }