/*
 * Copyright (C) 2016-2017 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import java.io.Serializable;
import java.net.URI;
import com.authlete.common.dto.AuthorizationResponse;
import com.authlete.common.dto.Client;
import com.authlete.common.dto.Scope;
import com.authlete.common.types.User;

/**
 * Model class to hold data which are referred to in an authorization page.
 *
 *

 * Feel free to extend this class as necessary.
 *

 *
 * @author Takahiko Kawasaki
 */
public class AuthorizationPageModel implements Serializable
{
    private static final long serialVersionUID = 2L;

    /**
     * The name of the service.
     */
    private String serviceName;

    /**
     * The name of the client application.
     */
    private String clientName;

    /**
     * The description of the client application.
     */
    private String description;

    /**
     * The URL of the logo image of the client application.
     */
    private String logoUri;

    /**
     * The URL of the homepage of the client application.
     */
    private String clientUri;

    /**
     * The URL of the policy page of the client application.
     */
    private String policyUri;

    /**
     * The URL of "Terms of Service" page of the client application.
     */
    private String tosUri;

    /**
     * Scopes requested by the authorization request.
     */
    private Scope[] scopes;

    /**
     * The login ID that should be used as the initial value for the
     * login ID field in the authorization page.
     */
    private String loginId;

    /**
     * This variable holds {@code "readonly"} when the initial value
     * of the login ID should not be changed.
     */
    private String loginIdReadOnly;

    /**
     * Currently logged in user, could be null if no user is logged in.
     */
    private User user;

    /**
     * The default constructor with default values.
     */
    public AuthorizationPageModel()
    {
    }

    /**
     * Create an {@link AuthorizationPageModel} instance using information
     * contained in an {@link AuthorizationResponse} object, which represents
     * a response from Authlete's {@code /api/auth/authorization} API.
     *
     *

     * {@code user} parameter was added by version 2.1.
     *

     *
     * @param info
     *         An {@link AuthorizationResponse} object, which represents a
     *         response from Authlete's {@code /api/auth/authorization} API.
     *
     * @param user
     */
    public AuthorizationPageModel(AuthorizationResponse info, User user)
    {
        Client client = info.getClient();

        serviceName     = info.getService().getServiceName();
        clientName      = client.getClientName();
        description     = client.getDescription();
        logoUri         = toString(client.getLogoUri());
        clientUri       = toString(client.getClientUri());
        policyUri       = toString(client.getPolicyUri());
        tosUri          = toString(client.getTosUri());
        scopes          = info.getScopes();
        loginId         = computeLoginId(info);
        loginIdReadOnly = computeLoginIdReadOnly(info);

        // current logged in user, could be null
        this.user       = user;
    }

    /**
     * Get the name of the service.
     *
     * @return
     *         The name of the service.
     */
    public String getServiceName()
    {
        return serviceName;
    }

    /**
     * Set the name of the service.
     *
     * @param serviceName
     *         The name of the service.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setServiceName(String serviceName)
    {
        this.serviceName = serviceName;

        return this;
    }

    /**
     * Get the name of the client application.
     *
     * @return
     *         The name of the client application.
     */
    public String getClientName()
    {
        return clientName;
    }

    /**
     * Set the name of the client application.
     *
     * @param clientName
     *         The name of the client application.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setClientName(String clientName)
    {
        this.clientName = clientName;

        return this;
    }

    /**
     * Get the description of the client application.
     *
     * @return
     *         The description of the client application.
     */
    public String getDescription()
    {
        return description;
    }

    /**
     * Set the description of the client application.
     *
     * @param description
     *         The description of the client application.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setDescription(String description)
    {
        this.description = description;

        return this;
    }

    /**
     * Get the URL of the logo image of the client application.
     *
     * @return
     *         The URL of the logo image of the client application.
     */
    public String getLogoUri()
    {
        return logoUri;
    }

    /**
     * Set the URL of the logo image of the client application.
     *
     * @param logoUri
     *         The URL of the logo image of the client application.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setLogoUri(String logoUri)
    {
        this.logoUri = logoUri;

        return this;
    }

    /**
     * Get the URL of the homepage of the client application.
     *
     * @return
     *         The URL of the homepage of the client application.
     */
    public String getClientUri()
    {
        return clientUri;
    }

    /**
     * Set the URL of the homepage of the client application.
     *
     * @param clientUri
     *         The URL of the homepage of the client application.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setClientUri(String clientUri)
    {
        this.clientUri = clientUri;

        return this;
    }

    /**
     * Get the URL of the policy page of the client application.
     *
     * @return
     *         The URL of the policy page of the client application.
     */
    public String getPolicyUri()
    {
        return policyUri;
    }

    /**
     * Set the URL of the policy page of the client application.
     *
     * @param policyUri
     *         The URL of the policy page of the client application.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setPolicyUri(String policyUri)
    {
        this.policyUri = policyUri;

        return this;
    }

    /**
     * Get the URL of "Terms of Service" page of the client application.
     *
     * @return
     *         The URL of "Terms of Service" page of the client application.
     */
    public String getTosUri()
    {
        return tosUri;
    }

    /**
     * Set the URL of "Terms of Service" page of the client application.
     *
     * @param tosUri
     *         The URL of "Terms of Service" page of the client application.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setTosUri(String tosUri)
    {
        this.tosUri = tosUri;

        return this;
    }

    /**
     * Get the list of scopes requested by the authorization request.
     *
     * @return
     *         The list of requested scopes.
     */
    public Scope[] getScopes()
    {
        return scopes;
    }

    /**
     * Set the list of scopes requested by the authorization request.
     *
     * @param scopes
     *         The list of requested scopes.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setScopes(Scope[] scopes)
    {
        this.scopes = scopes;

        return this;
    }

    /**
     * Get the login ID which should be set to the login ID field
     * in the authorization page as the initial value.
     *
     * @return
     *         The initial value of the login ID.
     */
    public String getLoginId()
    {
        return loginId;
    }

    /**
     * Set the login ID which should be set to the login ID field
     * in the authorization page as the initial value.
     *
     * @param loginId
     *         The initial value of the login ID.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setLoginId(String loginId)
    {
        this.loginId = loginId;

        return this;
    }

    /**
     * Return {@code "readonly"} if the initial value of the login ID
     * should not be changed.
     *
     * @return
     *         {@code "readonly"} if the initial value of the login ID
     *         should not be changed. Otherwise, {@code null}.
     */
    public String getLoginIdReadOnly()
    {
        return loginIdReadOnly;
    }

    /**
     * Set the value returned from {@link #getLoginIdReadOnly()}.
     *
     * @param loginIdReadOnly
     *         Pass {@code "readonly"} if the initial value of the login
     *         ID should not be changed. Otherwise, pass {@code null}.
     *
     * @return
     *         {@code this} object.
     */
    public AuthorizationPageModel setLoginIdReadOnly(String loginIdReadOnly)
    {
        this.loginIdReadOnly = loginIdReadOnly;

        return this;
    }

    /**
     * Get the user.
     *
     * @return
     *         The user.
     *
     * @since 2.1
     */
    public User getUser()
    {
        return user;
    }

    /**
     * Set the user.
     *
     * @param user
     *            The user to set.
     *
     * @since 2.1
     */
    public void setUser(User user)
    {
        this.user = user;
    }

    /**
     * Get the string representation of the given URI.
     *
     * @param uri
     *         A URI.
     *
     * @return
     *         The string representation of the given URI. If {@code null}
     *         is given, {@code null} is returned.
     */
    private static String toString(URI uri)
    {
        return (uri == null) ? null : uri.toString();
    }

    /**
     * Compute the initial value for the login ID field in the
     * authorization page.
     */
    private static String computeLoginId(AuthorizationResponse info)
    {
        if (info.getSubject() != null)
        {
            return info.getSubject();
        }

        return info.getLoginHint();
    }

    /**
     * Return {@code "readonly"} if the authorization request requires
     * that a specific subject be used.
     */
    private static String computeLoginIdReadOnly(AuthorizationResponse info)
    {
        if (info.getSubject() != null)
        {
            return "readonly";
        }
        else
        {
            return null;
        }
    }
}


/*
 * Copyright (C) 2016 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;

/**
 * A base class for JWK Set endpoints.
 *
 *

 * An OpenID Provider (OP) is required to expose its JSON Web Key Set document
 * (JWK Set) so that client applications can (1) verify signatures by the OP
 * and (2) encrypt their requests to the OP. The URI of a JWK Set endpoint can
 * be found as the value of {@code jwks_uri} in OpenID Provider Metadata if the OP supports OpenID
 * Connect Discovery 1.0.
 *

 *
 * @see RFC 7517, JSON Web Key (JWK)
 *
 * @see OpenID Connect Core 1.0
 *
 * @see OpenID Connect Discovery 1.0
 *
 * @since 1.1
 *
 * @author Takahiko Kawasaki
 */
