/**
 * Copyright 2005-2013 Restlet S.A.S.
 * 
 * The contents of this file are subject to the terms of one of the following
 * open source licenses: Apache 2.0 or LGPL 3.0 or LGPL 2.1 or CDDL 1.0 or EPL
 * 1.0 (the "Licenses"). You can select the license that you prefer but you may
 * not use this file except in compliance with one of these Licenses.
 * 
 * You can obtain a copy of the Apache 2.0 license at
 * http://www.opensource.org/licenses/apache-2.0
 * 
 * You can obtain a copy of the LGPL 3.0 license at
 * http://www.opensource.org/licenses/lgpl-3.0
 * 
 * You can obtain a copy of the LGPL 2.1 license at
 * http://www.opensource.org/licenses/lgpl-2.1
 * 
 * You can obtain a copy of the CDDL 1.0 license at
 * http://www.opensource.org/licenses/cddl1
 * 
 * You can obtain a copy of the EPL 1.0 license at
 * http://www.opensource.org/licenses/eclipse-1.0
 * 
 * See the Licenses for the specific language governing permissions and
 * limitations under the Licenses.
 * 
 * Alternatively, you can obtain a royalty free commercial license with less
 * limitations, transferable or non-transferable, directly at
 * http://www.restlet.com/products/restlet-framework
 * 
 * Restlet is a registered trademark of Restlet S.A.S.
 */

package org.restlet.ext.openid;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.openid4java.discovery.Identifier;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.Form;
import org.restlet.data.Reference;
import org.restlet.ext.openid.internal.OpenIdUser;
import org.restlet.ext.openid.internal.XRDS;
import org.restlet.representation.Representation;
import org.restlet.security.User;
import org.restlet.security.Verifier;

/**
 * Verifier that will do remote verification of using a provided
 * openid_identifier. The verifier will search for the openid_identifier in the
 * following three ways
 * <ol>
 * <li>Check the query for an openid_identifier
 * <li>Check the request attribute map for an openid_identifier
 * <li>Use the default openid_identifier if possible
 * </ol>
 * If an openid_identifier is found, it will do a temporary redirect (or return
 * a form) to the identifier to continue the authentication process. Upon
 * successful authentication the verifier will set the User.
 * <p>
 * The verifier can also try to request the following attributes to be returned
 * by the OpenIdProvider - setOptionalAttribute and setRequiredAttribute.
 * </p>
 * <ul>
 * <li>nickname
 * <li>email
 * <li>fullname
 * <li>dob
 * <li>gender
 * <li>postcode
 * <li>country
 * <li>language
 * <li>timezone
 * </ul>
 * 
 * @author Martin Svensson
 */
public class OpenIdVerifier implements Verifier {

    // Known OpenId Providers:
    public static final String PROVIDER_FLICKR = "http://flickr.com";

    public static final String PROVIDER_GOOGLE = "https://www.google.com/accounts/o8/id";

    public static final String PROVIDER_MYOPENID = "https://www.myopenid.com/";

    public static final String PROVIDER_MYSPACE = "http://api.myspace.com/openid";

    public static final String PROVIDER_YAHOO = "http://me.yahoo.com";

    private volatile String defaultProvider;

    private final Set<AttributeExchange> optionalAttributes;

    private final Set<AttributeExchange> requiredAttributes;

    private final RelayingParty rp;

    private volatile boolean useDefault = false;

    /**
     * Default constructor.
     */
    public OpenIdVerifier() {
        this(null, null);
    }

    /**
     * Constructor with a default OpenIdProvider/Identifier.
     * 
     * @param defaultProvider
     *            The default OpenIdProvider/Identifier.
     */
    public OpenIdVerifier(String defaultProvider) {
        this(defaultProvider, null);
    }

    public OpenIdVerifier(String defaultProvider, RelayingParty rp) {
        this.rp = (rp != null) ? rp : new RelayingParty();
        optionalAttributes = new HashSet<AttributeExchange>();
        requiredAttributes = new HashSet<AttributeExchange>();
        if (defaultProvider != null)
            setDefaultProvider(defaultProvider);
    }

    /**
     * Adds required User attribute to retrieve during authentication.
     * 
     * @param attributeName
     *            The name of the attribute. See valid attributes.
     */
    public void addOptionalAttribute(AttributeExchange attributeName) {
        this.optionalAttributes.add(attributeName);
    }

