/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2010-2011 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.jersey.samples.contacts.client;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import com.sun.jersey.atom.abdera.ContentHelper;
import com.sun.jersey.samples.contacts.models.Contact;
import com.sun.jersey.samples.contacts.models.EmailAddress;
import com.sun.jersey.samples.contacts.models.User;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;
import org.apache.abdera.Abdera;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.Feed;

/**
 * <p>Simplified Java client for the <code>AtomPub Contacts Server</code>.</p>
 */
public class ContactsClient {


    // ------------------------------------------------------------ Constructors


    /**
     * <p>Construct a fully configured client instance.</p>
     *
     * @param uri URI of the deployed Contacts Service to contact
     * @param username Username of the client calling this service
     * @param password Password of the client calling this service
     */
    public ContactsClient(String uri, String username, String password) {
        ClientConfig config = new DefaultClientConfig();
        Client client = Client.create(config);
        service = client.resource(uri);
        authentication = "Basic " + encodeCredentialsBasic(username, password);
        helper = new ContentHelper(client.getProviders());
    }


    // -------------------------------------------------------- Static Variables


    /**
     * <p>HTTP header for sending authentication credentials.</p>
     */
    private static final String AUTHENTICATION_HEADER = "Authorization";


    /**
     * <p>Default media type for content element.</p>
     */
    private static final MediaType CONTENT_MEDIA_TYPE = MediaType.APPLICATION_XML_TYPE;


    /**
     * <p>Default media type for entry entities.</p>
     */
    private static final MediaType ENTRY_MEDIA_TYPE;
    static {
        Map<String,String> params = new HashMap<String,String>(1);
        params.put("type", "entry");
        ENTRY_MEDIA_TYPE = new MediaType("application", "xml", params);
    }


    /**
     * <p>Default media type for feed entities.</p>
     */
    private static final MediaType FEED_MEDIA_TYPE;
    static {
        Map<String,String> params = new HashMap<String,String>(1);
        params.put("type", "feed");
        FEED_MEDIA_TYPE = new MediaType("application", "xml", params);
    }


    /**
     * <p>Abdera singleton instance for this application.</p>
     */
    private static Abdera abdera = Abdera.getInstance();


    // ------------------------------------------------------ Instance Variables


    /**
     * <p>HTTP authentication header value to include on each request.</p>
     */
    private String authentication = null;


    /**
     * <p>The {@link ContentHelper} instance we will use for converting
     * to or from custom content XML representations.</p>
     */
    private ContentHelper helper = null;


    /**
     * <p>Client proxy for accessing service resources.</p>
     */
    private WebResource service = null;


    // ------------------------------------------------- Contacts Public Methods