public class BaseJwksEndpoint extends BaseEndpoint
{
    /**
     * Handle a request for a JWK Set document.
     *
     *

     * This method internally creates a {@link JwksRequestHandler} instance and
     * calls its {@link JwksRequestHandler#handle()} method. Then, this method
     * uses the value returned from the {@code handle()} method as a response
     * from this method.
     *

     *
     *

     * When {@code JwksRequestHandler.handle()} method raises a {@link
     * WebApplicationException}, this method calls {@link #onError(WebApplicationException)
     * onError()} method with the exception. The default implementation of {@code onError()}
     * calls {@code printStackTrace()} of the exception and does nothing else. You
     * can override the method as necessary. After calling {@code onError()} method,
     * this method calls {@code getResponse()} method of the exception and uses the
     * returned value as a response from this method.
     *

     *
     * @param api
     *         An implementation of {@link AuthleteApi}.
     *
     * @return
     *         A response that should be returned to the client application.
     */
    public Response handle(AuthleteApi api)
    {
        try
        {
            // Create a handler.
            JwksRequestHandler handler = new JwksRequestHandler(api);

            // Delegate the task to the handler.
            return handler.handle();
        }
        catch (WebApplicationException e)
        {
            // An error occurred in the handler.
            onError(e);

            // Convert the error to a Response.
            return e.getResponse();
        }
    }
}


/*
 * Copyright (C) 2016-2018 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.jaxrs.spi.TokenRequestHandlerSpi;

/**
 * A base class for token endpoints.
 *
 * @since 1.2
 *
 * @see RFC 6749, 3.2. Token Endpoint
 *
 * @see OpenID Connect Core 1.0, 3.3.3. Token Endpoint
 *
 * @author Takahiko Kawasaki
 */
public class BaseTokenEndpoint extends BaseEndpoint
{
    /**
     * Handle a token request.
     *
     * This method is an alias of the {@link #handle(AuthleteApi,
     * TokenRequestHandlerSpi, MultivaluedMap, String, String[]) handle()}
     * method that accepts 5 arguments.
     *
     * @param api
     *         An implementation of {@link AuthleteApi}.
     *
     * @param spi
     *         An implementation of {@link TokenRequestHandlerSpi}.
     *
     * @param parameters
     *         Request parameters of the token request.
     *
     * @param authorization
     *         The value of {@code Authorization} header of the token request.
     *
     * @return
     *         A response that should be returned to the client application.
     */
    public Response handle(
            AuthleteApi api, TokenRequestHandlerSpi spi,
            MultivaluedMap<String, String> parameters, String authorization)
    {
        return handle(api, spi, parameters, authorization, null);
    }

    /**
     * Handle a token request.
     *
     *

     * This method internally creates a {@link TokenRequestHandler} instance and
     * calls its {@link TokenRequestHandler#handle(MultivaluedMap, String)} method.
     * Then, this method uses the value returned from the {@code handle()} method
     * as a response from this method.
     *

     *
     *

     * When {@code TokenRequestHandler.handle()} method raises a {@link
     * WebApplicationException}, this method calls {@link #onError(WebApplicationException)
     * onError()} method with the exception. The default implementation of {@code onError()}
     * does nothing. You can override the method as necessary. After calling
     * {@code onError()} method, this method calls {@code getResponse()} method of
     * the exception and uses the returned value as a response from this method.
     *

     *
     * @param api
     *         An implementation of {@link AuthleteApi}.
     *
     * @param spi
     *         An implementation of {@link TokenRequestHandlerSpi}.
     *
     * @param parameters
     *         Request parameters of the token request.
     *
     * @param authorization
     *         The value of {@code Authorization} header of the token request.
     *
     * @param clientCertificatePath
     *         The certificate path used in mutual TLS authentication, in PEM format. The
     *         client's own certificate is the first in this array. Can be {@code null}.
     *
     * @return
     *         A response that should be returned to the client application.
     *
     * @since 2.8
     */
    public Response handle(
            AuthleteApi api, TokenRequestHandlerSpi spi,
            MultivaluedMap<String, String> parameters, String authorization, String[] clientCertificatePath)
    {
        try
        {
            // Create a handler.
            TokenRequestHandler handler = new TokenRequestHandler(api, spi);

            // Delegate the task to the handler.
            return handler.handle(parameters, authorization, clientCertificatePath);
        }
        catch (WebApplicationException e)
        {
            // An error occurred in the handler.
            onError(e);

            // Convert the error to a Response.
            return e.getResponse();
        }
    }
}


/*
 * Copyright (C) 2017 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;

/**
 * A base class for introspection endpoint implementations.
 *
 * @see RFC 7662 : OAuth 2.0 Token Introspection
 *
 * @see IntrospectionRequestHandler
 *
 * @since 2.2
 *
 * @author Takahiko Kawasaki
 */
public class BaseIntrospectionEndpoint extends BaseEndpoint
{
    /**
     * Handle an introspection request.
     *
     *

     * This method internally creates an {@link IntrospectionRequestHandler}
     * instance and calls its {@link IntrospectionRequestHandler#handle(MultivaluedMap)
     * handle()} method with the {@code parameters} argument. Then, this
     * method uses the value returned from the {@code handle()} method as a
     * response from this method.
     *

     *
     *

     * When {@code IntrospectionRequestHandler.handle()} method raises a
     * {@link WebApplicationException}, this method calls {@link
     * #onError(WebApplicationException) onError()} method with the exception.
     * The default implementation of {@code onError()} calls {@code
     * printStackTrace()} of the exception and does nothing else. You can
     * override the method as necessary. After calling {@code onError()}
     * method, this method calls {@code getResponse()} method of the
     * exception and uses the returned value as a response from this method.
     *

     *
     * @param api
     *         An implementation of {@link AuthleteApi}.
     *
     * @param parameters
     *         Request parameters of an introspection request.
     *
     * @return
     *         A response that should be returned to the client application.
     */
    public Response handle(AuthleteApi api, MultivaluedMap<String, String> parameters)
    {
        try
        {
            // Create a handler.
            IntrospectionRequestHandler handler = new IntrospectionRequestHandler(api);

            // Delegate the task to the handler.
            return handler.handle(parameters);
        }
        catch (WebApplicationException e)
        {
            // An error occurred in the handler.
            onError(e);

            // Convert the error to a Response.
            return e.getResponse();
        }
    }
}


/*
 * Copyright (C) 2015-2016 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;

/**
 * Utility class to generate a response from an endpoint.
 *
 * @author Takahiko Kawasaki
 */
class ResponseUtil
{
    /**
     * {@code "application/json;charset=UTF-8"}
     */
    private static final MediaType MEDIA_TYPE_JSON =
            MediaType.APPLICATION_JSON_TYPE.withCharset("UTF-8");

    /**
     * {@code "text/html;charset=UTF-8"}
     */
    private static final MediaType MEDIA_TYPE_HTML =
            MediaType.TEXT_HTML_TYPE.withCharset("UTF-8");

    /**
     * {@code "application/jwt"}
     */
    static final MediaType MEDIA_TYPE_JWT = new MediaType("application", "jwt");

    /**
     * {@code "application/javascript;charset=UTF-8"}
     */
    private static final MediaType MEDIA_TYPE_JAVASCRIPT =
            new MediaType("application", "javascript", "UTF-8");

    /**
     * {@code "Cache-Control: no-store"}
     */
    private static final CacheControl CACHE_CONTROL;

    static
    {
        // Setup for Cache-Control header.
        CACHE_CONTROL = new CacheControl();
        CACHE_CONTROL.setNoStore(true);
    }

    /**
     * Create a response of {@code "200 OK"}
     * with the given entity formatted in
     * {@code "application/json;charset=UTF-8"}.
     */
    public static Response ok(String entity)
    {
        return ok(entity, MEDIA_TYPE_JSON);
    }

    /**
     * Create a response of {@code "200 OK"} with the given entity
     * with the given media format.
     */
    public static Response ok(String entity, MediaType mediaType)
    {
        return builder(Status.OK, entity, mediaType).build();
    }

    /**
     * Create a response of {@code "200 OK"}
     * with the given entity formatted in
     * {@code "text/html;charset=UTF-8"}.
     */
    public static Response form(String entity)
    {
        return builder(Status.OK, entity, MEDIA_TYPE_HTML).build();
    }

    /**
     * Create a response of {@code "200 OK"}
     * with the given entity formatted in
     * {@code "application/javascript;charset=UTF-8"}.
     */
    public static Response javaScript(String entity)
    {
        return builder(Status.OK, entity, MEDIA_TYPE_JAVASCRIPT).build();
    }

    /**
     * Create a response of {@code "204 No Content"}.
     */
    public static Response noContent()
    {
        return builder(Status.NO_CONTENT).build();
    }

    /**
     * Create a response of {@code "302 Found"}
     * with {@code Location} header having the given location.
     */
    public static Response location(String location)
    {
        return builder(Status.FOUND)
                .header(HttpHeaders.LOCATION, location)
                .build();
    }

    /**
     * Create a response of {@code "400 Bad Request"}
     * with the given entity formatted in
     * {@code "application/json;charset=UTF-8"}.
     */
    public static Response badRequest(String entity)
    {
        return builder(Status.BAD_REQUEST, entity, MEDIA_TYPE_JSON).build();
    }

    /**
     * Create a response of {@code "401 Unauthorized"}
     * with the given entity formatted in
     * {@code "application/json;charset=UTF-8"} and
     * with {@code WWW-Authenticate} header having the
     * value specified by {@code challenge}.
     */
    public static Response unauthorized(String entity, String challenge)
    {
        return builder(Status.UNAUTHORIZED, entity, MEDIA_TYPE_JSON)
                .header(HttpHeaders.WWW_AUTHENTICATE, challenge)
                .build();
    }

