public class PagedResult<T> {
    public T[] Results;
    public String ResultsOrderedBy;
    public String OrderDirection;
    public int PageNumber;
    public int PageSize;
    public int RecordsOnThisPage;
    public int TotalNumberOfRecords;
    public int NumberOfPages;
    
    @Override
    public String toString() {
        return String.format("{ Results: %s, ResultsOrderedBy: %s, OrderDirection: %s, PageNumber: %s, PageSize: %s, RecordsOnThisPage: %s, TotalNumberOfRecords: %s, NumberOfPages: %s }",
            Arrays.deepToString(Results), ResultsOrderedBy, OrderDirection, PageNumber,
            PageSize, RecordsOnThisPage, TotalNumberOfRecords,
            NumberOfPages);
    }
}
 public class CampaignClick extends CampaignEventWithGeoData {
	public String URL;
	
    @Override
    public String toString() {
    	return String.format("{ URL: %s, EmailAddress: %s, ListID: %s, Date: %s, IPAddress: %s, Latitude: %s, Longitude: %s, City: %s, Region: %s, CountryCode: %s, CountryName: %s }",
                URL, EmailAddress, ListID, Date, IPAddress, Latitude, Longitude, City, Region, CountryCode, CountryName);
    }
} public class CampaignForCreationFromTemplate extends Campaign {
    public String FromName;
    public String FromEmail;
    public String ReplyTo;
    public String[] ListIDs;
    public String[] SegmentIDs;
    public String TemplateID;
    public TemplateContent TemplateContent;
} public class CampaignSummary {
    public String Name;
    public int Recipients;
    public int TotalOpened;
    public int Clicks;
    public int Unsubscribed;
    public int SpamComplaints;
    public int Bounced;
    public int UniqueOpened;
    public int Forwards;
    public int Mentions;
    public int Likes;
    public String WebVersionURL;
    public String WebVersionTextURL;
    public String WorldviewURL;

    @Override
    public String toString() {
        return String.format(
            "{ Name: %s, Recipients: %s, TotalOpened: %s, Clicks: %s, Unsubscribed: %s, SpamComplaints: %s, " +
            "Bounced: %s, UniqueOpened: %s, ForwardToAFriends: %s, TwitterTweets: %s, FacebookLikes: %s, " +
            "WebVersionURL: %s, WebVersionTextURL: %s, WorldviewURL: %s }",
            Name, Recipients, TotalOpened, Clicks, Unsubscribed, SpamComplaints,
            Bounced, UniqueOpened, Forwards, Mentions, Likes,
            WebVersionURL, WebVersionTextURL, WorldviewURL);
    }
}
 public class ListsAndSegments {
    public ListBasics[] Lists;
    public Segment[] Segments;
    
    @Override
    public String toString() {
        return String.format("{ Lists: %s, Segments: %s }", 
            Arrays.deepToString(Lists), Arrays.deepToString(Segments));
    }
}
 public class PreviewData {
    public String[] PreviewRecipients;
    public String Personalize;
}
 public class Subscriber {
    public String ListID;
    public String EmailAddress;
    public String Name;
    public Date Date;
    public String State; // TODO: Probably want to move this to an enum
    public CustomField[] CustomFields;
    public String ReadsEmailWith;
    public ConsentToTrack ConsentToTrack;
    
    @Override
    public String toString() {
        return String.format("{ ListID: %s, EmailAddress: %s, Name: %s, Date: %s, State: %s }",
            ListID, EmailAddress, Name, Date, State);
    }
}
 public abstract class AuthenticationDetails { } public class JerseyClientImpl implements JerseyClient {

    /**
     * As per Jersey docs the creation of a Client is expensive. We cache the client
     */
    private static Client client;
    static {
        ClientConfig cc = new DefaultClientConfig(); 
        cc.getClasses().add(JsonProvider.class); 
        
        Map<String, Object> properties = cc.getProperties();
        properties.put(ClientConfig.PROPERTY_CHUNKED_ENCODING_SIZE, 64 * 1024);
        properties.put(com.sun.jersey.api.json.JSONConfiguration.FEATURE_POJO_MAPPING, "true");

        client = Client.create(cc);
        client.setFollowRedirects(false);

        if (Configuration.Current.isLoggingEnabled()) {
            client.addFilter(new LoggingFilter(System.out));
        }

        client.addFilter(new GZIPContentEncodingFilter(false));
        client.addFilter(new UserAgentFilter());
    }

    private ErrorDeserialiser<String> defaultDeserialiser = new ErrorDeserialiser<String>(){};
    private ResourceFactory authorisedResourceFactory;
    private AuthenticationDetails authDetails;

    /**
     * Constructs a JerseyClientImpl instance, including an OAuth access token and refresh token.
     * @param auth 
     */
    public JerseyClientImpl(AuthenticationDetails auth) {
    	this.setAuthenticationDetails(auth);
    }
    
    public AuthenticationDetails getAuthenticationDetails() {
    	return this.authDetails;
    }

	public void setAuthenticationDetails(AuthenticationDetails authDetails) {
		this.authDetails = authDetails;
    	if (authDetails instanceof OAuthAuthenticationDetails) {
			OAuthAuthenticationDetails oauthDetails = (OAuthAuthenticationDetails)authDetails;
			authorisedResourceFactory = new AuthorisedResourceFactory(oauthDetails.getAccessToken());
    	} else if (authDetails instanceof ApiKeyAuthenticationDetails) {
			ApiKeyAuthenticationDetails apiKeyDetails = (ApiKeyAuthenticationDetails)authDetails;
			authorisedResourceFactory = new AuthorisedResourceFactory(apiKeyDetails.getApiKey(), "x");
    	} else {
    		authorisedResourceFactory = new UnauthorisedResourceFactory();
    	}
	}

    /**
     * Performs a HTTP GET on the route specified by the pathElements deserialising the 
     * result to an instance of klass.
     * @param <T> The type of model expected from the API call.
     * @param klass The class of the model to deserialise. 
     * @param pathElements The path of the API resource to access
     * @return The model returned from the API call
     * @throws CreateSendException If the API call results in a HTTP status code >= 400
     */
    public <T> T get(Class<T> klass, String... pathElements) throws CreateSendException {
        return get(klass, null, authorisedResourceFactory, pathElements);
    }
        
    public <T> T get(Class<T> klass, ErrorDeserialiser<?> errorDeserialiser, 
            String... pathElements) throws CreateSendException {
        return get(klass, null, authorisedResourceFactory, errorDeserialiser, pathElements);
    }
    
    /**
     * Performs a HTTP GET on the route specified by the pathElements deserialising the 
     * result to an instance of klass.
     * @param <T> The type of model expected from the API call.
     * @param klass The class of the model to deserialise. 
     * @param queryString The query string params to use for the request. 
     * Use <code>null</code> when no query string is required.
     * @param pathElements The path of the API resource to access
     * @return The model returned from the API call
     * @throws CreateSendException If the API call results in a HTTP status code >= 400
     */
    public <T> T get(Class<T> klass, MultivaluedMap<String, String> queryString,
        String... pathElements) throws CreateSendException {
        return get(klass, queryString, authorisedResourceFactory, pathElements);
    }
        
    public <T> T get(Class<T> klass, MultivaluedMap<String, String> queryString,
        ResourceFactory resourceFactory, String... pathElements) throws CreateSendException {
        return get(klass, queryString, resourceFactory, defaultDeserialiser, pathElements);
    }
        
    public <T> T get(Class<T> klass, MultivaluedMap<String, String> queryString,
        ResourceFactory resourceFactory, ErrorDeserialiser<?> errorDeserialiser, 
        String... pathElements) throws CreateSendException {
        WebResource resource = resourceFactory.getResource(client, pathElements);
        
        if(queryString != null) {
            resource = resource.queryParams(queryString);
        }
        
        try {
            return fixStringResult(klass, resource.get(klass));
        } catch (UniformInterfaceException ue) {
            throw handleErrorResponse(ue, errorDeserialiser);
        }   
    }
    
    /**
     * Performs a HTTP GET on the route specified attempting to deserialise the
     * result to a paged result of the given type.
     * @param <T> The type of paged result data expected from the API call. 
     * @param queryString The query string values to use for the request.
     * @param pathElements The path of the API resource to access
     * @return The model returned from the API call
     * @throws CreateSendException If the API call results in a HTTP status code >= 400
     */
    public <T> PagedResult<T> getPagedResult(Integer page, Integer pageSize, String orderField, 
        String orderDirection, MultivaluedMap<String, String> queryString, String... pathElements) 
        throws CreateSendException {
        WebResource resource = authorisedResourceFactory.getResource(client, pathElements);
        if(queryString == null) queryString = new MultivaluedMapImpl();
        
        addPagingParams(queryString, page, pageSize, orderField, orderDirection);
        
        try {
            if(queryString != null) {
                resource = resource.queryParams(queryString);
            }
            
            return resource.get(new GenericType<PagedResult<T>>(getGenericReturnType()));
        } catch (UniformInterfaceException ue) {
            throw handleErrorResponse(ue, defaultDeserialiser);
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        
        return null;
    }
        
    /**
     * Posts the provided entity to the url specified by the provided path elements. 
     * The result of the call will be deserialised to an instance of the specified class.
     * @param <T> The class to use for model deserialisation
     * @param klass The class to use for model deserialisation
     * @param entity The entity to use as the body of the post request
     * @param pathElements The path to send the post request to
     * @return An instance of klass returned by the api call
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     */
    public <T> T post(Class<T> klass, Object entity, String... pathElements) throws CreateSendException {
        return post(null, klass, entity, defaultDeserialiser, MediaType.APPLICATION_JSON_TYPE, pathElements);
    }

    public <T> T post(Class<T> klass, MultivaluedMap<String, String> queryString, Object entity, String... pathElements) throws CreateSendException {
        return post(null, klass, queryString, entity, defaultDeserialiser, MediaType.APPLICATION_JSON_TYPE, pathElements);
    }

    public <T> T post(Class<T> klass, Object entity, 
            ErrorDeserialiser<?> errorDeserialiser, String... pathElements) throws CreateSendException {
    	return post(null, klass, entity, errorDeserialiser, MediaType.APPLICATION_JSON_TYPE, pathElements);
    }

    public <T> T post(String baseUri, Class<T> klass, Object entity, String... pathElements) throws CreateSendException {
        return post(baseUri, klass, entity, defaultDeserialiser, MediaType.APPLICATION_JSON_TYPE, pathElements);
    }

    public <T> T post(String baseUri, Class<T> klass, Object entity,
            ErrorDeserialiser<?> errorDeserialiser, String... pathElements) throws CreateSendException {
    	return post(baseUri, klass, entity, errorDeserialiser, MediaType.APPLICATION_JSON_TYPE, pathElements);
    }

    public <T> T post(Class<T> klass, Object entity,
            MediaType mediaType, String... pathElements) throws CreateSendException {
    	return post(null, klass, entity, defaultDeserialiser, mediaType, pathElements);
    }

    public <T> T post(String baseUri, Class<T> klass, Object entity,
            MediaType mediaType, String... pathElements) throws CreateSendException {
    	return post(baseUri, klass, entity, defaultDeserialiser, mediaType, pathElements);
    }

    public <T> T post(String baseUri, Class<T> klass, Object entity,
            ErrorDeserialiser<?> errorDeserialiser,
            MediaType mediaType, String... pathElements) throws CreateSendException {
    	return post(baseUri, klass, null, entity, errorDeserialiser, mediaType, pathElements);
    }

    private <T> T post(String baseUri,
                      Class<T> klass,
                      MultivaluedMap<String, String> queryString,
                      Object entity,
                      ErrorDeserialiser<?> errorDeserialiser,
                      MediaType mediaType,
                      String... pathElements) throws CreateSendException {
        WebResource resource;
        if (baseUri != null)
            resource = authorisedResourceFactory.getResource(baseUri, client, pathElements);
        else
            resource = authorisedResourceFactory.getResource(client, pathElements);

        if( queryString != null )
            resource = resource.queryParams(queryString);

        try {
            WebResource.Builder builder = resource.type(mediaType);

            return fixStringResult(klass,
                    entity == null ?
                            builder.post(klass, "") :
                            builder.post(klass, entity)
            );
        } catch (UniformInterfaceException ue) {
            throw handleErrorResponse(ue, errorDeserialiser);
        }
    }


    /**
     * Makes a HTTP PUT request to the path specified, using the provided entity as the 
     * request body.
     * @param entity The entity to use as the request body
     * @param pathElements The path to make the request to.
     * @throws CreateSendException Raised when the API responds with a HTTP Status >= 400
     */
    public void put(Object entity, String... pathElements) throws CreateSendException {
        put(entity, null, defaultDeserialiser, pathElements);
    }
    
    public <T> T put(Class<T> klass, Object entity, String... pathElements) throws CreateSendException {
        WebResource resource = authorisedResourceFactory.getResource(client, pathElements);
        try { 
            return fixStringResult(klass, resource.
                type(MediaType.APPLICATION_JSON_TYPE).
                put(klass, entity));
        } catch (UniformInterfaceException ue) {
            throw handleErrorResponse(ue, defaultDeserialiser);
        }
    }
    
    public void put(Object entity, MultivaluedMap<String, String> queryString, String... pathElements) throws CreateSendException {
        put(entity, queryString, defaultDeserialiser, pathElements);
    }
    
    public void put(Object entity, ErrorDeserialiser<?> errorDeserialiser,
            String... pathElements) throws CreateSendException {
        put(entity, null, errorDeserialiser, pathElements);
    }

    private void put(Object entity, MultivaluedMap<String, String> queryString, ErrorDeserialiser<?> errorDeserialiser,
        String... pathElements) throws CreateSendException {
        WebResource resource = authorisedResourceFactory.getResource(client, pathElements);
        
        if(queryString != null) {
            resource = resource.queryParams(queryString);
        }
        
        try { 
            resource.
                type(MediaType.APPLICATION_JSON_TYPE).
                put(entity);
        } catch (UniformInterfaceException ue) {
            throw handleErrorResponse(ue, errorDeserialiser);
        }
    }

    /**
     * Makes a HTTP DELETE request to the specified path
     * @param pathElements The path of the resource to delete
     * @throws CreateSendException Raised when the API responds with a HTTP Status >= 400
     */
    public void delete(String... pathElements) throws CreateSendException {
        delete(null, pathElements);
    }
    
    /**
     * Makes a HTTP DELETE request to the specified path with the specified query string
     * @param pathElements The path of the resource to delete
     * @throws CreateSendException Raised when the API responds with a HTTP Status >= 400
     */
    @Override
	public void delete(MultivaluedMap<String, String> queryString, String... pathElements) throws CreateSendException {
        WebResource resource = authorisedResourceFactory.getResource(client, pathElements);
        
        if( queryString != null )
        	resource = resource.queryParams(queryString);
        
        try { 
            resource.delete();
        } catch (UniformInterfaceException ue) {
            throw handleErrorResponse(ue, defaultDeserialiser);
        }
    }

    protected void addPagingParams(MultivaluedMap<String, String> queryString,  
        Integer page, Integer pageSize, String orderField, String orderDirection) {        
        if(page != null) {
            queryString.add("page", page.toString());
        }
        
        if(pageSize != null) {
            queryString.add("pagesize", pageSize.toString());
        }
        
        if(orderField != null) {
            queryString.add("orderfield", orderField); 
        }
        
        if(orderDirection != null) {
            queryString.add("orderdirection", orderDirection);
        }
    }
    
    /**
     * Jersey is awesome in that even though we specify a JSON response and to use 
     * the {@link com.createsend.util.jersey.JsonProvider} it sees that we want a 
     * String result and that the response is already a String so just use that. 
     * This method strips any enclosing quotes required as per the JSON spec. 
     * @param <T> The type of result we are expecting
     * @param klass The class of the provided result
     * @param result The result as deserialised by Jersey
     * @return If the result if anything but a String just return the result. 
     * If the result is a String then strip any enclosing quotes ("). 
     */
    @SuppressWarnings("unchecked")
    protected <T> T fixStringResult(Class<T> klass, T result) {
        if(klass == String.class) { 
            String strResult = (String)result;
            if(strResult.startsWith("\"")) { 
                strResult = strResult.substring(1);
            }
            
            if(strResult.endsWith("\"")) {
                strResult = strResult.substring(0, strResult.length() - 1);
            }
            
            return (T)strResult;
        }
        
        return result;
    }
    
    /**
     * Creates an exception, inheriting from @{link com.createsend.util.exceptions.CreateSendException} 
     * to represent the API error resulting in the raised {@link com.sun.jersey.api.client.UniformInterfaceException}
     * @param ue The error raised during the failed API request
     * @return An exception representing the API error
     */
    private <T> CreateSendException handleErrorResponse(UniformInterfaceException ue, 
        ErrorDeserialiser<T> deserialiser) {
        ClientResponse response = ue.getResponse();
        ApiErrorResponse<T> apiResponse = null;
        
        Status responseStatus = response.getClientResponseStatus();
        if(responseStatus == Status.BAD_REQUEST || 
           responseStatus == Status.NOT_FOUND ||
           responseStatus == Status.UNAUTHORIZED ||
           responseStatus == Status.INTERNAL_SERVER_ERROR) {
            try { 
                apiResponse = deserialiser.getResponse(response);
            } catch (Throwable t) { }                
        }

        if (apiResponse == null) {
            return handleUnknownError(responseStatus);
        } else if (apiResponse.error != null && apiResponse.error.length() > 0) {
            return handleOAuthErrorResponse(responseStatus, apiResponse);
        } else {
        	return handleAPIErrorResponse(responseStatus, apiResponse);
        }
    }

    private <T> CreateSendException handleUnknownError(Status responseStatus) {
        return new CreateSendHttpException(responseStatus);
    }
    
    private <T> CreateSendException handleAPIErrorResponse(
    		Status responseStatus, ApiErrorResponse<T> apiResponse) {
        switch(responseStatus) {
	        case BAD_REQUEST:
	            return new BadRequestException(apiResponse.Code, apiResponse.Message, apiResponse.ResultData);
	        case INTERNAL_SERVER_ERROR:
	            return new ServerErrorException(apiResponse.Code, apiResponse.Message);
	        case NOT_FOUND:
	            return new NotFoundException(apiResponse.Code, apiResponse.Message);
	        case UNAUTHORIZED:
	        	if (apiResponse.Code == 121)
	        		return new ExpiredOAuthTokenException(apiResponse.Code, apiResponse.Message);
	            return new UnauthorisedException(apiResponse.Code, apiResponse.Message);
	        default:
	            return new CreateSendHttpException(responseStatus);
	    }
    }

    private <T> CreateSendException handleOAuthErrorResponse(
    		Status responseStatus, ApiErrorResponse<T> apiResponse) {
    	return new CreateSendHttpException(
    			String.format("The CreateSend OAuth receiver responded with the following error - %s: %s",
    					apiResponse.error, apiResponse.error_description),
    			responseStatus.getStatusCode(), 0, apiResponse.error_description);
    }

    private ParameterizedType getGenericReturnType() {
        return getGenericReturnType(null, 4);
    }
    
    public static ParameterizedType getGenericReturnType(Class<?> klass, int stackFrame) {   
        StackTraceElement element = Thread.currentThread().getStackTrace()[stackFrame];
        String callingMethodName = element.getMethodName();
        
        if(klass == null) {
            try { 
                klass = Class.forName(element.getClassName());
            } catch (ClassNotFoundException e) { }
        }
        
        if (klass != null) {
            for (Method method : klass.getMethods()) {
                if (method.getName().equals(callingMethodName)) {
                    return (ParameterizedType) method.getGenericReturnType();
                }
            }
        }
        
        return null;
    }
}
 public class CreateSendException extends Exception {
    private static final long serialVersionUID = 1695317869199799783L;
    
    public CreateSendException(String message) {
        super(message);
    }
}
 public class JsonProvider extends JacksonJsonProvider {

    public static final DateFormat ApiDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm") {
        final SimpleDateFormat ApiDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        final SimpleDateFormat ApiDateFormatTz = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");

        @Override
        public Date parse(String source, ParsePosition pos) {
            if (source.length() - pos.getIndex() == ApiDateFormat.toPattern().length())
                return ApiDateFormat.parse(source, pos);
            return ApiDateFormatTz.parse(source, pos);
        }
    };

    @Override
    public void writeTo(Object value, Class<?> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, Object> httpHeaders,
            OutputStream entityStream) throws IOException {
        ObjectMapper mapper = locateMapper(type, mediaType);
        mapper.setSerializationInclusion(Include.NON_NULL);
        
        super.writeTo(value, type, genericType, annotations, mediaType, httpHeaders,
                entityStream);
    }
    
    @Override
    public Object readFrom(Class<Object> type, Type genericType,
            Annotation[] annotations, MediaType mediaType,
            MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
            throws IOException {
        ObjectMapper mapper = locateMapper(type, mediaType);
        mapper.setDateFormat(ApiDateFormat);
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        return super.readFrom(type, genericType, annotations, mediaType, httpHeaders,
                entityStream);
    }
}

import java.util.Date;
import javax.ws.rs.core.MultivaluedMap;
import com.createsend.models.PagedResult;
import com.createsend.models.campaigns.CampaignClick;
import com.createsend.models.campaigns.CampaignForCreation;
import com.createsend.models.campaigns.CampaignForCreationFromTemplate;
import com.createsend.models.campaigns.CampaignOpen;
import com.createsend.models.campaigns.CampaignSummary;
import com.createsend.models.campaigns.EmailClient;
import com.createsend.models.campaigns.ListsAndSegments;
import com.createsend.models.campaigns.PreviewData;
import com.createsend.models.campaigns.Schedule;
import com.createsend.models.subscribers.BouncedSubscriber;
import com.createsend.models.subscribers.Subscriber;
import com.createsend.util.AuthenticationDetails;
import com.createsend.util.ErrorDeserialiser;
import com.createsend.util.JerseyClientImpl;
import com.createsend.util.exceptions.CreateSendException;
import com.createsend.util.jersey.JsonProvider;
import com.sun.jersey.core.util.MultivaluedMapImpl;

/**
 * Copyright (c) 2011 Toby Brain
 * 
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 */
package com.createsend;




/**
 * Provides methods for accessing all <a href="http://www.campaignmonitor.com/api/campaigns/" target="_blank">
 * Campaign</a> resources in the Campaign Monitor API
 */
public class Campaigns extends CreateSendBase {
    private String campaignID;

    /**
     * Constructor used to create new campaigns.
     * @param auth The authentication details to use when making API calls.
     * May be either an OAuthAuthenticationDetails or
     * ApiKeyAuthenticationDetails instance.
     */
    public Campaigns(AuthenticationDetails auth) {
        this(auth, null);
    }

    /**
     * Constructor used for working with existing campaigns.
     * @param auth The authentication details to use when making API calls.
     * May be either an OAuthAuthenticationDetails or
     * ApiKeyAuthenticationDetails instance.
     * @param campaignID The campaign ID to use when making API calls.
     */
    public Campaigns(AuthenticationDetails auth, String campaignID) {
    	this.setCampaignID(campaignID);
        this.jerseyClient = new JerseyClientImpl(auth);
    }

    /**
     * Gets the current campaign ID.
     * @return The current campaign ID.
     */
    public String getCampaignID() {
        return campaignID;
    }

    /**
     * Sets the ID of the campaign to apply API calls to.
     * @param campaignID The ID of the campaign to apply API calls to.
     */
    public void setCampaignID(String campaignID) {
        this.campaignID = campaignID;
    }

    /**
     * Creates a new campaign for the specified client based on the provided campaign data.
     * @param clientID The ID of the client to create the campaign for
     * @param campaign The campaign information used for the new campaign
     * @return The ID of the newly created campaign
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#creating_a_draft_campaign" target="_blank">
     * Creating a campaign</a>
     */
    public String create(String clientID, CampaignForCreation campaign) throws CreateSendException {
        campaignID = jerseyClient.post(String.class, campaign, "campaigns", clientID + ".json");
        return campaignID;
    }

    /**
     * Creates a new campaign for the specified client, based on the template
     * and campaign data specified.
     * @param clientID The ID of the client to create the campaign for
     * @param campaign The campaign information used for the new campaign
     * @return The ID of the newly created campaign
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#creating_a_campaign_from_template" target="_blank">
     * Creating a campaign</a>
     */
    public String createFromTemplate(String clientID, CampaignForCreationFromTemplate campaign) throws CreateSendException {
        campaignID = jerseyClient.post(String.class, campaign, "campaigns", clientID + "/fromtemplate.json");
        return campaignID;
    }

    /**
     * Sends an existing draft campaign using the provided confirmation email and send date.
     * To schedule a campaign for immediate delivery use a <code>null</code> sendDate.
     * @param confirmationEmail An email address to send the delivery confirmation to.
     * @param sendDate The date to send the campaign at. This date should be in the clients timezone
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#sending_a_campaign" target="_blank">
     * Sending a campaign</a>
     */
    public void send(String confirmationEmail, Date sendDate) throws CreateSendException {
        Schedule sched = new Schedule();
        sched.ConfirmationEmail = confirmationEmail;
        sched.SendDate = sendDate == null ? "Immediately" : JsonProvider.ApiDateFormat.format(sendDate);
        
        jerseyClient.post(String.class, sched, "campaigns", campaignID, "send.json");
    }
    
    /**
     * Unschedules a campaign and moves it back to the drafts. If a campaign has already been sent or 
     * is in the process of sending, this api request will fail.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#unscheduling_a_campaign" target="_blank">
     * Sending a campaign</a>
     */
    public void unschedule() throws CreateSendException {
        jerseyClient.post(String.class, "", "campaigns", campaignID, "unschedule.json");
    }
    
    /**
     * Sends a preview of an existing draft campaign to the recipients specified in the preview data.
     * @param data The recipients and personalisation scheme to use for the preview
     * @throws CreateSendException Raised when the API responds with a HTTP status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#sending_a_campaign_preview" target="_blank">
     * Sending a campaign preview</a>
     */
    public void test(PreviewData data) throws CreateSendException {
        jerseyClient.post(String.class, data, new ErrorDeserialiser<String[]>(), 
            "campaigns", campaignID, "sendpreview.json");
    }
    
    /**
     * Get summary reporting data for the specified campaign
     * @return Summary reporting data for the specified campaign
     * @throws CreateSendException Raised if the API responds with a HTTP Status >= 400
     */
    public CampaignSummary summary() throws CreateSendException {
        return jerseyClient.get(CampaignSummary.class, "campaigns", campaignID, "summary.json");
    }

    /**
     * Gets the email clients that subscribers used to open the campaign
     * @return Array of email clients used to open the specified campaign
     * @throws CreateSendException Raised if the API responds with a HTTP Status >= 400
     */
    public EmailClient[] emailClientUsage() throws CreateSendException {
        return jerseyClient.get(EmailClient[].class, "campaigns", campaignID, "emailclientusage.json");
    }

    /**
     * Gets the lists and segments that a campaign is to be, or has been sent to.
     * @return The lists and segments that the campaign is to be, or has been sent to
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_listsandsegments" target="_blank">
     * Getting lists and segments</a>
     */
    public ListsAndSegments listsAndSegments() throws CreateSendException {
        return jerseyClient.get(ListsAndSegments.class, "campaigns", campaignID, "listsandsegments.json");
    }
    
    /**
     * Gets a paged list of recipients for the specified campaign
     * @param page The page number of results to get. Use <code>null</code> for the default (page=1)
     * @param pageSize The number of records to get on the current page. Use <code>null</code> for the default.
     * @param orderField The field used to order the results by. Use <code>null</code> for the default.
     * @param orderDirection The direction to order the results by. Use <code>null</code> for the default.
     * @return The paged recipients returned by the api call.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_recipients" target="_blank">
     * Getting campaign recipients</a>
     */
    public PagedResult<Subscriber> recipients(
        Integer page, Integer pageSize, String orderField, String orderDirection) throws CreateSendException {
        return jerseyClient.getPagedResult(page, pageSize, orderField, orderDirection, null, 
            "campaigns", campaignID, "recipients.json");
    }

    /**
     * Gets a paged list of bounces for the specified campaign
     * @return The paged bounces returned by the api call.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_bouncelist" target="_blank">
     * Getting campaign bounces</a>
     */
    public PagedResult<BouncedSubscriber> bounces() throws CreateSendException {
    	return bounces(1, 1000, "date", "asc");
    }

    /**
     * Gets a paged list of bounces for the specified campaign
     * @param page The page number of results to get. Use <code>null</code> for the default (page=1)
     * @param pageSize The number of records to get on the current page. Use <code>null</code> for the default.
     * @param orderField The field used to order the results by. Use <code>null</code> for the default.
     * @param orderDirection The direction to order the results by. Use <code>null</code> for the default.
     * @return The paged bounces returned by the api call.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_bouncelist" target="_blank">
     * Getting campaign bounces</a>
     */
    public PagedResult<BouncedSubscriber> bounces(
            Integer page, Integer pageSize, String orderField, String orderDirection) throws CreateSendException {
    	return bounces("", page, pageSize, orderField, orderDirection);
    }

    /**
     * Gets a paged list of bounces for the specified campaign
     * @param bouncesFrom The date to start getting bounce results from. This field is required
     * @param page The page number of results to get. Use <code>null</code> for the default (page=1)
     * @param pageSize The number of records to get on the current page. Use <code>null</code> for the default.
     * @param orderField The field used to order the results by. Use <code>null</code> for the default.
     * @param orderDirection The direction to order the results by. Use <code>null</code> for the default.
     * @return The paged bounces returned by the api call.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_bouncelist" target="_blank">
     * Getting campaign bounces</a>
     */
    public PagedResult<BouncedSubscriber> bounces(Date bouncesFrom,
            Integer page, Integer pageSize, String orderField, String orderDirection) throws CreateSendException {
    	return bounces(JsonProvider.ApiDateFormat.format(bouncesFrom),
    			page, pageSize, orderField, orderDirection);
    }
    
    private PagedResult<BouncedSubscriber> bounces(String bouncesFrom,
        Integer page, Integer pageSize, String orderField, String orderDirection) throws CreateSendException {
        MultivaluedMap<String, String> queryString = new MultivaluedMapImpl();
        queryString.add("date", bouncesFrom);
        
        return jerseyClient.getPagedResult(page, pageSize, orderField, orderDirection, queryString, 
            "campaigns", campaignID, "bounces.json");
    }

    /**
     * Gets a paged list of opens for the specified campaign
     * @return The paged opens returned by the api call.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_openslist" target="_blank">
     * Getting campaign opens</a>
     */
    public PagedResult<CampaignOpen> opens() throws CreateSendException {
    	return opens(1, 1000, "date", "asc");
    }
    
    /**
     * Gets a paged list of opens for the specified campaign
     * @param page The page number of results to get. Use <code>null</code> for the default (page=1)
     * @param pageSize The number of records to get on the current page. Use <code>null</code> for the default.
     * @param orderField The field used to order the results by. Use <code>null</code> for the default.
     * @param orderDirection The direction to order the results by. Use <code>null</code> for the default.
     * @return The paged opens returned by the api call.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_openslist" target="_blank">
     * Getting campaign opens</a>
     */
    public PagedResult<CampaignOpen> opens(
            Integer page, Integer pageSize, String orderField, String orderDirection) throws CreateSendException {
    	return opens("", page, pageSize, orderField, orderDirection);
    }

    /**
     * Gets a paged list of opens for the specified campaign
     * @param opensFrom The date to start getting open results from. This field is required
     * @param page The page number of results to get. Use <code>null</code> for the default (page=1)
     * @param pageSize The number of records to get on the current page. Use <code>null</code> for the default.
     * @param orderField The field used to order the results by. Use <code>null</code> for the default.
     * @param orderDirection The direction to order the results by. Use <code>null</code> for the default.
     * @return The paged opens returned by the api call.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_openslist" target="_blank">
     * Getting campaign opens</a>
     */
    public PagedResult<CampaignOpen> opens(Date opensFrom,
            Integer page, Integer pageSize, String orderField, String orderDirection) throws CreateSendException {
    	return opens(JsonProvider.ApiDateFormat.format(opensFrom),
    			page, pageSize, orderField, orderDirection);
    }

    private PagedResult<CampaignOpen> opens(String opensFrom,
        Integer page, Integer pageSize, String orderField, String orderDirection) throws CreateSendException {
        MultivaluedMap<String, String> queryString = new MultivaluedMapImpl();
        queryString.add("date", opensFrom);
        
        return jerseyClient.getPagedResult(page, pageSize, orderField, orderDirection,
            queryString, "campaigns", campaignID, "opens.json");
    }

    /**
     * Gets a paged list of clicks for the specified campaign
     * @return The paged clicks returned by the api call.
     * @throws CreateSendException Thrown when the API responds with a HTTP Status >= 400
     * @see <a href="http://www.campaignmonitor.com/api/campaigns/#campaign_clickslist" target="_blank">
     * Getting campaign clicks</a>
     */

    public PagedResult<CampaignClick> clicks() throws CreateSendException {