    /**
     * Adds an optional User attribute to retrieve during authentication
     * 
     * @param attributeName
     *            The name of the attribute. See valid attributes.
     */
    public void addRequiredAttribute(AttributeExchange attributeName) {
        this.requiredAttributes.add(attributeName);
    }

    /**
     * Clears the set of optional attributes to retrieve.
     */
    public void clearOptionalAttributes() {
        this.optionalAttributes.clear();
    }

    /**
     * Clears the set of required attributes to retrieve.
     */
    public void clearRequiredAttributes() {
        this.requiredAttributes.clear();
    }

    /**
     * Extracts the Target (openid identifier/provider) from a set of
     * parameters. First by looking at the query string, then in the request
     * attributes and finally if a default provider has been set
     * 
     * @param queryParams
     *            The set of parameters taken from the query string.
     * @param request
     *            The request.
     * @return The target (openid identifier/provider).
     */
    private String getTarget(Form queryParams, Request request) {
        String target = queryParams.getFirstValue("openid_identifier");
        if (target == null)
            target = (String) request.getAttributes().get("openid_identifier");
        if (target == null && useDefault)
            target = defaultProvider;
        if (target == null) {
            Context.getCurrentLogger().fine("no target or return specified");
        }
        return target;
    }

    /**
     * Sets the default provider. Will also set useDefaultProvider to true.
     */
    public void setDefaultProvider(String provider) {
        this.defaultProvider = provider;
        this.useDefault = true;
    }

    /**
     * Indicates if the defaultProvider must be used in case none is provided in
     * the request.
     * 
     * @param useDefault
     *            True if the defaultProvider must be used.
     */
    public void setUseDefaultProvider(boolean useDefault) {
        this.useDefault = useDefault;
    }

    /**
     * Verifies a request. The verifier will be called twice to verify a request
     * since verification is done remotely using callbacks. Also sets the user
     * object.
     * 
     * @return {@link Verifier#RESULT_INVALID} if it fails,
     *         {@link Verifier#RESULT_VALID} if success,
     *         {@link Verifier#RESULT_MISSING} while waiting for a callback
     *         response.
     */
    public int verify(Request request, Response response) {
        Form params = request.getResourceRef().getQueryAsForm();
        if (rp.hasReturnTo(request)) {
            Context.getCurrentLogger().info("handling return");
            Map<AttributeExchange, String> axResp = new HashMap<AttributeExchange, String>();
            try {
                Identifier identifier = rp.verify(axResp, request, true);
                // do some processing
                if (identifier != null && identifier.getIdentifier() != null) {
                    User u = new User(identifier.getIdentifier());
                    if (axResp.size() > 0) {
                        for (Map.Entry<AttributeExchange, String> entry : axResp
                                .entrySet()) {
                            OpenIdUser.setValueFromAX(entry.getKey(),
                                    entry.getValue(), u);
                        }
                    }
                    request.getClientInfo().setUser(u);
                    return RESULT_VALID;
                } else
                    return RESULT_INVALID;
            } catch (Exception e) { // assume rp discovery
                Reference ref = new Reference(request.getResourceRef()
                        .getHostIdentifier()
                        + request.getResourceRef().getPath());
                Context.getCurrentLogger().info("Generating XRDS Response");
                if (params.getFirst("sessionId") != null) {
                    ref.addQueryParameter("sessionId",
                            params.getFirstValue("sessionId"));
                    ref.addQueryParameter("return", "true");
                }
                try {
                    Representation rep = XRDS.returnToXrds(ref.toString());
                    response.setEntity(rep);
                    return RESULT_MISSING;
                } catch (Exception e1) {
                    // should not happen
                    e.printStackTrace();
                    return RESULT_UNKNOWN;
                }
            }

        } else {
            // generate a request
            String target = this.getTarget(params, request);
            Reference ref = new Reference(request.getResourceRef()
                    .getHostIdentifier() + request.getResourceRef().getPath());
            Context.getCurrentLogger().info(
                    "generating a authentication request");
            try {
                rp.authRequest(target, true, true, ref.toString(),
                        optionalAttributes, requiredAttributes, request,
                        response);
                return RESULT_MISSING;
            } catch (Exception e) {
                e.printStackTrace();
                return RESULT_INVALID;
            }
        }

    }

}