    /**
     * Create a response of {@code "403 Forbidden"}
     * with the given entity formatted in
     * {@code "application/json;charset=UTF-8"}.
     */
    public static Response forbidden(String entity)
    {
        return builder(Status.FORBIDDEN, entity, MEDIA_TYPE_JSON).build();
    }

    /**
     * Create a response of {@code "404 Not Found"}
     * with the given entity formatted in
     * {@code "application/json;charset=UTF-8"}.
     */
    public static Response notFound(String entity)
    {
        return builder(Status.NOT_FOUND, entity, MEDIA_TYPE_JSON).build();
    }

    /**
     * Create a response of {@code "500 Internal Server Error"}
     * with the given entity formatted in
     * {@code "application/json;charset=UTF-8"}.
     */
    public static Response internalServerError(String entity)
    {
        return internalServerError(entity, MEDIA_TYPE_JSON);
    }

    /**
     * Create a response of {@code "500 Internal Server Error"}
     * with the given entity.
     */
    public static Response internalServerError(String entity, MediaType mediaType)
    {
        return builder(Status.INTERNAL_SERVER_ERROR, entity, mediaType).build();
    }

    /**
     * Create a {@link ResponseBuilder} with the specified status,
     * {@code Cache-Control} header and {@code Pragma} header.
     */
    private static ResponseBuilder builder(Status status)
    {
        return Response
                .status(status)
                .cacheControl(CACHE_CONTROL)
                .header("Pragma", "no-cache");
    }

    /**
     * Create a {@link ResponseBuilder}.
     */
    private static ResponseBuilder builder(Status status, String entity, MediaType type)
    {
        return builder(status)
                .entity(entity)
                .type(type);
    }

    /**
     * Create a response with the given status and {@code WWW-Authenticate}
     * header having the given challenge as its value.
     */
    public static Response bearerError(Status status, String challenge)
    {
        return builder(status).header("WWW-Authenticate", challenge).build();
    }
}


/*
 * Copyright (C) 2014-2018 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs.api;

import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.ResponseProcessingException;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.StatusType;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.api.AuthleteApiException;
import com.authlete.common.api.Settings;
import com.authlete.common.conf.AuthleteConfiguration;
import com.authlete.common.dto.ApiResponse;
import com.authlete.common.dto.AuthorizationFailRequest;
import com.authlete.common.dto.AuthorizationFailResponse;
import com.authlete.common.dto.AuthorizationIssueRequest;
import com.authlete.common.dto.AuthorizationIssueResponse;
import com.authlete.common.dto.AuthorizationRequest;
import com.authlete.common.dto.AuthorizationResponse;
import com.authlete.common.dto.AuthorizedClientListResponse;
import com.authlete.common.dto.BackchannelAuthenticationCompleteRequest;
import com.authlete.common.dto.BackchannelAuthenticationCompleteResponse;
import com.authlete.common.dto.BackchannelAuthenticationFailRequest;
import com.authlete.common.dto.BackchannelAuthenticationFailResponse;
import com.authlete.common.dto.BackchannelAuthenticationIssueRequest;
import com.authlete.common.dto.BackchannelAuthenticationIssueResponse;
import com.authlete.common.dto.BackchannelAuthenticationRequest;
import com.authlete.common.dto.BackchannelAuthenticationResponse;
import com.authlete.common.dto.Client;
import com.authlete.common.dto.ClientAuthorizationDeleteRequest;
import com.authlete.common.dto.ClientAuthorizationGetListRequest;
import com.authlete.common.dto.ClientAuthorizationUpdateRequest;
import com.authlete.common.dto.ClientListResponse;
import com.authlete.common.dto.ClientRegistrationRequest;
import com.authlete.common.dto.ClientRegistrationResponse;
import com.authlete.common.dto.ClientSecretRefreshResponse;
import com.authlete.common.dto.ClientSecretUpdateRequest;
import com.authlete.common.dto.ClientSecretUpdateResponse;
import com.authlete.common.dto.GrantedScopesGetResponse;
import com.authlete.common.dto.IntrospectionRequest;
import com.authlete.common.dto.IntrospectionResponse;
import com.authlete.common.dto.JoseVerifyRequest;
import com.authlete.common.dto.JoseVerifyResponse;
import com.authlete.common.dto.RevocationRequest;
import com.authlete.common.dto.RevocationResponse;
import com.authlete.common.dto.Service;
import com.authlete.common.dto.ServiceListResponse;
import com.authlete.common.dto.StandardIntrospectionRequest;
import com.authlete.common.dto.StandardIntrospectionResponse;
import com.authlete.common.dto.TokenCreateRequest;
import com.authlete.common.dto.TokenCreateResponse;
import com.authlete.common.dto.TokenFailRequest;
import com.authlete.common.dto.TokenFailResponse;
import com.authlete.common.dto.TokenIssueRequest;
import com.authlete.common.dto.TokenIssueResponse;
import com.authlete.common.dto.TokenListResponse;
import com.authlete.common.dto.TokenRequest;
import com.authlete.common.dto.TokenResponse;
import com.authlete.common.dto.TokenUpdateRequest;
import com.authlete.common.dto.TokenUpdateResponse;
import com.authlete.common.dto.UserInfoIssueRequest;
import com.authlete.common.dto.UserInfoIssueResponse;
import com.authlete.common.dto.UserInfoRequest;
import com.authlete.common.dto.UserInfoResponse;
import com.authlete.common.web.BasicCredentials;

/**
 * The implementation of {@link AuthleteApi} using JAX-RS 2.0 client API.
 *
 * @author Takahiko Kawasaki
 */
public class AuthleteApiImpl implements AuthleteApi
{
    // "application/json;charset=UTF-8"
    private static final MediaType JSON_UTF8_TYPE = APPLICATION_JSON_TYPE.withCharset("UTF-8");

    private interface AuthleteApiCall<TResponse>
    {
        TResponse call();
    }

    private static final String AUTH_AUTHORIZATION_API_PATH                   = "/api/auth/authorization";
    private static final String AUTH_AUTHORIZATION_FAIL_API_PATH              = "/api/auth/authorization/fail";
    private static final String AUTH_AUTHORIZATION_ISSUE_API_PATH             = "/api/auth/authorization/issue";
    private static final String AUTH_TOKEN_API_PATH                           = "/api/auth/token";
    private static final String AUTH_TOKEN_CREATE_API_PATH                    = "/api/auth/token/create";
    private static final String AUTH_TOKEN_FAIL_API_PATH                      = "/api/auth/token/fail";
    private static final String AUTH_TOKEN_GET_LIST_API_PATH                  = "/api/auth/token/get/list";
    private static final String AUTH_TOKEN_ISSUE_API_PATH                     = "/api/auth/token/issue";
    private static final String AUTH_TOKEN_UPDATE_API_PATH                    = "/api/auth/token/update";
    private static final String AUTH_REVOCATION_API_PATH                      = "/api/auth/revocation";
    private static final String AUTH_USERINFO_API_PATH                        = "/api/auth/userinfo";
    private static final String AUTH_USERINFO_ISSUE_API_PATH                  = "/api/auth/userinfo/issue";
    private static final String AUTH_INTROSPECTION_API_PATH                   = "/api/auth/introspection";
    private static final String AUTH_INTROSPECTION_STANDARD_API_PATH          = "/api/auth/introspection/standard";
    private static final String SERVICE_CONFIGURATION_API_PATH                = "/api/service/configuration";
    private static final String SERVICE_CREATE_API_PATH                       = "/api/service/create";
    private static final String SERVICE_DELETE_API_PATH                       = "/api/service/delete/%d";
    private static final String SERVICE_GET_API_PATH                          = "/api/service/get/%d";
    private static final String SERVICE_GET_LIST_API_PATH                     = "/api/service/get/list";
    private static final String SERVICE_JWKS_GET_API_PATH                     = "/api/service/jwks/get";
    private static final String SERVICE_UPDATE_API_PATH                       = "/api/service/update/%d";
    private static final String CLIENT_CREATE_API_PATH                        = "/api/client/create";
    private static final String CLIENT_REGISTER_API_PATH                      = "/api/client/register";
    private static final String CLIENT_DELETE_API_PATH                        = "/api/client/delete/%d";
    private static final String CLIENT_GET_API_PATH                           = "/api/client/get/%d";
    private static final String CLIENT_GET_LIST_API_PATH                      = "/api/client/get/list";
    private static final String CLIENT_SECRET_REFRESH_API_PATH                = "/api/client/secret/refresh/%s";
    private static final String CLIENT_SECRET_UPDATE_API_PATH                 = "/api/client/secret/update/%s";
    private static final String CLIENT_UPDATE_API_PATH                        = "/api/client/update/%d";
    private static final String REQUESTABLE_SCOPES_DELETE_API_PATH            = "/api/client/extension/requestable_scopes/delete/%d";
    private static final String REQUESTABLE_SCOPES_GET_API_PATH               = "/api/client/extension/requestable_scopes/get/%d";
    private static final String REQUESTABLE_SCOPES_UPDATE_API_PATH            = "/api/client/extension/requestable_scopes/update/%d";
    private static final String GRANTED_SCOPES_GET_API_PATH                   = "/api/client/granted_scopes/get/%d";
    private static final String GRANTED_SCOPES_DELETE_API_PATH                = "/api/client/granted_scopes/delete/%d";
    private static final String CLIENT_AUTHORIZATION_DELETE_API_PATH          = "/api/client/authorization/delete/%d";
    private static final String CLIENT_AUTHORIZATION_GET_LIST_API_PATH        = "/api/client/authorization/get/list";
    private static final String CLIENT_AUTHORIZATION_UPDATE_API_PATH          = "/api/client/authorization/update/%d";
    private static final String JOSE_VERIFY_API_PATH                          = "/api/jose/verify";
    private static final String BACKCHANNEL_AUTHENTICATION_API_PATH           = "/api/backchannel/authentication";
    private static final String BACKCHANNEL_AUTHENTICATION_COMPLETE_API_PATH  = "/api/backchannel/authentication/complete";
    private static final String BACKCHANNEL_AUTHENTICATION_FAIL_API_PATH      = "/api/backchannel/authentication/fail";
    private static final String BACKCHANNEL_AUTHENTICATION_ISSUE_API_PATH     = "/api/backchannel/authentication/issue";