    /**
     * <p>Add the specified contact to the set of registered contacts for
     * the specified username.  Return the URI of the newly created contact.</p>
     *
     * @param username Username of the user to add a contact for
     * @param contact Contact information forthe new contact
     *
     * @exception IllegalArgumentException if a contact with this id
     *  is already registered
     * @exception IllegalStateException if the caller is not authorized to
     *  perform this request
     */
    public String createContact(String username, Contact contact) {
        try {
            ClientResponse response = service.
              path("contacts").
              path(username).
              type(ENTRY_MEDIA_TYPE).
              header(AUTHENTICATION_HEADER, authentication).
              post(ClientResponse.class, contact.asEntry());
            return response.getLocation().toString();
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 409) {
                throw new IllegalArgumentException("Username '" + username +
                        "' already has a contact with this id registered");
            } else {
                throw e;
            }
        }
    }


    /**
     * <p>Delete the specified contact for the specified user.</p>
     *
     * @param username Username of the user for which to delete a contact
     * @param id Contact identifier of the contact to delete
     *
     * @exception IllegalArgumentException if a user with this username
     *  or a contact with this id is not currently registered
     * @exception IllegalStateException if the caller is not authorized to
     *  perform this request
     */
    public void deleteContact(String username, String id) {
        try {
            service.
              path("contacts").
              path(username).
              path(id).
              header(AUTHENTICATION_HEADER, authentication).
              delete();
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 404) {
                throw new IllegalArgumentException("Username '" + username +
                        "' and/or contact id '" + id + "' does not exist");
            } else {
                throw e;
            }
        }
    }


    /**
     * <p>Find and return the contact with the specified id for the user
     * with the specified username, if any.</p>
     *
     * @param username Username of the user to return a contact for
     * @param id Id of the contact to return
     *
     * @exception IllegalArgumentException if the specified username does not
     *  identify a valid user or the specified id does not identify a
     *  valid contact for that user
     * @exception IllegalStateException if the caller is not authorized to
     *  perform this request
     */
    public Contact findContact(String username, String id) {
        try {
            Entry entry = service.
                    path("contacts").
                    path(username).
                    path(id).
                    accept(ENTRY_MEDIA_TYPE).
                    header(AUTHENTICATION_HEADER, authentication).
                    get(Entry.class);
            return Contact.fromEntry(entry);
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 404) {
                throw new IllegalArgumentException("Username '" + username +
                        "' and/or contact id '" + id + "' does not exist", e);
            } else {
                throw e;
            }
        }
    }


    /**
     * <p>Find and return all contacts for the specified username
     * registered with the Contacts service.</p>
     *
     * @param username Username for which to retrieve contacts
     *
     * @exception IllegalArgumentException if the specified user
     *  does not exist
     * @exception IllegalStateException if the caller is not authorized to
     *  perform this request
     */
    public List<Contact> findContacts(String username) {
        Feed feed = null;
        try {
            feed = service.
               path("contacts").
               path(username).
               accept(FEED_MEDIA_TYPE).
               header(AUTHENTICATION_HEADER, authentication).
               get(Feed.class);
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 404) {
                throw new IllegalArgumentException("Username '" + username + "' does not exist");
            } else {
                throw e;
            }
        }
        List<Contact> results = new ArrayList<Contact>();
        for (Entry entry : feed.getEntries()) {
            results.add(Contact.fromEntry(entry));
        }
        return results;
    }


    /**
     * <p>Update the specified contact for the specified user.</p>
     *
     * @param username Username for which to update a contact
     * @param contact Contact information to be updated
     *
     * @exception IllegalArgumentException if a contact with this id
     *  is not already registered
     * @exception IllegalStateException if the caller is not authorized to
     *  perform this request
     */
    public void updateContact(String username, Contact contact) {
        try {
            service.
              path("contacts").
              path(username).
              path(contact.getId()).
              type(ENTRY_MEDIA_TYPE).
              header(AUTHENTICATION_HEADER, authentication).
              put(ClientResponse.class, contact.asEntry());
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 404) {
                throw new IllegalArgumentException("Username '" + username +
                        "' and/or contact id '" + contact.getId() + "' does not exist");
            } else {
                throw e;
            }
        }
    }


    // ---------------------------------------------------- Users Public Methods

    /**
     * Check if a user exists.
     *
     * @param username User name of the user
     * @return true if the user exists otherwise false
      *@exception IllegalStateException if you are not authorized to
     *  perform this request
     */
    public boolean doesUserExist(String username) {
        try {
            service.
              path("users").
              path(username).
              header(AUTHENTICATION_HEADER, authentication).
              get(String.class);
            return true;
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 404) {
                return false;
            } else if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else {
                throw e;
            }
        }
    }
    /**
     * <p>Add the specified user to the Contacts System.  Returns the URI
     * of the newly created user.</p>
     *
     * @param user User information to be added
     *
     * @exception IllegalArgumentException if a user with this username
     *  already exists
     * @exception IllegalStateException if you are not authorized
     *  to perform this request
     */
    public String createUser(User user) {
        try {
            Entry entry = abdera.newEntry();
            entry.setTitle(user.getUsername());
            helper.setContentEntity(entry, CONTENT_MEDIA_TYPE, user);
            ClientResponse response = service.
              path("users").
              type(ENTRY_MEDIA_TYPE).
              header(AUTHENTICATION_HEADER, authentication).
              post(ClientResponse.class, entry);
            return response.getLocation().toString();
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 409) {
                throw new IllegalArgumentException("Username '" + user.getUsername() + "' already exists");
            } else {
                throw e;
            }
        }
    }


    /**
     * <p>Delete the user with the specified name, and all that user's contacts.</p>
     *
     * @param username Username of the user to delete
     *
     * @exception IllegalArgumentException if no such user exists
     * @exception IllegalStateException if you are not authorized to
     *  perform this request
     */
    public void deleteUser(String username) {
        try {
            service.
              path("users").
              path(username).
              header(AUTHENTICATION_HEADER, authentication).
              delete();
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 404) {
                throw new IllegalArgumentException("Username '" + username + "' does not exist");
            } else {
                throw e;
            }
        }
    }


    /**
     * <p>Find and return the user with the specified username.</p>
     *
     * @param username Username of the requested user
     *
     * @exception IllegalArgumentException if no such user exists
     * @exception IllegalStateException if you are not authorized to
     *  perform this request
     */
    public User findUser(String username) {
        try {
            Entry entry = service.
                    path("users").
                    path(username).
                    accept(ENTRY_MEDIA_TYPE).
                    header(AUTHENTICATION_HEADER, authentication).
                    get(Entry.class);
            return helper.getContentEntity(entry, User.class);
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 404) {
                throw new IllegalArgumentException("Username '" + username + "' does not exist");
            } else {
                throw e;
            }
        }
    }


    /**
     * <p>Find and return all registered users.</p>
     *
     * @exception IllegalStateException if the caller is not authorized to
     *  perform this request
     */
    public List<User> findUsers() {
        Feed feed = null;
        try {
            feed = service.
               path("users").
               accept(FEED_MEDIA_TYPE).
               header(AUTHENTICATION_HEADER, authentication).
               get(Feed.class);
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else {
                throw e;
            }
        }
        List<User> results = new ArrayList<User>();
        for (Entry entry : feed.getEntries()) {
            results.add(helper.getContentEntity(entry, User.class));
        }
        return results;
    }


    /**
     * <p>Update the specified user.</p>
     *
     * @param user User information to be updated
     *
     * @exception IllegalArgumentException if no such user exists
     * @exception IllegalStateException if the caller is not authorized to
     *  perform this request
     */
    public void updateUser(User user) {
        try {
            Entry entry = abdera.newEntry();
            entry.setTitle(user.getUsername());
            helper.setContentEntity(entry, ENTRY_MEDIA_TYPE, user);
            service.
              path("users").
              path(user.getUsername()).
              type(ENTRY_MEDIA_TYPE).
              header(AUTHENTICATION_HEADER, authentication).
              put(ClientResponse.class, entry);
        } catch (UniformInterfaceException e) {
            if (e.getResponse().getStatus() == 401) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 403) {
                throw new IllegalStateException(e);
            } else if (e.getResponse().getStatus() == 404) {
                throw new IllegalArgumentException("Username '" + user.getUsername() + "' does not exist");
            } else {
                throw e;
            }
        }
    }


    // --------------------------------------------------------- Private Methods


    /**
     * <p>Convenience string for Base 64 encoding.</p>
     */
    private static final String BASE64_CHARS =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
            "abcdefghijklmnopqrstuvwxyz" +
            "0123456789+/";


    /**
     * <p>Encode the specified credentials into a String as required by
     * HTTP Basic Authentication (<a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>).</p>
     *
     * @param username Username to be encoded
     * @param password Password to be encoded
     */
    public String encodeCredentialsBasic(String username, String password) {

        String encode = username + ":" + password;
        int paddingCount = (3 - (encode.length() % 3)) % 3;
        encode += "\0\0".substring(0, paddingCount);
        StringBuilder encoded = new StringBuilder();
        for (int i = 0; i < encode.length(); i += 3) {
            int j = (encode.charAt(i) << 16) + (encode.charAt(i + 1) << 8) + encode.charAt(i + 2);
            encoded.append(BASE64_CHARS.charAt((j >> 18) & 0x3f));
            encoded.append(BASE64_CHARS.charAt((j >> 12) & 0x3f));
            encoded.append(BASE64_CHARS.charAt((j >> 6) & 0x3f));
            encoded.append(BASE64_CHARS.charAt(j & 0x3f));
        }
        return encoded.toString();

    }

    private static int getPort(int defaultPort) {
        String port = System.getProperty("jersey.test.port");
        if (null != port) {
            try {
                return Integer.parseInt(port);
            } catch (NumberFormatException e) {
            }
        }
        return defaultPort;
    }

    private static URI getBaseURI() {
        return UriBuilder.fromUri("http://localhost/").port(getPort(9998)).build();
    }

    public static final URI BASE_URI = getBaseURI();

    /**
     *  This is only a sample main method, you can experiment on your own by using above implemented methods.
     */
    public static void main(String[] args) {

        String uri, username, password;

        if (args.length < 3) {
            uri = BASE_URI.toString();
            username = "admin";
            password = "password";
        } else {
            uri = args[0];
            username = args[1];
            password = args[2];
        }
        
        ContactsClient client = new ContactsClient(uri, username, password);

        User bob = new User();
        bob.setUsername("bob");
        bob.setPassword("alice");

        if (client.doesUserExist(bob.getUsername())) {
            System.out.println("User bob already exists");
            return;
        } else {
            System.out.println("Creating user bob with one contact");
        }

        client.createUser(bob);

        Contact c = new Contact();
        c.setName("bobs");
        c.setContent("Bob contacts");

        EmailAddress email = new EmailAddress("bob@example.com", "work e-mail", true, "work");

        c.getEmailAddresses().add(email);

        client.createContact(bob.getUsername(), c);
    }
}