    private final String mBaseUrl;
    private final String mServiceOwnerAuth;
    private final String mServiceAuth;
    private final Settings mSettings;
    private javax.ws.rs.client.Client mJaxRsClient;

    private Object mConnectionTimeoutLock = new Object();
    private int mCurrentConnectionTimeout;

    private Object mReadTimeoutLock = new Object();
    private int mCurrentReadTimeout;

    private ClientBuilder jaxRsClientBuilder;

    /**
     * The constructor with an instance of {@link AuthleteConfiguration}.
     *
     * <p>
     * The existence of a constructor of this type is a required by
     * {@link com.authlete.common.api.AuthleteApiFactory AuthleteApiFactory}.
     * </p>
     *
     * @param configuration
     *         An instance of {@link AuthleteConfiguration}.
     */
    public AuthleteApiImpl(AuthleteConfiguration configuration)
    {
        if (configuration == null)
        {
            throw new IllegalArgumentException("configuration is null.");
        }

        mBaseUrl          = configuration.getBaseUrl();
        mServiceOwnerAuth = createServiceOwnerCredentials(configuration);
        mServiceAuth      = createServiceCredentials(configuration);
        mSettings         = new Settings();
    }

    /**
     * Create an authorization header for the service owner.
     */
    private String createServiceOwnerCredentials(AuthleteConfiguration configuration)
    {
        if (configuration.getServiceOwnerAccessToken() != null)
        {
            return "Bearer " + configuration.getServiceOwnerAccessToken();
        }
        else
        {
            String key    = configuration.getServiceOwnerApiKey();
            String secret = configuration.getServiceOwnerApiSecret();

            return new BasicCredentials(key, secret).format();
        }
    }

    /**
     * Create an authorization header for the service.
     */
    private String createServiceCredentials(AuthleteConfiguration configuration)
    {
        if (configuration.getServiceAccessToken() != null)
        {
            return "Bearer " + configuration.getServiceAccessToken();
        }
        else
        {
            String key    = configuration.getServiceApiKey();
            String secret = configuration.getServiceApiSecret();

            return new BasicCredentials(key, secret).format();
        }
    }

    /**
     * Get an instance of JAX-RS client.
     */
    private javax.ws.rs.client.Client getJaxRsClient()
    {
        // If a JAX-RS client has not been created yet.
        if (mJaxRsClient == null)
        {
            // Create a JAX-RS client.
            javax.ws.rs.client.Client client = createJaxRsClient();

            synchronized (this)
            {
                if (mJaxRsClient == null)
                {
                    mJaxRsClient = client;
                }
            }
        }

        // Set a connection timeout.
        setConnectionTimeout(mJaxRsClient);

        // Set a read timeout.
        setReadTimeout(mJaxRsClient);

        return mJaxRsClient;
    }

    /**
     * Create an instance of JAX-RS client.
     */
    private javax.ws.rs.client.Client createJaxRsClient()
    {
        if (getJaxRsClientBuilder() != null) {
            // if we have a builder configured, use it
            return getJaxRsClientBuilder().build();
        } else {
            // otherwise just use the system discovered default
            return ClientBuilder.newClient();
        }
    }

    /**
     * Set a connection timeout.
     */
    private void setConnectionTimeout(javax.ws.rs.client.Client client)
    {
        // The timeout value.
        int timeout = mSettings.getConnectionTimeout();

        synchronized (mConnectionTimeoutLock)
        {
            if (mCurrentConnectionTimeout == timeout)
            {
                // The given value is the same as the current one.
                // Let's skip calling property() method.
                return;
            }

            // The given value is different from the current value.
            // Let's update the configuration.
            mCurrentConnectionTimeout = timeout;
        }

        //----------------------------------------------------------------------
        // Note that there was no standardized way to set the connection timeout
        // before JAX-RS API 2.1 (Java EE 8) (cf. ClientBuilder.connectTimeout).
        //----------------------------------------------------------------------

        // Convert int to Integer before calling property() method multiple times
        // in order to reduce the number of object creation by autoboxing.
        Integer value = Integer.valueOf(timeout);

        // For Jersey
        client.property("jersey.config.client.connectTimeout", value);

        // For Apache CXF
        client.property("http.connection.timeout", value);

        // For WebSphere (8.5.5.7+)
        client.property("com.ibm.ws.jaxrs.client.connection.timeout", value);
    }

    /**
     * Set a read timeout.
     */
    private void setReadTimeout(javax.ws.rs.client.Client client)
    {
        // The timeout value.
        int timeout = mSettings.getReadTimeout();

        synchronized (mReadTimeoutLock)
        {
            if (mCurrentReadTimeout == timeout)
            {
                // The given value is the same as the current one.
                // Let's skip calling property() method.
                return;
            }

            // The given value is different from the current value.
            // Let's update the configuration.
            mCurrentReadTimeout = timeout;
        }

        //----------------------------------------------------------------------
        // Note that there was no standardized way to set the read timeout
        // before JAX-RS API 2.1 (Java EE 8) (cf. ClientBuilder.readTimeout).
        //----------------------------------------------------------------------

        // Convert int to Integer before calling property() method multiple times
        // in order to reduce the number of object creation by autoboxing.
        Integer value = Integer.valueOf(timeout);

        // For Jersey
        client.property("jersey.config.client.readTimeout", value);

        // For Apache CXF
        client.property("http.receive.timeout", value);

        // For WebSphere (8.5.5.7+)
        client.property("com.ibm.ws.jaxrs.client.receive.timeout", value);
    }

    private WebTarget getTarget()
    {
        return getJaxRsClient().target(mBaseUrl);
    }

    /**
     * Execute an Authlete API call.
     */
    private <TResponse> TResponse executeApiCall(AuthleteApiCall<TResponse> apiCall) throws AuthleteApiException
    {
        try
        {
            // Call the Authlete API.
            return apiCall.call();
        }
        catch (WebApplicationException e)
        {
            // Throw an exception with HTTP response information.
            throw createApiException(e, e.getResponse());
        }
        catch (ResponseProcessingException e)
        {
            // Throw an exception with HTTP response information.
            throw createApiException(e, e.getResponse());
        }
        catch (Throwable t)
        {
            // Throw an exception without HTTP response information.
            throw createApiException(t, null);
        }
    }

    /**
     * Create an {@link AuthleteApiException} instance.
     */
    private AuthleteApiException createApiException(Throwable cause, Response response)
    {
        // Error message.
        String message = cause.getMessage();

        if (response == null)
        {
            // Create an exception without HTTP response information.
            return new AuthleteApiException(message, cause);
        }

        // Status code and status message.
        int statusCode = 0;
        String statusMessage = null;

        // Get the status information.
        StatusType type = response.getStatusInfo();
        if (type != null)
        {
            statusCode    = type.getStatusCode();
            statusMessage = type.getReasonPhrase();
        }

        // Response body.
        String responseBody = null;

        // If the response has response body.
        if (hasEntity(response))
        {
            // Get the response body.
            responseBody = extractResponseBody(response);
        }

        // Response headers.
        Map<String, List<String>> headers = response.getStringHeaders();

        // Create an exception with HTTP response information.
        return new AuthleteApiException(message, cause, statusCode, statusMessage, responseBody, headers);
    }

    private boolean hasEntity(Response response)
    {
        try
        {
            // True if there is an entity available in the response.
            return response.hasEntity();
        }
        catch (IllegalStateException e)
        {
            // IllegalStateException is thrown in case the response has been closed.
            // A typical error message is "Entity input stream has already been closed."
            // Anyway, an entity is not available.
            return false;
        }
    }

    private String extractResponseBody(Response response)
    {
        try
        {
            // Convert the entity body into a String.
            return response.readEntity(String.class);
        }
        catch (Exception e)
        {
            // Failed to convert the entity body into a String.
            e.printStackTrace();

            // Response body is not available.
            return null;
        }
    }

    private <TResponse> TResponse callGetApi(
            String auth, String path, Class<TResponse> responseClass, Map<String, Object[]> params)
    {
        WebTarget webTarget = getTarget().path(path);

        if (params != null)
        {
            for (Map.Entry<String, Object[]> param : params.entrySet())
            {
                webTarget = webTarget.queryParam(param.getKey(), param.getValue());
            }
        }

        return webTarget
                .request(APPLICATION_JSON_TYPE)
                .header(AUTHORIZATION, auth)
                .get(responseClass);
    }

    private <TResponse> TResponse callServiceOwnerGetApi(
            String path, Class<TResponse> responseClass, Map<String, Object[]> params)
    {
        return callGetApi(mServiceOwnerAuth, path, responseClass, params);
    }

    private <TResponse> TResponse callServiceGetApi(
            String path, Class<TResponse> responseClass, Map<String, Object[]> params)
    {
        return callGetApi(mServiceAuth, path, responseClass, params);
    }

    private Void callDeleteApi(String auth, String path)
    {
        getTarget()
            .path(path)
            .request()
            .header(AUTHORIZATION, auth)
            .delete();

        return null;
    }

    private Void callServiceOwnerDeleteApi(String path)
    {
        return callDeleteApi(mServiceOwnerAuth, path);
    }

    private Void callServiceDeleteApi(String path)
    {
        return callDeleteApi(mServiceAuth, path);
    }

    private <TResponse> TResponse callPostApi(String auth, String path, Object request, Class<TResponse> responseClass)
    {
        return getTarget()
                .path(path)
                .request(APPLICATION_JSON_TYPE)
                .header(AUTHORIZATION, auth)
                .post(Entity.entity(request, JSON_UTF8_TYPE), responseClass);
    }

    private <TResponse> TResponse callServiceOwnerPostApi(String path, Object request, Class<TResponse> responseClass)
    {
        return callPostApi(mServiceOwnerAuth, path, request, responseClass);
    }

    private <TResponse> TResponse callServicePostApi(String path, Object request, Class<TResponse> responseClass)
    {
        return callPostApi(mServiceAuth, path, request, responseClass);
    }

    private static abstract class ApiCaller<TResponse> implements AuthleteApiCall<TResponse>
    {
        protected final String mPath;
        protected final Object mRequest;
        protected final Class<TResponse> mResponseClass;
        protected final Map<String, Object[]> mParams = new LinkedHashMap<>();

        ApiCaller(Class<TResponse> responseClass, Object request, String path)
        {
            mPath          = path;
            mRequest       = request;
            mResponseClass = responseClass;
        }

        ApiCaller(Class<TResponse> responseClass, Object request, String format, Object... args)
        {
            this(responseClass, request, String.format(format, args));
        }

        public ApiCaller<TResponse> addParam(String name, Object... values)
        {
            mParams.put(name, values);

            return this;
        }
    }

    private class ServiceOwnerDeleteApiCaller extends ApiCaller<Void>
    {
        ServiceOwnerDeleteApiCaller(String path)
        {
            super(Void.class, null, path);
        }

        ServiceOwnerDeleteApiCaller(String format, Object... args)
        {
            super(Void.class, null, format, args);
        }

        @Override
        public Void call()
        {
            return callServiceOwnerDeleteApi(mPath);
        }
    }

    private class ServiceOwnerGetApiCaller<TResponse> extends ApiCaller<TResponse>
    {
        ServiceOwnerGetApiCaller(Class<TResponse> responseClass, String path)
        {
            super(responseClass, null, path);
        }

        ServiceOwnerGetApiCaller(Class<TResponse> responseClass, String format, Object... args)
        {
            super(responseClass, null, format, args);
        }

        @Override
        public TResponse call()
        {
            return callServiceOwnerGetApi(mPath, mResponseClass, mParams);
        }
    }

    private class ServiceOwnerPostApiCaller<TResponse> extends ApiCaller<TResponse>
    {
        ServiceOwnerPostApiCaller(Class<TResponse> responseClass, Object request, String path)
        {
            super(responseClass, request, path);
        }

        ServiceOwnerPostApiCaller(Class<TResponse> responseClass, Object request, String format, Object... args)
        {
            super(responseClass, request, format, args);
        }

        @Override
        public TResponse call()
        {
            return callServiceOwnerPostApi(mPath, mRequest, mResponseClass);
        }
    }

    private class ServiceDeleteApiCaller extends ApiCaller<Void>
    {
        ServiceDeleteApiCaller(String path)
        {
            super(Void.class, null, path);
        }

        ServiceDeleteApiCaller(String format, Object... args)
        {
            super(Void.class, null, format, args);
        }

        @Override
        public Void call()
        {
            return callServiceDeleteApi(mPath);
        }
    }

    private class ServiceGetApiCaller<TResponse> extends ApiCaller<TResponse>
    {
        ServiceGetApiCaller(Class<TResponse> responseClass, String path)
        {
            super(responseClass, null, path);
        }

        ServiceGetApiCaller(Class<TResponse> responseClass, String format, Object... args)
        {
            super(responseClass, null, format, args);
        }

        @Override
        public TResponse call()
        {
            return callServiceGetApi(mPath, mResponseClass, mParams);
        }
    }

    private class ServicePostApiCaller<TResponse> extends ApiCaller<TResponse>
    {
        ServicePostApiCaller(Class<TResponse> responseClass, Object request, String path)
        {
            super(responseClass, request, path);
        }

        ServicePostApiCaller(Class<TResponse> responseClass, Object request, String format, Object... args)
        {
            super(responseClass, request, format, args);
        }

        @Override
        public TResponse call()
        {
            return callServicePostApi(mPath, mRequest, mResponseClass);
        }
    }

    /**
     * Call {@code /api/auth/authorization} API.
     */
    @Override
    public AuthorizationResponse authorization(AuthorizationRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<AuthorizationResponse>(
                        AuthorizationResponse.class, request, AUTH_AUTHORIZATION_API_PATH));
    }

    /**
     * Call {@code /api/auth/authorization/fail} API.
     */
    @Override
    public AuthorizationFailResponse authorizationFail(AuthorizationFailRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<AuthorizationFailResponse>(
                        AuthorizationFailResponse.class, request, AUTH_AUTHORIZATION_FAIL_API_PATH));
    }

    /**
     * Call {@code /api/auth/authorization/issue} API.
     */
    @Override
    public AuthorizationIssueResponse authorizationIssue(AuthorizationIssueRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<AuthorizationIssueResponse>(
                        AuthorizationIssueResponse.class, request, AUTH_AUTHORIZATION_ISSUE_API_PATH));
    }

    /**
     * Call {@code /api/auth/token} API.
     */
    @Override
    public TokenResponse token(TokenRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<TokenResponse>(
                        TokenResponse.class, request, AUTH_TOKEN_API_PATH));
    }

    /**
     * Call {@code /api/auth/token/create} API.
     */
    @Override
    public TokenCreateResponse tokenCreate(TokenCreateRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<TokenCreateResponse>(
                        TokenCreateResponse.class, request, AUTH_TOKEN_CREATE_API_PATH));
    }

    /**
     * Call {@code /api/auth/token/fail} API.
     */
    @Override
    public TokenFailResponse tokenFail(TokenFailRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<TokenFailResponse>(
                        TokenFailResponse.class, request, AUTH_TOKEN_FAIL_API_PATH));
    }

    /**
     * Call {@code /api/auth/token/issue} API.
     */
    @Override
    public TokenIssueResponse tokenIssue(TokenIssueRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<TokenIssueResponse>(
                        TokenIssueResponse.class, request, AUTH_TOKEN_ISSUE_API_PATH));
    }

    /**
     * Call {@code /api/auth/token/update} API.
     */
    @Override
    public TokenUpdateResponse tokenUpdate(TokenUpdateRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<TokenUpdateResponse>(
                        TokenUpdateResponse.class, request, AUTH_TOKEN_UPDATE_API_PATH));
    }

    @Override
    public TokenListResponse getTokenList() throws AuthleteApiException
    {
        return getTokenList(null, null, 0, 0, false);
    }

    @Override
    public TokenListResponse getTokenList(String clientIdentifier, String subject) throws AuthleteApiException
    {
        return getTokenList(clientIdentifier, subject, 0, 0, false);
    }

    @Override
    public TokenListResponse getTokenList(int start, int end) throws AuthleteApiException
    {
        return getTokenList(null, null, start, end, true);
    }

    @Override
    public TokenListResponse getTokenList(String clientIdentifier, String subject, int start, int end) throws AuthleteApiException
    {
        return getTokenList(clientIdentifier, subject, start, end, true);
    }

    private TokenListResponse getTokenList(
            final String clientIdentifier, final String subject,
            final int start, final int end, final boolean rangeGiven) throws AuthleteApiException
    {
        return executeApiCall(new AuthleteApiCall<TokenListResponse>() {
            @Override
            public TokenListResponse call()
            {
                return callGetTokenList(clientIdentifier, subject, start, end, rangeGiven);
            }
        });
    }

    private TokenListResponse callGetTokenList(
            String clientIdentifier, String subject, int start, int end, boolean rangeGiven)
    {
        WebTarget target = getTarget().path(AUTH_TOKEN_GET_LIST_API_PATH);

        if (clientIdentifier != null)
        {
            target = target.queryParam("clientIdentifier", clientIdentifier);
        }

        if (subject != null)
        {
            target = target.queryParam("subject", subject);
        }

        if (rangeGiven)
        {
            target = target.queryParam("start", start).queryParam("end", end);
        }

        return target
                .request(APPLICATION_JSON_TYPE)
                .header(AUTHORIZATION, mServiceAuth)
                .get(TokenListResponse.class);
    }

    /**
     * Call {@code /api/auth/revocation} API.
     */
    @Override
    public RevocationResponse revocation(RevocationRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<RevocationResponse>(
                        RevocationResponse.class, request, AUTH_REVOCATION_API_PATH));
    }

    /**
     * Call {@code /api/auth/userinfo} API.
     */
    @Override
    public UserInfoResponse userinfo(UserInfoRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<UserInfoResponse>(
                        UserInfoResponse.class, request, AUTH_USERINFO_API_PATH));
    }

    /**
     * Call {@code /api/auth/userinfo/issue} API.
     */
    @Override
    public UserInfoIssueResponse userinfoIssue(UserInfoIssueRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<UserInfoIssueResponse>(
                        UserInfoIssueResponse.class, request, AUTH_USERINFO_ISSUE_API_PATH));
    }

    /**
     * Call {@code /api/auth/introspection} API.
     */
    @Override
    public IntrospectionResponse introspection(IntrospectionRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<IntrospectionResponse>(
                        IntrospectionResponse.class, request, AUTH_INTROSPECTION_API_PATH));
    }

    /**
     * Call {@code /api/auth/introspection/standard} API.
     */
    @Override
    public StandardIntrospectionResponse standardIntrospection(
            StandardIntrospectionRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<StandardIntrospectionResponse>(
                        StandardIntrospectionResponse.class, request, AUTH_INTROSPECTION_STANDARD_API_PATH));
    }

    /**
     * Call {@code /api/service/create} API.
     */
    @Override
    public Service createService(Service service) throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceOwnerPostApiCaller<Service>(
                        Service.class, service, SERVICE_CREATE_API_PATH));
    }

    /**
     * Call {@code /api/service/create} API.
     */
    @Override
    @Deprecated
    public Service createServie(Service service) throws AuthleteApiException
    {
        return createService(service);
    }

    /**
     * Call <code>/api/service/delete/<i>{serviceApiKey}</i></code> API.
     */
    @Override
    public void deleteService(long apiKey) throws AuthleteApiException
    {
        executeApiCall(
                new ServiceOwnerDeleteApiCaller(
                        SERVICE_DELETE_API_PATH, apiKey));
    }

    /**
     * Call <code>/api/service/get/<i>{serviceApiKey}</i></code> API.
     */
    @Override
    public Service getService(long apiKey) throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceOwnerGetApiCaller<Service>(
                        Service.class, SERVICE_GET_API_PATH, apiKey));
    }

    /**
     * Call {@code /api/service/get/list} API.
     */
    @Override
    public ServiceListResponse getServiceList() throws AuthleteApiException
    {
        return getServiceList(0, 0, false);
    }

    @Override
    public ServiceListResponse getServiceList(int start, int end) throws AuthleteApiException
    {
        return getServiceList(start, end, true);
    }

    private ServiceListResponse getServiceList(
            final int start, final int end, final boolean rangeGiven) throws AuthleteApiException
    {
        return executeApiCall(new AuthleteApiCall<ServiceListResponse>() {
            @Override
            public ServiceListResponse call()
            {
                return callGetServiceList(start, end, rangeGiven);
            }
        });
    }

    /**
     * Call <code>/service/get/list</code>.
     */
    private ServiceListResponse callGetServiceList(int start, int end, boolean rangeGiven)
    {
        WebTarget target = getTarget().path(SERVICE_GET_LIST_API_PATH);

        if (rangeGiven)
        {
            target = target.queryParam("start", start).queryParam("end", end);
        }

        return target
                .request(APPLICATION_JSON_TYPE)
                .header(AUTHORIZATION, mServiceOwnerAuth)
                .get(ServiceListResponse.class);
    }

    /**
     * Call <code>/api/service/update/<i>{serviceApiKey}</i></code> API.
     */
    @Override
    public Service updateService(final Service service) throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceOwnerPostApiCaller<Service>(
                        Service.class, service, SERVICE_UPDATE_API_PATH, service.getApiKey()));
    }

    /**
     * Call {@code /api/service/jwks/get} API
     */
    @Override
    public String getServiceJwks() throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceGetApiCaller<String>(
                        String.class, SERVICE_JWKS_GET_API_PATH));
    }

    /**
     * Call {@code /api/service/jwks/get} API
     */
    @Override
    public String getServiceJwks(boolean pretty, boolean includePrivateKeys) throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceGetApiCaller<String>(
                        String.class, SERVICE_JWKS_GET_API_PATH)
                .addParam("pretty", pretty)
                .addParam("includePrivateKeys", includePrivateKeys));
    }

    /**
     * Call {@code /api/service/configuration} API
     */
    @Override
    public String getServiceConfiguration() throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceGetApiCaller<String>(
                        String.class, SERVICE_CONFIGURATION_API_PATH));
    }

    /**
     * Call {@code /api/service/configuration} API
     */
    @Override
    public String getServiceConfiguration(boolean pretty) throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceGetApiCaller<String>(
                        String.class, SERVICE_CONFIGURATION_API_PATH)
                .addParam("pretty", pretty));
    }

    /**
     * Call {@code /api/client/create} API.
     */
    @Override
    public Client createClient(Client client) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<Client>(
                        Client.class, client, CLIENT_CREATE_API_PATH));
    }

    /**
     * Call {@code /api/client/register} API.
     */
    @Override
    public ClientRegistrationResponse registerClient(ClientRegistrationRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<ClientRegistrationResponse>(
                        ClientRegistrationResponse.class, request, CLIENT_REGISTER_API_PATH));
    }

    /**
     * Call <code>/api/client/delete/<i>{clientId}</i></code> API.
     */
    @Override
    public void deleteClient(final long clientId) throws AuthleteApiException
    {
        executeApiCall(
                new ServiceDeleteApiCaller(
                        CLIENT_DELETE_API_PATH, clientId));
    }

    /**
     * Call <code>/api/client/get/<i>{clientId}</i></code> API.
     */
    @Override
    public Client getClient(final long clientId) throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceGetApiCaller<Client>(
                        Client.class, CLIENT_GET_API_PATH, clientId));
    }

    /**
     * Call {@code /api/client/get/list} API.
     */
    @Override
    public ClientListResponse getClientList() throws AuthleteApiException
    {
        return getClientList(null, 0, 0, false);
    }

    @Override
    public ClientListResponse getClientList(String developer) throws AuthleteApiException
    {
        return getClientList(developer, 0, 0, false);
    }

    @Override
    public ClientListResponse getClientList(int start, int end) throws AuthleteApiException
    {
        return getClientList(null, start, end, true);
    }

    @Override
    public ClientListResponse getClientList(String developer, int start, int end) throws AuthleteApiException
    {
        return getClientList(developer, start, end, true);
    }

    private ClientListResponse getClientList(
            final String developer, final int start, final int end, final boolean rangeGiven) throws AuthleteApiException
    {
        return executeApiCall(new AuthleteApiCall<ClientListResponse>() {
            @Override
            public ClientListResponse call()
            {
                return callGetClientList(developer, start, end, rangeGiven);
            }
        });
    }

    private ClientListResponse callGetClientList(String developer, int start, int end, boolean rangeGiven)
    {
        WebTarget target = getTarget().path(CLIENT_GET_LIST_API_PATH);

        if (developer != null)
        {
            target = target.queryParam("developer", developer);
        }

        if (rangeGiven)
        {
            target = target.queryParam("start", start).queryParam("end", end);
        }

        return target
                .request(APPLICATION_JSON_TYPE)
                .header(AUTHORIZATION, mServiceAuth)
                .get(ClientListResponse.class);
    }

    /**
     * Call <code>/api/client/update/<i>{clientId}</i></code> API.
     */
    @Override
    public Client updateClient(Client client) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<Client>(
                        Client.class, client, CLIENT_UPDATE_API_PATH, client.getClientId()));
    }

    /**
     * Call <code>/api/client/extension/requestable_scopes/get/<i>{clientId}</i></code> API.
     */
    @Override
    public String[] getRequestableScopes(long clientId) throws AuthleteApiException
    {
        // Call the API.
        RequestableScopes response = executeApiCall(
                new ServiceGetApiCaller<RequestableScopes>(
                        RequestableScopes.class, REQUESTABLE_SCOPES_GET_API_PATH, clientId));

        if (response != null)
        {
            // Extract 'requestableScopes' from the response.
            return response.getRequestableScopes();
        }
        else
        {
            return null;
        }
    }

    @Override
    public String[] setRequestableScopes(long clientId, String[] scopes) throws AuthleteApiException
    {
        // Prepare a request body.
        RequestableScopes request = new RequestableScopes().setRequestableScopes(scopes);

        // Call the API.
        RequestableScopes response = executeApiCall(
                new ServicePostApiCaller<RequestableScopes>(
                        RequestableScopes.class, request, REQUESTABLE_SCOPES_UPDATE_API_PATH, clientId));

        if (response != null)
        {
            // Extract 'requestableScopes' from the response.
            return response.getRequestableScopes();
        }
        else
        {
            return null;
        }
    }

    /**
     * Call <code>/api/client/extension/requestable_scopes/delete/<i>{clientId}</i></code> API.
     */
    @Override
    public void deleteRequestableScopes(long clientId) throws AuthleteApiException
    {
        executeApiCall(
                new ServiceDeleteApiCaller(
                        REQUESTABLE_SCOPES_DELETE_API_PATH, clientId));
    }

    @Override
    public GrantedScopesGetResponse getGrantedScopes(long clientId, String subject)
    {
        // Prepare a request body.
        GrantedScopesRequest request = new GrantedScopesRequest(subject);

        // Call the API.
        return executeApiCall(
                new ServicePostApiCaller<GrantedScopesGetResponse>(
                        GrantedScopesGetResponse.class, request, GRANTED_SCOPES_GET_API_PATH, clientId));
    }

    @Override
    public void deleteGrantedScopes(long clientId, String subject)
    {
        // Prepare a request body.
        GrantedScopesRequest request = new GrantedScopesRequest(subject);

        executeApiCall(
                new ServicePostApiCaller<ApiResponse>(
                        ApiResponse.class, request, GRANTED_SCOPES_DELETE_API_PATH, clientId));
    }

    private static final class GrantedScopesRequest
    {
        private String subject;

        public GrantedScopesRequest(String subject)
        {
            this.subject = subject;
        }

        @SuppressWarnings("unused")
        public String getSubject()
        {
            return subject;
        }

        @SuppressWarnings("unused")
        public void setSubject(String subject)
        {
            this.subject = subject;
        }
    }

    @Override
    public void deleteClientAuthorization(long clientId, String subject) throws AuthleteApiException
    {
        // Prepare a request body.
        ClientAuthorizationDeleteRequest request = new ClientAuthorizationDeleteRequest(subject);

        executeApiCall(
                new ServicePostApiCaller<ApiResponse>(
                        ApiResponse.class, request, CLIENT_AUTHORIZATION_DELETE_API_PATH, clientId));
    }

    @Override
    public AuthorizedClientListResponse getClientAuthorizationList(ClientAuthorizationGetListRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<AuthorizedClientListResponse>(
                        AuthorizedClientListResponse.class, request, CLIENT_AUTHORIZATION_GET_LIST_API_PATH));
    }

    @Override
    public void updateClientAuthorization(long clientId, ClientAuthorizationUpdateRequest request) throws AuthleteApiException
    {
        executeApiCall(
                new ServicePostApiCaller<ApiResponse>(
                        ApiResponse.class, request, CLIENT_AUTHORIZATION_UPDATE_API_PATH, clientId));
    }

    @Override
    public ClientSecretRefreshResponse refreshClientSecret(long clientId) throws AuthleteApiException
    {
        return refreshClientSecret(String.valueOf(clientId));
    }

    @Override
    public ClientSecretRefreshResponse refreshClientSecret(String clientIdentifier) throws AuthleteApiException
    {
        return executeApiCall(
                new ServiceGetApiCaller<ClientSecretRefreshResponse>(
                        ClientSecretRefreshResponse.class,
                        CLIENT_SECRET_REFRESH_API_PATH, clientIdentifier));
    }

    @Override
    public ClientSecretUpdateResponse updateClientSecret(long clientId, String clientSecret) throws AuthleteApiException
    {
        return updateClientSecret(String.valueOf(clientId), clientSecret);
    }

    @Override
    public ClientSecretUpdateResponse updateClientSecret(String clientIdentifier, String clientSecret) throws AuthleteApiException
    {
        // Prepare a request body. setClientSecret(String) method
        // throws IllegalArgumentException if the given client secret
        // does not comply with the format.
        ClientSecretUpdateRequest request
            = new ClientSecretUpdateRequest().setClientSecret(clientSecret);

        return executeApiCall(
                new ServicePostApiCaller<ClientSecretUpdateResponse>(
                        ClientSecretUpdateResponse.class, request,
                        CLIENT_SECRET_UPDATE_API_PATH, clientIdentifier));
    }

    @Override
    public Settings getSettings()
    {
        return mSettings;
    }

    /**
     * Call {@code /api/jose/verify} API.
     */
    @Override
    public JoseVerifyResponse verifyJose(JoseVerifyRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<JoseVerifyResponse>(
                        JoseVerifyResponse.class, request, JOSE_VERIFY_API_PATH));
    }

    public ClientBuilder getJaxRsClientBuilder()
    {
        return jaxRsClientBuilder;
    }

    public void setJaxRsClientBuilder(ClientBuilder jaxRsClientBuilder)
    {
        this.jaxRsClientBuilder = jaxRsClientBuilder;
    }

    /**
     * Call {@code /api/backchannel/authentication} API.
     */
    @Override
    public BackchannelAuthenticationResponse backchannelAuthentication(BackchannelAuthenticationRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<BackchannelAuthenticationResponse>(
                        BackchannelAuthenticationResponse.class, request, BACKCHANNEL_AUTHENTICATION_API_PATH));
    }

    /**
     * Call {@code /api/backchannel/authentication/issue} API.
     */
    @Override
    public BackchannelAuthenticationIssueResponse backchannelAuthenticationIssue(BackchannelAuthenticationIssueRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<BackchannelAuthenticationIssueResponse>(
                        BackchannelAuthenticationIssueResponse.class, request, BACKCHANNEL_AUTHENTICATION_ISSUE_API_PATH));
    }

    /**
     * Call {@code /api/backchannel/authentication/fail} API.
     */
    @Override
    public BackchannelAuthenticationFailResponse backchannelAuthenticationFail(BackchannelAuthenticationFailRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<BackchannelAuthenticationFailResponse>(
                        BackchannelAuthenticationFailResponse.class, request, BACKCHANNEL_AUTHENTICATION_FAIL_API_PATH));
    }

    /**
     * Call {@code /api/backchannel/authentication/complete} API.
     */
    @Override
    public BackchannelAuthenticationCompleteResponse backchannelAuthenticationComplete(BackchannelAuthenticationCompleteRequest request) throws AuthleteApiException
    {
        return executeApiCall(
                new ServicePostApiCaller<BackchannelAuthenticationCompleteResponse>(
                        BackchannelAuthenticationCompleteResponse.class, request, BACKCHANNEL_AUTHENTICATION_COMPLETE_API_PATH));
    }
}


/*
 * Copyright (C) 2016 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.jaxrs.spi.AuthorizationRequestHandlerSpi;

/**
 * A base class for authorization endpoints.
 *
 * @since 1.2
 *
 * @see RFC 6749, 3.1. Authorization Endpoint
 *
 * @see OpenID Connect Core 1.0, 3.1.2. Authorization Endpoint (Authorization Code Flow)
 *
 * @see OpenID Connect Core 1.0, 3.2.2. Authorization Endpoint (Implicit Flow)
 *
 * @see OpenID Connect Core 1.0, 3.3.2. Authorization Endpoint (Hybrid Flow)
 *
 * @author Takahiko Kawasaki
 */
public class BaseAuthorizationEndpoint extends BaseEndpoint
{
    /**
     * Handle an authorization request.
     *
     *

     * This method internally creates a {@link AuthorizationRequestHandler} instance and
     * calls its {@link AuthorizationRequestHandler#handle(MultivaluedMap)} method.
     * Then, this method uses the value returned from the {@code handle()} method
     * as a response from this method.
     *

     *
     *

     * When {@code AuthorizationRequestHandler.handle()} method raises a {@link
     * WebApplicationException}, this method calls {@link #onError(WebApplicationException)
     * onError()} method with the exception. The default implementation of {@code onError()}
     * calls {@code printStackTrace()} of the exception and does nothing else. You
     * can override the method as necessary. After calling {@code onError()} method,
     * this method calls {@code getResponse()} method of the exception and uses the
     * returned value as a response from this method.
     *

     *
     * @param api
     *         An implementation of {@link AuthleteApi}.
     *
     * @param spi
     *         An implementation of {@link AuthorizationRequestHandlerSpi}.
     *
     * @param parameters
     *         Request parameters of the authorization request.
     *
     * @return
     *         A response that should be returned to the client application.
     */
    public Response handle(
            AuthleteApi api, AuthorizationRequestHandlerSpi spi,
            MultivaluedMap<String, String> parameters)
    {
        try
        {
            // Create a handler.
            AuthorizationRequestHandler handler = new AuthorizationRequestHandler(api, spi);

            // Delegate the task to the handler.
            return handler.handle(parameters);
        }
        catch (WebApplicationException e)
        {
            // An error occurred in the handler.
            onError(e);

            // Convert the error to a Response.
            return e.getResponse();
        }
    }
}


/*
 * Copyright (C) 2017 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.dto.StandardIntrospectionResponse;
import com.authlete.common.dto.StandardIntrospectionResponse.Action;

/**
 * Handler for token introspection requests
 * (RFC 7662).
 *
 *

 * In an implementation of introspection endpoint, call {@link
 * #handle(MultivaluedMap) handle()} method and use the response
 * as the response from the endpoint to the client application.
 * {@code handle()} method calls Authlete's {@code
 * /api/auth/introspection/standard} API, receives a response
 * from the API, and dispatches processing according to the
 * {@code action} parameter in the response.
 *

 *
 * @since 2.2
 *
 * @author Takahiko Kawasaki
 */
public class IntrospectionRequestHandler extends BaseHandler
{
    /**
     * Constructor with an implementation of {@link AuthleteApi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     */
    public IntrospectionRequestHandler(AuthleteApi api)
    {
        super(api);
    }

    /**
     * Handle an introspection request (RFC 7662).
     *
     * @param parameters
     *         Request parameters of a token introspection request.
     *
     * @return
     *         A response that should be returned from the endpoint to the
     *         client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle(MultivaluedMap<String, String> parameters) throws WebApplicationException
    {
        try
        {
            // Process the given parameters.
            return process(parameters);
        }
        catch (WebApplicationException e)
        {
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in IntrospectionRequestHandler", t);
        }
    }

    /**
     * Process the parameters of the introspection request.
     */
    private Response process(MultivaluedMap<String, String> parameters)
    {
        // Call Authlete's /api/auth/introspection/standard API.
        StandardIntrospectionResponse response = getApiCaller().callStandardIntrospection(parameters);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        Action action = response.getAction();

        // The content of the response to the client application.
        String content = response.getResponseContent();

        // Dispatch according to the action.
        switch (action)
        {
            case INTERNAL_SERVER_ERROR:
                // 500 Internal Server Error
                return ResponseUtil.internalServerError(content);

            case BAD_REQUEST:
                // 400 Bad Request
                return ResponseUtil.badRequest(content);

            case OK:
                // 200 OK
                return ResponseUtil.ok(content);

            default:
                // This never happens.
                throw getApiCaller().unknownAction("/api/auth/introspection/standard", action);
        }
    }
}


/*
 * Copyright (C) 2016 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import com.authlete.common.api.AuthleteApi;
import com.authlete.common.dto.UserInfoResponse;
import com.authlete.common.dto.UserInfoResponse.Action;
import com.authlete.jaxrs.spi.UserInfoRequestHandlerSpi;

/**
 * Handler for userinfo requests to a UserInfo Endpoint defined in OpenID Connect
 * Core 1.0.
 *
 *

 * In an implementation of userinfo endpoint, call {@link #handle(String)
 * handle()} method and use the response as the response from the endpoint
 * to the client application. {@code handle()} method calls Authlete's
 * {@code /api/auth/userinfo} API and {@code /api/auth/userinfo/issue} API.
 *

 *
 * @since 1.2
 *
 * @author Takahiko Kawasaki
 */
public class UserInfoRequestHandler extends BaseHandler
{
    private static final String CHALLENGE_ON_MISSING_ACCESS_TOKEN
        = "Bearer error=\"invalid_token\",error_description=\""
        + "An access token must be sent as a Bearer Token. "
        + "See OpenID Connect Core 1.0, 5.3.1. UserInfo Request for details.\"";

    /**
     * Implementation of {@link UserInfoRequestHandlerSpi} interface.
     */
    private final UserInfoRequestHandlerSpi mSpi;

    /**
     * Constructor with an implementation of {@link AuthleteApi} interface
     * and an implementation of {@link UserInfoRequestHandlerSpi} interface.
     *
     * @param api
     *         Implementation of {@link AuthleteApi} interface.
     *
     * @param spi
     *         Implementation of {@link UserInfoRequestHandlerSpi} interface.
     */
    public UserInfoRequestHandler(AuthleteApi api, UserInfoRequestHandlerSpi spi)
    {
        super(api);

        mSpi = spi;
    }

    /**
     * Handle a userinfo request to a UserInfo Endpoint defined in OpenID Connect
     * Core 1.0.
     *
     * @param accessToken
     *         An access token.
     *
     * @return
     *         A response that should be returned from the endpoint to the
     *         client application.
     *
     * @throws WebApplicationException
     *         An error occurred.
     */
    public Response handle(String accessToken) throws WebApplicationException
    {
        // If an access token is not available.
        if (accessToken == null)
        {
            // Return "400 Bad Request".
            return ResponseUtil.bearerError(
                    Status.BAD_REQUEST, CHALLENGE_ON_MISSING_ACCESS_TOKEN);
        }

        try
        {
            // Process the userinfo request with the access token.
            return process(accessToken);
        }
        catch (WebApplicationException e)
        {
            throw e;
        }
        catch (Throwable t)
        {
            // Unexpected error.
            throw unexpected("Unexpected error in UserInfoRequestHandler", t);
        }
    }

    /**
     * Process the userinfo request with the access token.
     */
    private Response process(String accessToken)
    {
        // Call Authlete's /api/auth/userinfo API.
        UserInfoResponse response = getApiCaller().callUserInfo(accessToken);

        // 'action' in the response denotes the next action which
        // this service implementation should take.
        Action action = response.getAction();

        // The content of the response to the client application.
        String content = response.getResponseContent();

        // Dispatch according to the action.
        switch (action)
        {
            case INTERNAL_SERVER_ERROR:
                // 500 Internal Server Error
                return ResponseUtil.bearerError(Status.INTERNAL_SERVER_ERROR, content);

            case BAD_REQUEST:
                // 400 Bad Request
                return ResponseUtil.bearerError(Status.BAD_REQUEST, content);

            case UNAUTHORIZED:
                // 401 Unauthorized
                return ResponseUtil.bearerError(Status.UNAUTHORIZED, content);

            case FORBIDDEN:
                // 403 Forbidden
                return ResponseUtil.bearerError(Status.FORBIDDEN, content);

            case OK:
                // Return the user information.
                return getUserInfo(response);

            default:
                // This never happens.
                throw getApiCaller().unknownAction("/api/auth/userinfo", action);
        }
    }

    /**
     * Generate a JSON or a JWT containing user information by calling
     * Authlete's {@code /api/auth/userinfo/issue} API.
     */
    private Response getUserInfo(UserInfoResponse response)
    {
        // Collect claim values of the user.
        Map<String, Object> claims = collectClaims(response.getSubject(), response.getClaims());

        try
        {
            // Generate a JSON or a JWT containing user information
            // by calling Authlete's /api/auth/userinfo/issue API.
            return getApiCaller().userInfoIssue(response.getToken(), claims);
        }
        catch (WebApplicationException e)
        {
            return e.getResponse();
        }
    }

    private Map<String, Object> collectClaims(String subject, String[] claimNames)
    {
        // If no claim is required.
        if (claimNames == null || claimNames.length == 0)
        {
            return null;
        }

        // Let the implementation of UserInfoRequestHandlerSpi prepare
        // claims of the user who is identified by the subject.
        mSpi.prepareUserClaims(subject, claimNames);

        // Claim values.
        Map<String, Object> claims = new HashMap<String, Object>();

        // For each requested claim.
        for (String claimName : claimNames)
        {
            // If the claim name is empty.
            if (claimName == null || claimName.length() == 0)
            {
                continue;
            }

            // Split the claim name into the name part and the tag part.
            String[] elements = claimName.split("#", 2);
            String name = elements[0];
            String tag  = (elements.length == 2) ? elements[1] : null;

            // If the name part is empty.
            if (name == null || name.length() == 0)
            {
                continue;
            }

            // Get the claim value of the claim.
            Object value = mSpi.getUserClaim(name, tag);

            // If the claim value was not obtained.
            if (value == null)
            {
                continue;
            }

            if (tag == null)
            {
                // Just for an edge case where claimName ends with "#".
                claimName = name;
            }

            // Add the pair of the claim name and the claim value.
            claims.put(claimName, value);
        }

        // If no claim value has been obtained.
        if (claims.size() == 0)
        {
            return null;
        }

        // Obtained claim values.
        return claims;
    }
}


/*
 * Copyright (C) 2016 Authlete, Inc.
 *
 * Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
 *
 * 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 com.authlete.jaxrs;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import com.authlete.common.api.AuthleteApi;
import com.authlete.jaxrs.spi.UserInfoRequestHandlerSpi;

/**
 * A base class for userinfo endpoints.
 *
 * @since 1.2
 *
 * @see OpenID Connect Core 1.0, 5.3. UserInfo Endpoint
 *
 * @author Takahiko Kawasaki
 */
public class BaseUserInfoEndpoint extends BaseResourceEndpoint
{
    /**
     * Handle a userinfo request.
     *
     *

     * This method internally creates a {@link UserInfoRequestHandler} instance
     * and calls its {@link UserInfoRequestHandler#handle(String)} method.
     * Then, this method uses the value returned from the {@code handle()} method
     * as a response from this method.
     *

     *
     *

     * When {@code UserInfoRequestHandler.handle()} method raises a {@link
     * WebApplicationException}, this method calls {@link #onError(WebApplicationException)
     * onError()} method with the exception. The default implementation of {@code onError()}
     * calls {@code printStackTrace()} of the exception and does nothing else. You
     * can override the method as necessary. After calling {@code onError()} method,
     * this method calls {@code getResponse()} method of the exception and uses the
     * returned value as a response from this method.
     *

     *
     * @param api
     *         An implementation of {@link AuthleteApi}.
     *
     * @param spi
     *         An implementation of {@link UserInfoRequestHandlerSpi}.
     *
     * @param accessToken
     *         An access token.
     *
     * @return
     *         A response that should be returned to the client application.
     */
    public Response handle(
            AuthleteApi api, UserInfoRequestHandlerSpi spi, String accessToken)
    {
        try
        {
            // Create a handler.
            UserInfoRequestHandler handler = new UserInfoRequestHandler(api, spi);

            // Delegate the task to the handler.
            return handler.handle(accessToken);
        }
        catch (WebApplicationException e)
        {
            // An error occurred in the handler.
            onError(e);

            // Convert the error to a Response.
            return e.getResponse();
        }
    }
}