diff options
| author | Jesse Morgan <jesse@jesterpm.net> | 2016-03-19 02:05:33 -0700 | 
|---|---|---|
| committer | Jesse Morgan <jesse@jesterpm.net> | 2016-03-19 02:07:24 -0700 | 
| commit | b9eb1329a6dbec7b75c21d8e0eb13134121db6bb (patch) | |
| tree | fec73ab32ff625c304513c24e864809845eede1a | |
Initial commit for the CCB API Client.
The client currently supports the following APIs:
* individual_profiles
* individual_profile_from_id
* individual_profile_from_login_password
* individual_profile_from_micr
* custom_field_labels
45 files changed, 2863 insertions, 0 deletions
| diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6990d22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +CCBAPI.iml +target/ @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" +         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" +         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +    <modelVersion>4.0.0</modelVersion> + +    <groupId>com.p4square</groupId> +    <artifactId>ccbapi</artifactId> +    <version>1.0-SNAPSHOT</version> + +    <developers> +        <developer> +            <name>Jesse Morgan</name> +            <email>jesse@jesterpm.net</email> +        </developer> +    </developers> + +    <dependencies> +        <dependency> +            <groupId>org.apache.httpcomponents</groupId> +            <artifactId>httpclient</artifactId> +            <version>4.1.1</version> +        </dependency> +        <dependency> +            <groupId>junit</groupId> +            <artifactId>junit</artifactId> +            <version>4.8.2</version> +            <scope>test</scope> +        </dependency> +        <dependency> +            <groupId>org.easymock</groupId> +            <artifactId>easymock</artifactId> +            <version>3.4</version> +            <scope>test</scope> +        </dependency> +    </dependencies> + +    <build> +        <testResources> +            <testResource> +                <directory>src/test/resources</directory> +            </testResource> +        </testResources> +        <plugins> +            <plugin> +                <groupId>org.apache.maven.plugins</groupId> +                <artifactId>maven-compiler-plugin</artifactId> +                <version>[3.0)</version> +                <configuration> +                    <!-- or whatever version you use --> +                    <source>1.7</source> +                    <target>1.7</target> +                </configuration> +            </plugin> +        </plugins> +    </build> +</project>
\ No newline at end of file diff --git a/src/main/java/com/p4square/ccbapi/ApacheHttpClientImpl.java b/src/main/java/com/p4square/ccbapi/ApacheHttpClientImpl.java new file mode 100644 index 0000000..0833c67 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/ApacheHttpClientImpl.java @@ -0,0 +1,90 @@ +package com.p4square.ccbapi; + +import com.p4square.ccbapi.exception.CCBException; +import com.p4square.ccbapi.exception.CCBRetryableErrorException; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * ApacheHttpClientImpl is an implementation of HTTPInterface which uses the Apache HTTP Client library. + */ +public class ApacheHttpClientImpl implements HTTPInterface { + +    private final DefaultHttpClient httpClient; + +    public ApacheHttpClientImpl(final URI apiBaseUri, final String username, final String password) { +        // Create the HTTP client. +        this.httpClient = new DefaultHttpClient(); + +        // Prepare the CredentialsProvider for the HTTP Client. +        int port = apiBaseUri.getPort(); +        if (port == -1) { +            if ("http".equalsIgnoreCase(apiBaseUri.getScheme())) { +                port = 80; +            } else if ("https".equalsIgnoreCase(apiBaseUri.getScheme())) { +                port = 443; +            } else { +                throw new IllegalArgumentException("Cannot determine port for unknown scheme."); +            } +        } +        this.httpClient.getCredentialsProvider().setCredentials(new AuthScope(apiBaseUri.getHost(), port), +                                                                new UsernamePasswordCredentials(username, password)); +    } + +    @Override +    public void close() throws IOException { +        httpClient.getConnectionManager().shutdown(); +    } + +    @Override +    public InputStream sendPostRequest(URI uri, Map<String, String> form) throws IOException { +        // Build the request. +        final HttpPost httpPost = new HttpPost(uri); +        final List<NameValuePair> formParameters = new ArrayList<>(); +        for (Map.Entry<String, String> entry : form.entrySet()) { +            formParameters.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); +        } +        httpPost.setEntity(new UrlEncodedFormEntity(formParameters)); + +        // Make the request. +        final HttpResponse response = httpClient.execute(httpPost); + +        // Process the response. +        final int statusCode = response.getStatusLine().getStatusCode(); +        if (statusCode != 200) { +            // Consume the entity and close the connection. +            EntityUtils.consume(response.getEntity()); + +            // Determine the type of failure and throw an exception. +            if (statusCode >= 400 && statusCode < 500) { +                throw new CCBException("Unexpected non-retryable error: " + response.getStatusLine().toString()); +            } else if (statusCode >= 500 && statusCode < 600) { +                throw new CCBRetryableErrorException("Retryable error: " + response.getStatusLine().toString()); +            } else { +                throw new CCBException("Unexpected status code: " + response.getStatusLine().toString()); +            } +        } + +        final HttpEntity entity = response.getEntity(); +        if (entity != null) { +            return entity.getContent(); +        } else { +            return null; +        } +    } +} diff --git a/src/main/java/com/p4square/ccbapi/CCBAPI.java b/src/main/java/com/p4square/ccbapi/CCBAPI.java new file mode 100644 index 0000000..e54e20b --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/CCBAPI.java @@ -0,0 +1,42 @@ +package com.p4square.ccbapi; + +import com.p4square.ccbapi.model.*; + +import java.io.Closeable; +import java.io.IOException; + +/** + * CCBAPI is a Java interface for using the Church Community Builder API. + */ +public interface CCBAPI extends Closeable { +    /** +     * Retrieve the set of custom (user-defined) fields and the associated labels. +     * +     * @return A GetCustomFieldLabelsResponse containing the fields. +     * @throws IOException on failure. +     */ +    GetCustomFieldLabelsResponse getCustomFieldLabels() throws IOException; + +    /** +     * Retrieve one or more IndividualProfiles. +     * +     * If any of the following properties are set on the request, +     * this method will return the matching individual, if one exists. +     * +     * <ul> +     *     <li>Individual ID</li> +     *     <li>Login and Password</li> +     *     <li>MICR</li> +     * </ul> +     * +     * If more than one property is included only the first, in the order listed above, will be used. +     * If none of the options are included, all individuals will be returned. +     * +     * The appropriate CCB API will be selected based on the options used. +     * +     * @param request A GetIndividualProfilesRequest. +     * @return A GetIndividualProfilesResponse object on success, including when no individuals match. +     * @throws IOException on failure. +     */ +    GetIndividualProfilesResponse getIndividualProfiles(GetIndividualProfilesRequest request) throws IOException; +} diff --git a/src/main/java/com/p4square/ccbapi/CCBAPIClient.java b/src/main/java/com/p4square/ccbapi/CCBAPIClient.java new file mode 100644 index 0000000..782f305 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/CCBAPIClient.java @@ -0,0 +1,171 @@ +package com.p4square.ccbapi; + +import com.p4square.ccbapi.exception.CCBErrorResponseException; +import com.p4square.ccbapi.model.*; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * CCBAPIClient is an implementation of CCBAPI using the Apache HttpClient. + * + * This implementation is built against the API documentations found here: + * https://designccb.s3.amazonaws.com/helpdesk/files/official_docs/api.html + * + * This client is thread-safe. + */ +public class CCBAPIClient implements CCBAPI { + +    private static final Map<String, String> EMPTY_MAP = Collections.emptyMap(); + +    private final URI apiBaseUri; +    private final HTTPInterface httpClient; +    private final CCBXmlBinder xmlBinder; + +    /** +     * Create a new CCB API Client. +     * +     * @param church The church identifier used with CCB. +     * @param username The API username. +     * @param password The API password. +     * @throws URISyntaxException If the church parameter contains unsafe URI characters. +     */ +    public CCBAPIClient(final String church, final String username, final String password) throws URISyntaxException { +        this(new URI("https://" + church + ".ccbchurch.com/api.php"), username, password); +    } + +    /** +     * Create a new CCB API Client. +     * +     * @param apiUri The base URI to use when contacting CCB. +     * @param username The API username. +     * @param password The API password. +     */ +    public CCBAPIClient(final URI apiUri, final String username, final String password) { +        this(apiUri, new ApacheHttpClientImpl(apiUri, username, password)); +    } + +    /** +     * A private constructor which allows for dependency injection. +     * +     * @param apiUri The base URI to use when contacting CCB. +     * @param httpClient The HTTP client used to send requests. +     */ +    protected CCBAPIClient(final URI apiUri, final HTTPInterface httpClient) { +        this.apiBaseUri = apiUri; +        this.httpClient = httpClient; +        this.xmlBinder = new CCBXmlBinder(); +    } + +    @Override +    public void close() throws IOException { +        httpClient.close(); +    } + +    @Override +    public GetIndividualProfilesResponse getIndividualProfiles(GetIndividualProfilesRequest request) throws IOException { +        // Prepare the request. +        String serviceName; +        final Map<String, String> params = new HashMap<>(); +        if (request.getId() != 0) { +            // Use individual_profile_from_id (individual_id) +            serviceName = "individual_profile_from_id"; +            params.put("individual_id", String.valueOf(request.getId())); + +        } else if (request.getLogin() != null && request.getPassword() != null) { +            // Use individual_profile_from_login_password (login, password) +            serviceName = "individual_profile_from_login_password"; +            params.put("login", request.getLogin()); +            params.put("password", request.getPassword()); + +        } else if (request.getRoutingNumber() != null && request.getAccountNumber() != null) { +            // Use individual_profile_from_micr (account_number, routing_number) +            serviceName = "individual_profile_from_micr"; +            params.put("routing_number", request.getRoutingNumber()); +            params.put("account_number", request.getAccountNumber()); + +        } else { +            // Use individual_profiles +            serviceName = "individual_profiles"; +            if (request.getModifiedSince() != null) { +                params.put("modified_since", request.getModifiedSince().toString()); +            } +            if (request.getIncludeInactive() != null) { +                params.put("include_inactive", request.getIncludeInactive() ? "true" : "false"); +            } +            if (request.getPage() != 0) { +                params.put("page", String.valueOf(request.getPage())); +            } +            if (request.getPerPage() != 0) { +                params.put("per_page", String.valueOf(request.getPerPage())); +            } +        } + +        // Send the request and parse the response. +        return makeRequest(serviceName, params, EMPTY_MAP, GetIndividualProfilesResponse.class); +    } + +    @Override +    public GetCustomFieldLabelsResponse getCustomFieldLabels() throws IOException { +        return makeRequest("custom_field_labels", EMPTY_MAP, EMPTY_MAP, GetCustomFieldLabelsResponse.class); +    } + +    /** +     * Build the URI for a particular service call. +     * +     * @param service The CCB API service to call (i.e. the srv query parameter). +     * @param parameters A map of query parameters to include on the URI. +     * @return The apiBaseUri with the additional query parameters appended. +     */ +    private URI makeURI(final String service, final Map<String, String> parameters) { +        try { +            StringBuilder queryStringBuilder = new StringBuilder(); +            if (apiBaseUri.getQuery() != null) { +                queryStringBuilder.append(apiBaseUri.getQuery()).append("&"); +            } +            queryStringBuilder.append("srv=").append(service); +            for (Map.Entry<String, String> entry: parameters.entrySet()) { +                queryStringBuilder.append("&").append(entry.getKey()).append("=").append(entry.getValue()); +            } +            return new URI(apiBaseUri.getScheme(), apiBaseUri.getAuthority(), apiBaseUri.getPath(), +                    queryStringBuilder.toString(), apiBaseUri.getFragment()); +        } catch (URISyntaxException e) { +            // This shouldn't happen, but needs to be caught regardless. +            throw new AssertionError("Could not construct API URI", e); +        } +    } + +    /** +     * Send a request to CCB. +     * +     * @param api The CCB service name. +     * @param params The URL query params. +     * @param form The form body parameters. +     * @param clazz The response class. +     * @param <T> The type of response. +     * @return The response. +     * @throws IOException if an error occurs. +     */ +    private <T extends CCBAPIResponse> T makeRequest(final String api, final Map<String, String> params, +                                                     final Map<String, String> form, final Class<T> clazz) +            throws IOException { + +        final InputStream entity = httpClient.sendPostRequest(makeURI(api, params), form); +        try { +            T response = xmlBinder.bindResponseXML(entity, clazz); +            if (response.getErrors() != null && response.getErrors().size() > 0) { +                throw new CCBErrorResponseException(response.getErrors()); +            } +            return response; +        } finally { +            if (entity != null) { +                entity.close(); +            } +        } +    } +} diff --git a/src/main/java/com/p4square/ccbapi/CCBXmlBinder.java b/src/main/java/com/p4square/ccbapi/CCBXmlBinder.java new file mode 100644 index 0000000..f347643 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/CCBXmlBinder.java @@ -0,0 +1,78 @@ +package com.p4square.ccbapi; + +import com.p4square.ccbapi.exception.CCBParseException; +import com.p4square.ccbapi.model.CCBAPIResponse; + +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import java.io.InputStream; +import java.util.concurrent.ConcurrentHashMap; + +/** + * CCBXmlBinder is used to bind XML responses to CCBAPIResponse implementations. + * + * This class is thread-safe. + */ +public class CCBXmlBinder { +    private final XMLInputFactory xmlInputFactory; +    private final ConcurrentHashMap<Class<? extends CCBAPIResponse>, JAXBContext> jaxbContextCache; + +    public CCBXmlBinder() { +        xmlInputFactory = XMLInputFactory.newFactory(); +        jaxbContextCache = new ConcurrentHashMap<>(); +    } + +    public <T extends CCBAPIResponse> T bindResponseXML(InputStream response, Class<T> responseClass) +            throws CCBParseException { +        try { +            final XMLStreamReader xmlReader = xmlInputFactory.createXMLStreamReader(response); +            final JAXBContext jaxbContext = getJAXBContext(responseClass); +            final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + +            // Skip ahead to the response entity. +            while (xmlReader.hasNext()) { +                xmlReader.next(); +                if (xmlReader.isStartElement() && "response".equalsIgnoreCase(xmlReader.getLocalName())) { +                    // Parse and return the response. +                    // If the response cannot be parsed a JAXBException will be thrown. +                    return (T) unmarshaller.unmarshal(xmlReader); +                } +            } + +            // If we reach this point then the response did not contain a response element. +            throw new CCBParseException("Response did not contain a response element."); + +        } catch (XMLStreamException | JAXBException e) { +            throw new CCBParseException("Failed to parse response.", e); +        } +    } + +    /** +     * Find or create the JAXBContext for a CCBAPIResponse implementation. +     * +     * @param responseClass The response implementation class. +     * @return a JAXBContext which can be used to unmarshell the responseClass. +     */ +    private JAXBContext getJAXBContext(Class<? extends CCBAPIResponse> responseClass) { +        if (!jaxbContextCache.containsKey(responseClass)) { +            synchronized (jaxbContextCache) { +                // Check again to be sure. +                if (!jaxbContextCache.containsKey(responseClass)) { +                    try { +                        final JAXBContext jaxbContext = JAXBContext.newInstance(responseClass); +                        jaxbContextCache.put(responseClass, jaxbContext); +                    } catch (JAXBException e) { +                        throw new AssertionError("Could not construct JAXBContext for " + responseClass.getName(), e); +                    } +                } +            } +        } +        return jaxbContextCache.get(responseClass); +    } +} diff --git a/src/main/java/com/p4square/ccbapi/HTTPInterface.java b/src/main/java/com/p4square/ccbapi/HTTPInterface.java new file mode 100644 index 0000000..11d87ec --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/HTTPInterface.java @@ -0,0 +1,34 @@ +package com.p4square.ccbapi; + +import com.p4square.ccbapi.exception.CCBException; +import com.p4square.ccbapi.exception.CCBRetryableErrorException; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Map; + +/** + * HTTPInterface exists to separate the HTTP client implementation from the CCB client. + * + * The concern here is that it may not be possible or desirable to take a dependency on the + * Apache HTTP Client library. If that case arises the CCBAPIClient and the HTTPInterface + * implementation can be split into separate packages. For simplicity's sake I'm not doing + * that now. But if it needs to be done at a later time the code will already be isolated. + */ +public interface HTTPInterface extends Closeable { +    /** +     * Send an HTTP POST request to the given URI. +     * +     * The form data for the request is specified in the form Map. +     * +     * @param uri The URI to request. +     * @param form Map of key/value pairs to send as form data. +     * @return The response received. +     * @throws com.p4square.ccbapi.exception.CCBRetryableErrorException +     * @throws CCBRetryableErrorException if a retryable error occurs. +     * @throws IOException If a non-retryable error occurs. +     */ +    InputStream sendPostRequest(URI uri, Map<String, String> form) throws IOException; +} diff --git a/src/main/java/com/p4square/ccbapi/LocalDateTimeXmlAdapter.java b/src/main/java/com/p4square/ccbapi/LocalDateTimeXmlAdapter.java new file mode 100644 index 0000000..f6ee565 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/LocalDateTimeXmlAdapter.java @@ -0,0 +1,26 @@ +package com.p4square.ccbapi; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * XmlAdapter implementation to convert CCB dates to LocalDateTime. + * + * Note that the datetime format used by CCB is not ISO-8601 formatted. + */ +public class LocalDateTimeXmlAdapter extends XmlAdapter<String, LocalDateTime> { + +    private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + +    @Override +    public LocalDateTime unmarshal(final String value) throws Exception { +        return LocalDateTime.parse(value, FORMAT); +    } + +    @Override +    public String marshal(final LocalDateTime value) throws Exception { +        return value.format(FORMAT); +    } + +}
\ No newline at end of file diff --git a/src/main/java/com/p4square/ccbapi/LocalDateXmlAdapter.java b/src/main/java/com/p4square/ccbapi/LocalDateXmlAdapter.java new file mode 100644 index 0000000..861f18f --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/LocalDateXmlAdapter.java @@ -0,0 +1,21 @@ +package com.p4square.ccbapi; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.time.LocalDate; + +/** + * XmlAdapter implementation for LocalDate. + */ +public class LocalDateXmlAdapter extends XmlAdapter<String, LocalDate> { + +    @Override +    public LocalDate unmarshal(final String value) throws Exception { +        return LocalDate.parse(value); +    } + +    @Override +    public String marshal(final LocalDate value) throws Exception { +        return value.toString(); +    } + +}
\ No newline at end of file diff --git a/src/main/java/com/p4square/ccbapi/exception/CCBErrorResponseException.java b/src/main/java/com/p4square/ccbapi/exception/CCBErrorResponseException.java new file mode 100644 index 0000000..dd13f75 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/exception/CCBErrorResponseException.java @@ -0,0 +1,24 @@ +package com.p4square.ccbapi.exception; + +import com.p4square.ccbapi.model.CCBErrorResponse; + +import java.util.List; + +/** + * CCBErrorResponseException is thrown when the CCB API returns one or more error responses. + */ +public class CCBErrorResponseException extends CCBException { +    private final List<CCBErrorResponse> errors; + +    public CCBErrorResponseException(List<CCBErrorResponse> errors) { +        super("CCB API service responded with errors: " + errors); +        this.errors = errors; +    } + +    /** +     * @return The error response returned by the service. +     */ +    public List<CCBErrorResponse> getErrors() { +        return errors; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/exception/CCBException.java b/src/main/java/com/p4square/ccbapi/exception/CCBException.java new file mode 100644 index 0000000..6d74ea4 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/exception/CCBException.java @@ -0,0 +1,16 @@ +package com.p4square.ccbapi.exception; + +import java.io.IOException; + +/** + * Common exception class for all CCB API library exceptions. + */ +public class CCBException extends IOException { +    public CCBException(String message) { +        super(message); +    } + +    public CCBException(String message, Throwable cause) { +        super(message, cause); +    } +} diff --git a/src/main/java/com/p4square/ccbapi/exception/CCBParseException.java b/src/main/java/com/p4square/ccbapi/exception/CCBParseException.java new file mode 100644 index 0000000..aee0e34 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/exception/CCBParseException.java @@ -0,0 +1,14 @@ +package com.p4square.ccbapi.exception; + +/** + * CCBParseException is thrown when a response from CCB cannot be parsed. + */ +public class CCBParseException extends CCBException { +    public CCBParseException(String message) { +        super(message); +    } + +    public CCBParseException(String message, Throwable cause) { +        super(message, cause); +    } +} diff --git a/src/main/java/com/p4square/ccbapi/exception/CCBRetryableErrorException.java b/src/main/java/com/p4square/ccbapi/exception/CCBRetryableErrorException.java new file mode 100644 index 0000000..c222662 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/exception/CCBRetryableErrorException.java @@ -0,0 +1,12 @@ +package com.p4square.ccbapi.exception; + +/** + * CCBRetryableErrorException is thrown when a retryable error is received. + * + * The caller may retry the request with an appropriate back-off. + */ +public class CCBRetryableErrorException extends CCBException { +    public CCBRetryableErrorException(final String message) { +        super(message); +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/Address.java b/src/main/java/com/p4square/ccbapi/model/Address.java new file mode 100644 index 0000000..9bbd6e3 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/Address.java @@ -0,0 +1,125 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.*; + +/** + * Representation of a United States postal address. + */ +@XmlRootElement(name="address") +@XmlAccessorType(XmlAccessType.NONE) +public class Address { +    @XmlType(namespace="Address") +    public enum Type { +        @XmlEnumValue("mailing") MAILING, +        @XmlEnumValue("home") HOME, +        @XmlEnumValue("work") WORK, +        @XmlEnumValue("other") OTHER; +    } + +    @XmlAttribute(name="type") +    private Type type; + +    @XmlElement(name="street_address") +    private String streetAddress; + +    @XmlElement(name="city") +    private String city; + +    @XmlElement(name="state") +    private String state; + +    @XmlElement(name="zip") +    private String zip; + +    @XmlElement(name="country") +    private Country country; + +    @XmlElement(name="line_1") +    private String line_1; + +    @XmlElement(name="line_2") +    private String line_2; + +    @XmlElement(name="latitude") +    private String latitude; + +    @XmlElement(name="longitude") +    private String longitude; + +    public Type getType() { +        return type; +    } + +    public void setType(Type type) { +        this.type = type; +    } + +    public String getStreetAddress() { +        return streetAddress; +    } + +    public void setStreetAddress(String streetAddress) { +        this.streetAddress = streetAddress; +        updateAddressLines(); +    } + +    public String getCity() { +        return city; +    } + +    public void setCity(String city) { +        this.city = city; +        updateAddressLines(); +    } + +    public String getState() { +        return state; +    } + +    public void setState(String state) { +        if (state.length() < 2 || state.length() > 3) { +            throw new IllegalArgumentException("Invalid state code."); +        } +        this.state = state.toUpperCase(); +        updateAddressLines(); +    } + +    public String getZip() { +        return zip; +    } + +    public void setZip(String zip) { +        this.zip = zip; +        updateAddressLines(); +    } + +    public Country getCountry() { +        return country; +    } + +    public void setCountry(Country country) { +        this.country = country; +        updateAddressLines(); +    } + +    public String getLine_1() { +        return line_1; +    } + +    public String getLine_2() { +        return line_2; +    } + +    public String getLatitude() { +        return latitude; +    } + +    public String getLongitude() { +        return longitude; +    } + +    private void updateAddressLines() { +        this.line_1 = streetAddress; +        this.line_2 = String.format("%s, %s %s", city, state, zip); +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/CCBAPIResponse.java b/src/main/java/com/p4square/ccbapi/model/CCBAPIResponse.java new file mode 100644 index 0000000..d54c8a9 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/CCBAPIResponse.java @@ -0,0 +1,35 @@ +package com.p4square.ccbapi.model; + +import com.p4square.ccbapi.model.CCBErrorResponse; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import java.util.List; + +/** + * Base class for all responses from the CCB API. + */ +public abstract class CCBAPIResponse { + +    @XmlElementWrapper(name="errors", nillable=true) +    @XmlElement(name="error") +    private List<CCBErrorResponse> errorResponses; + +    /** +     * Return the error message if present. +     * +     * @return A CCBErrorResponse if an error occurred. Null if the request was successful. +     */ +    public List<CCBErrorResponse> getErrors() { +        return errorResponses; +    } + +    /** +     * Set the error response. +     * +     * @param error The CCBErrorResponse to set. +     */ +    public void setErrors(final List<CCBErrorResponse> errors) { +        this.errorResponses = errors; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/CCBErrorResponse.java b/src/main/java/com/p4square/ccbapi/model/CCBErrorResponse.java new file mode 100644 index 0000000..7fb1948 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/CCBErrorResponse.java @@ -0,0 +1,48 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.*; + +/** + * Representation of an error response returned by CCB. + */ +@XmlRootElement(name="error") +@XmlAccessorType(XmlAccessType.NONE) +public class CCBErrorResponse { +    @XmlAttribute +    private int number; + +    @XmlAttribute +    private String type; + +    @XmlValue +    private String description; + +    public int getNumber() { +        return number; +    } + +    public void setNumber(int number) { +        this.number = number; +    } + +    public String getType() { +        return type; +    } + +    public void setType(String type) { +        this.type = type; +    } + +    public String getDescription() { +        return description; +    } + +    public void setDescription(String description) { +        this.description = description; +    } + +    @Override +    public String toString() { +        return String.format("%03d %s Error: %s", getNumber(), getType(), getDescription()); +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/Country.java b/src/main/java/com/p4square/ccbapi/model/Country.java new file mode 100644 index 0000000..e6936a5 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/Country.java @@ -0,0 +1,48 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.*; + +/** + * Country code and name pair. + */ +@XmlRootElement(name="country") +@XmlAccessorType(XmlAccessType.NONE) +public class Country { + +    @XmlAttribute(name="code") +    private String code; + +    @XmlValue +    private String name; + +    /** +     * @return The two letter country code. +     */ +    public String getCountryCode() { +        return code; +    } + +    /** +     * Set the two letter country code. +     * +     * This class does not attempt to resolve the country name from the country code. +     * When the country code is set the name will become null. + +     * @param code A two letter countey code. +     * @throws IllegalArgumentException if the country code is not valid. +     */ +    public void setCountryCode(final String code) { +        if (code.length() != 2) { +            throw new IllegalArgumentException("Argument must be a two letter country code."); +        } +        this.code = code.toUpperCase(); +        this.name = null; +    } + +    /** +     * @return The country name or null if the country name is unknown. +     */ +    public String getName() { +        return name; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/CustomDateFieldValue.java b/src/main/java/com/p4square/ccbapi/model/CustomDateFieldValue.java new file mode 100644 index 0000000..01436b7 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/CustomDateFieldValue.java @@ -0,0 +1,24 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import java.time.LocalDate; + +/** + * A user-defined date field and the associated value. + */ +@XmlAccessorType(XmlAccessType.NONE) +public class CustomDateFieldValue extends CustomField { + +    @XmlElement(name="date") +    private LocalDate date; + +    public LocalDate getDate() { +        return date; +    } + +    public void setDate(final LocalDate date) { +        this.date = date; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/CustomField.java b/src/main/java/com/p4square/ccbapi/model/CustomField.java new file mode 100644 index 0000000..c9917e4 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/CustomField.java @@ -0,0 +1,74 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Representation of a Custom/User Defined Field. + */ +@XmlRootElement(name="custom_field") +@XmlAccessorType(XmlAccessType.PROPERTY) +public class CustomField { +    private String name; +    private String label; +    private boolean adminOnly; + +    /** +     * Create an empty CustomField object. +     */ +    public CustomField() { +        // Default constructor +    } + +    /** +     * Create a CustomField object with the name and label set. +     * +     * adminOnly will be false by default. +     * +     * @param name The CustomField name. +     * @param label The CustomField label. +     */ +    public CustomField(final String name, final String label) { +        this(name, label, false); +    } + +    /** +     * Create a CustomField object with the name, label, and adminOnly fields set. +     * +     * @param name The CustomField name. +     * @param label The CustomField label. +     * @param adminOnly The value of the adminOnly field. +     */ +    public CustomField(final String name, final String label, final boolean adminOnly) { +        this.name = name; +        this.label = label; +        this.adminOnly = adminOnly; +    } + +    public String getName() { +        return name; +    } + +    public void setName(String name) { +        this.name = name; +    } + +    public String getLabel() { +        return label; +    } + +    public void setLabel(String label) { +        this.label = label; +    } + +    public boolean isAdminOnly() { +        return adminOnly; +    } + +    @XmlElement(name="admin_only") +    public void setAdminOnly(boolean adminOnly) { +        this.adminOnly = adminOnly; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/CustomFieldCollection.java b/src/main/java/com/p4square/ccbapi/model/CustomFieldCollection.java new file mode 100644 index 0000000..0a1f87d --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/CustomFieldCollection.java @@ -0,0 +1,137 @@ +package com.p4square.ccbapi.model; + +import java.util.*; + +/** + * A collection of CustomField derivatives with indexes to find a custom field and value by either name or label. + * + * This collection will only ever contain one value for any given name or label. + */ +public class CustomFieldCollection<T extends CustomField> implements Collection<T> { + +    private final List<T> values; +    private final Map<String, T> fieldLabelToValue; +    private final Map<String, T> fieldNameToValue; + +    public CustomFieldCollection() { +        this.values = new ArrayList<>(); +        this.fieldLabelToValue = new HashMap<>(); +        this.fieldNameToValue = new HashMap<>(); +    } + +    /** +     * Return the entry with given name (e.g. "udf_text_1"). +     * +     * @param name A CCB field name. +     * @return The entry associated with the field. +     */ +    public T getByName(final String name) { +        return fieldNameToValue.get(name); +    } + +    /** +     * Return the entry with given label (e.g. "Favorite Book"). +     * +     * @param label A CCB field label. +     * @return The entry associated with the field. +     */ +    public T getByLabel(final String label) { +        return fieldLabelToValue.get(label); +    } + +    public List<T> asList() { +        return Collections.unmodifiableList(values); +    } + +    @Override +    public int size() { +        return values.size(); +    } + +    @Override +    public boolean isEmpty() { +        return values.isEmpty(); +    } + +    @Override +    public boolean contains(final Object o) { +        return values.contains(o); +    } + +    @Override +    public Iterator<T> iterator() { +        return values.iterator(); +    } + +    @Override +    public Object[] toArray() { +        return values.toArray(); +    } + +    @Override +    public <T1> T1[] toArray(final T1[] a) { +        return values.toArray(a); +    } + +    @Override +    public boolean add(final T t) { +        // Clean up overwritten indexes. +        final T previousValueByName = fieldNameToValue.get(t.getName()); +        if (previousValueByName != null) { +            remove(previousValueByName); +        } + +        final T previousValueByLabel = fieldLabelToValue.get(t.getLabel()); +        if (previousValueByLabel != null) { +            remove(previousValueByLabel); +        } + +        fieldNameToValue.put(t.getName(), t); +        fieldLabelToValue.put(t.getLabel(), t); +        return values.add(t); +    } + +    @Override +    public boolean remove(final Object o) { +        if (values.remove(o)) { +            final T entry = (T) o; +            fieldNameToValue.remove(entry.getName()); +            fieldLabelToValue.remove(entry.getLabel()); +            return true; +        } +        return false; +    } + +    @Override +    public boolean containsAll(Collection<?> c) { +        return values.containsAll(c); +    } + +    @Override +    public boolean addAll(final Collection<? extends T> c) { +        boolean result = false; +        for (T obj : c) { +            result |= add(obj); +        } +        return result; +    } + +    @Override +    public boolean removeAll(final Collection<?> c) { +        boolean result = false; +        for (Object obj : c) { +            result |= remove(obj); +        } +        return result; +    } + +    @Override +    public boolean retainAll(final Collection<?> c) { +        throw new UnsupportedOperationException(); +    } + +    @Override +    public void clear() { +        values.clear(); +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/CustomPulldownFieldValue.java b/src/main/java/com/p4square/ccbapi/model/CustomPulldownFieldValue.java new file mode 100644 index 0000000..02a962f --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/CustomPulldownFieldValue.java @@ -0,0 +1,23 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +/** + * A user-defined pulldown field and the associated value. + */ +@XmlAccessorType(XmlAccessType.NONE) +public class CustomPulldownFieldValue extends CustomField { + +    @XmlElement(name="selection") +    private PulldownSelection selection; + +    public PulldownSelection getSelection() { +        return selection; +    } + +    public void setSelection(final PulldownSelection selection) { +        this.selection = selection; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/CustomTextFieldValue.java b/src/main/java/com/p4square/ccbapi/model/CustomTextFieldValue.java new file mode 100644 index 0000000..71595e3 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/CustomTextFieldValue.java @@ -0,0 +1,23 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +/** + * A user-defined text field and the associated value. + */ +@XmlAccessorType(XmlAccessType.NONE) +public class CustomTextFieldValue extends CustomField { + +    @XmlElement(name="text") +    private String text; + +    public String getText() { +        return text; +    } + +    public void setText(final String text) { +        this.text = text; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/FamilyMemberReference.java b/src/main/java/com/p4square/ccbapi/model/FamilyMemberReference.java new file mode 100644 index 0000000..212426c --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/FamilyMemberReference.java @@ -0,0 +1,34 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +/** + * Reference to a family member. + */ +@XmlAccessorType(XmlAccessType.NONE) +public class FamilyMemberReference { + +    @XmlElement(name="individual") +    private IndividualReference individualReference; + +    @XmlElement(name="family_position") +    private FamilyPosition familyPosition; + +    public IndividualReference getIndividualReference() { +        return individualReference; +    } + +    public void setIndividualReference(IndividualReference individualReference) { +        this.individualReference = individualReference; +    } + +    public FamilyPosition getFamilyPosition() { +        return familyPosition; +    } + +    public void setFamilyPosition(FamilyPosition familyPosition) { +        this.familyPosition = familyPosition; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/FamilyPosition.java b/src/main/java/com/p4square/ccbapi/model/FamilyPosition.java new file mode 100644 index 0000000..ecd1e37 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/FamilyPosition.java @@ -0,0 +1,26 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlEnumValue; + +/** + * Enumeration of the supported values for the family_position field of an IndividualProfile. + */ +public enum FamilyPosition { +    @XmlEnumValue("Primary Contact") PRIMARY_CONTACT("h"), +    @XmlEnumValue("Spouse") SPOUSE("s"), +    @XmlEnumValue("Child") CHILD("c"), +    @XmlEnumValue("Other") OTHER("o"); + +    private final String code; + +    FamilyPosition(String code) { +        this.code = code; +    } + +    /** +     * @return A one character string representing the enum value. +     */ +    public String getCode() { +        return this.code; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/FamilyReference.java b/src/main/java/com/p4square/ccbapi/model/FamilyReference.java new file mode 100644 index 0000000..0af220b --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/FamilyReference.java @@ -0,0 +1,34 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlValue; + +/** + * Reference to an family by id and name. + */ +@XmlAccessorType(XmlAccessType.NONE) +public class FamilyReference { +    @XmlAttribute(name="id") +    private int familyId; + +    @XmlValue +    private String name; + +    public int getFamilyId() { +        return familyId; +    } + +    public void setFamilyId(int familyId) { +        this.familyId = familyId; +    } + +    public String getName() { +        return name; +    } + +    public void setName(String name) { +        this.name = name; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/Gender.java b/src/main/java/com/p4square/ccbapi/model/Gender.java new file mode 100644 index 0000000..eabaa42 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/Gender.java @@ -0,0 +1,24 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlEnumValue; + +/** + * Enum representing the gender of an individual in CCB. + */ +public enum Gender { +    @XmlEnumValue("M") MALE("M"), +    @XmlEnumValue("F") FEMALE("F"); + +    private final String code; + +    Gender(String code) { +        this.code = code; +    } + +    /** +     * @return A one character string representing the enum value. +     */ +    public String getCode() { +        return this.code; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/GetCustomFieldLabelsResponse.java b/src/main/java/com/p4square/ccbapi/model/GetCustomFieldLabelsResponse.java new file mode 100644 index 0000000..9f56036 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/GetCustomFieldLabelsResponse.java @@ -0,0 +1,37 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +/** + * GetCustomFieldLabelsResponse models the response returned by the custom_field_labels API. + */ +@XmlRootElement(name="response") +@XmlAccessorType(XmlAccessType.NONE) +public class GetCustomFieldLabelsResponse extends CCBAPIResponse { + +    @XmlElementWrapper(name = "custom_fields") +    @XmlElement(name="custom_field") +    private List<CustomField> customFields; + +    public GetCustomFieldLabelsResponse() { +        customFields = new ArrayList<>(); +    } + +    /** +     * @return The list of custom field names and labels. +     */ +    public List<CustomField> getCustomFields() { +        return customFields; +    } + +    /** +     * Set the list of custom fields. +     * +     * @param fields The list to include in the response. +     */ +    public void setCustomFields(final List<CustomField> fields) { +        this.customFields = fields; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/GetIndividualProfilesRequest.java b/src/main/java/com/p4square/ccbapi/model/GetIndividualProfilesRequest.java new file mode 100644 index 0000000..589de3c --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/GetIndividualProfilesRequest.java @@ -0,0 +1,159 @@ +package com.p4square.ccbapi.model; + +import java.time.LocalDate; + +/** + * GetIndividualProfilesRequest is the set of options for retrieving individual profiles. + */ +public class GetIndividualProfilesRequest { + +    // Used with individual_profiles +    private LocalDate modifiedSince; +    private Boolean includeInactive; +    private int page; +    private int perPage; + +    // Used with individual_profile_from_id +    private int id; + +    // Used with individual_profile_from_login_password +    private String login; +    private String password; + +    // Used with individual_profile_from_micr +    private String routingNumber; +    private String accountNumber; + +    public int getId() { +        return id; +    } + +    /** +     * Request the IndividualProfile for the given individual id. +     * +     * This option is mutually exclusive with {@link #withLoginPassword(String, String)} +     * and {@link #withMICR(String, String)}. +     * +     * @param id The id. +     * @return this. +     */ +    public GetIndividualProfilesRequest withIndividualId(final int id) { +        this.id = id; +        this.login = this.password = this.accountNumber = this.routingNumber = null; +        return this; +    } + +    public String getLogin() { +        return login; +    } + +    public String getPassword() { +        return password; +    } + +    /** +     * Request the IndividualProfile for the given login and password. +     * +     * This option is mutually exclusive with {@link #withIndividualId(int)} +     * and {@link #withMICR(String, String)}. +     * +     * @param login The individual's login. +     * @param password The individual's password. +     * @return this. +     */ +    public GetIndividualProfilesRequest withLoginPassword(final String login, final String password) { +        this.login = login; +        this.password = password; +        this.id = 0; +        this.accountNumber = this.routingNumber = null; +        return this; +    } + +    public String getRoutingNumber() { +        return routingNumber; +    } + +    public String getAccountNumber() { +        return accountNumber; +    } + +    /** +     * Request the IndividualProfile for the given bank account information. +     * +     * This option is mutually exclusive with {@link #withIndividualId(int)} +     * and {@link #withLoginPassword(String, String)}. +     * +     * @param routingNumber The individual's bank routing number. +     * @param accountNumber The individual's bank account number. +     * @return this. +     */ +    public GetIndividualProfilesRequest withMICR(final String routingNumber, final String accountNumber) { +        this.routingNumber = routingNumber; +        this.accountNumber = accountNumber; +        return this; +    } + +    public LocalDate getModifiedSince() { +        return modifiedSince; +    } + +    /** +     * Request only IndividualProfiles modified since a given date. +     * +     * This option is only applicable when requesting all individuals. +     * +     * @param modifiedSince The date. +     * @return this. +     */ +    public GetIndividualProfilesRequest withModifiedSince(final LocalDate modifiedSince) { +        this.modifiedSince = modifiedSince; +        return this; +    } + +    public Boolean getIncludeInactive() { +        return includeInactive; +    } + +    public GetIndividualProfilesRequest withIncludeInactive(final boolean includeInactive) { +        this.includeInactive = includeInactive; +        return this; +    } + +    public int getPage() { +        return page; +    } + +    /** +     * Select the page of results when perPage is also specified. +     * +     * This option is only applicable when requesting all individuals. +     * +     * Defaults to 1 if {@link #withPerPage(int)} is specified on the request. +     * +     * @param page The starting page number. +     * @return this. +     */ +    public GetIndividualProfilesRequest withPage(final int page) { +        this.page = page; +        return this; +    } + +    public int getPerPage() { +        return perPage; +    } + +    /** +     * Limit the number of IndividualProfiles returned. +     * +     * This option is only applicable when requesting all individuals. +     * +     * Defaults to 25 if {@link #withPage(int)} is specified on the request. +     * +     * @param perPage The maximum number to return. +     * @return this. +     */ +    public GetIndividualProfilesRequest withPerPage(int perPage) { +        this.perPage = perPage; +        return this; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/GetIndividualProfilesResponse.java b/src/main/java/com/p4square/ccbapi/model/GetIndividualProfilesResponse.java new file mode 100644 index 0000000..f88bcf7 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/GetIndividualProfilesResponse.java @@ -0,0 +1,27 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.*; +import java.util.List; + +/** + * GetCustomFieldLabelsResponse models the response of a variety of APIs which return one or more Individual Profiles. + */ +@XmlRootElement(name="response") +@XmlAccessorType(XmlAccessType.NONE) +public class GetIndividualProfilesResponse extends CCBAPIResponse { + +    @XmlElementWrapper(name = "individuals") +    @XmlElement(name="individual") +    private List<IndividualProfile> individuals; + +    /** +     * @return The list of individuals retrieved from CCB. +     */ +    public List<IndividualProfile> getIndividuals() { +        return individuals; +    } + +    public void setIndividuals(List<IndividualProfile> individuals) { +        this.individuals = individuals; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/IndividualProfile.java b/src/main/java/com/p4square/ccbapi/model/IndividualProfile.java new file mode 100644 index 0000000..114d071 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/IndividualProfile.java @@ -0,0 +1,449 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Representation of a Individual Profile. + */ +@XmlRootElement(name="individual") +@XmlAccessorType(XmlAccessType.NONE) +public class IndividualProfile { + +    @XmlAttribute(name="id") +    private int id; + +    @XmlElement(name="other_id") +    private int otherId; + +    @XmlElement(name="sync_id") +    private int syncId; + +    @XmlElement(name="giving_number") +    private String givingNumber; + +    @XmlElement(name="active") +    private boolean active; + +    @XmlElement(name="first_name") +    private String firstName; + +    @XmlElement(name="last_name") +    private String lastName; + +    @XmlElement(name="middle_name") +    private String middleName; + +    @XmlElement(name="legal_first_name") +    private String legalFirstName; + +    @XmlElement(name="full_name") +    private String fullName; + +    @XmlElement(name="salutation") +    private String salutation; + +    @XmlElement(name="suffix") +    private String suffix; + +    @XmlElement(name="image") +    private String imageUrl; + +    @XmlElement(name="family_position", defaultValue = "Primary Contact") +    private FamilyPosition familyPosition; + +    @XmlElement(name="family") +    private FamilyReference family; + +    @XmlElement(name="family_image") +    private String familyImageUrl; + +    @XmlElementWrapper(name="family_members") +    @XmlElement(name="family_member") +    private List<FamilyMemberReference> familyMembers; + +    @XmlElement(name="email") +    private String email; + +    @XmlElement(name="login") +    private String login; + +    @XmlElement(name="allergies") +    private String allergies; + +    @XmlElement(name="confirmed_no_allergies") +    private boolean confirmedNoAllergies; + +    @XmlElement(name="gender") +    private Gender gender; + +    @XmlElement(name="marital_status", defaultValue="") +    private MaritalStatus maritalStatus; + +    @XmlElement(name="birthday") +    private LocalDate birthday; + +    @XmlElement(name="anniversary") +    private LocalDate anniversary; + +    @XmlElement(name="deceased") +    private LocalDate deceased; + +    @XmlElement(name="membership_date") +    private LocalDate membershipStartDate; + +    @XmlElement(name="membership_end") +    private LocalDate membershipEndDate; + +    @XmlElement(name="baptized") +    private boolean baptized; + +    @XmlElement(name="creator") +    private IndividualReference createdBy; + +    @XmlElement(name="created") +    private LocalDateTime createdTime; + +    @XmlElement(name="modifier") +    private IndividualReference modifiedBy; + +    @XmlElement(name="modified") +    private LocalDateTime modifiedTime; + +    @XmlElementWrapper(name="addresses") +    @XmlElement(name="address") +    private List<Address> addresses; + +    @XmlElementWrapper(name="phones") +    @XmlElement(name="phone") +    private List<Phone> phones; + +    @XmlElementWrapper(name="user_defined_text_fields") +    @XmlElement(name="user_defined_text_field") +    private CustomFieldCollection<CustomTextFieldValue> customTextFields; + +    @XmlElementWrapper(name="user_defined_date_fields") +    @XmlElement(name="user_defined_date_field") +    private CustomFieldCollection<CustomDateFieldValue> customDateFields; + +    @XmlElementWrapper(name="user_defined_pulldown_fields") +    @XmlElement(name="user_defined_pulldown_field") +    private CustomFieldCollection<CustomPulldownFieldValue> customPulldownFields; + +    public IndividualProfile() { +        familyMembers = new ArrayList<>(); +        addresses = new ArrayList<>(); +        phones = new ArrayList<>(); +        customTextFields = new CustomFieldCollection<>(); +        customDateFields = new CustomFieldCollection<>(); +        customPulldownFields = new CustomFieldCollection<>(); +    } + +    public int getId() { +        return id; +    } + +    public void setId(int id) { +        this.id = id; +    } + +    public int getOtherId() { +        return otherId; +    } + +    public IndividualProfile setOtherId(int otherId) { +        this.otherId = otherId; +        return this; +    } + +    public int getSyncId() { +        return syncId; +    } + +    public void setSyncId(int syncId) { +        this.syncId = syncId; +    } + +    public String getGivingNumber() { +        return givingNumber; +    } + +    public void setGivingNumber(String givingNumber) { +        this.givingNumber = givingNumber; +    } + +    public boolean isActive() { +        return active; +    } + +    public void setActive(boolean active) { +        this.active = active; +    } + +    public String getFirstName() { +        return firstName; +    } + +    public void setFirstName(String firstName) { +        this.firstName = firstName; +    } + +    public String getLastName() { +        return lastName; +    } + +    public void setLastName(String lastName) { +        this.lastName = lastName; +    } + +    public String getMiddleName() { +        return middleName; +    } + +    public void setMiddleName(String middleName) { +        this.middleName = middleName; +    } + +    public String getLegalFirstName() { +        return legalFirstName; +    } + +    public void setLegalFirstName(String legalFirstName) { +        this.legalFirstName = legalFirstName; +    } + +    public String getFullName() { +        return fullName; +    } + +    public void setFullName(String fullName) { +        this.fullName = fullName; +    } + +    public String getSalutation() { +        return salutation; +    } + +    public void setSalutation(String salutation) { +        this.salutation = salutation; +    } + +    public String getSuffix() { +        return suffix; +    } + +    public void setSuffix(String suffix) { +        this.suffix = suffix; +    } + +    public String getImageUrl() { +        return imageUrl; +    } + +    public void setImageUrl(String imageUrl) { +        this.imageUrl = imageUrl; +    } + +    public FamilyPosition getFamilyPosition() { +        return familyPosition; +    } + +    public void setFamilyPosition(FamilyPosition familyPosition) { +        this.familyPosition = familyPosition; +    } + +    public FamilyReference getFamily() { +        return family; +    } + +    public void setFamily(FamilyReference family) { +        this.family = family; +    } + +    public String getFamilyImageUrl() { +        return familyImageUrl; +    } + +    public void setFamilyImageUrl(String familyImageUrl) { +        this.familyImageUrl = familyImageUrl; +    } + +    public List<FamilyMemberReference> getFamilyMembers() { +        return familyMembers; +    } + +    public void setFamilyMembers(List<FamilyMemberReference> familyMembers) { +        this.familyMembers = familyMembers; +    } + +    public String getEmail() { +        return email; +    } + +    public void setEmail(String email) { +        this.email = email; +    } + +    public String getLogin() { +        return login; +    } + +    public void setLogin(String login) { +        this.login = login; +    } + +    public String getAllergies() { +        return allergies; +    } + +    public void setAllergies(String allergies) { +        this.allergies = allergies; +    } + +    public boolean isConfirmedNoAllergies() { +        return confirmedNoAllergies; +    } + +    public void setConfirmedNoAllergies(boolean confirmedNoAllergies) { +        this.confirmedNoAllergies = confirmedNoAllergies; +    } + +    public Gender getGender() { +        return gender; +    } + +    public void setGender(Gender gender) { +        this.gender = gender; +    } + +    public MaritalStatus getMaritalStatus() { +        return maritalStatus; +    } + +    public void setMaritalStatus(MaritalStatus maritalStatus) { +        this.maritalStatus = maritalStatus; +    } + +    public LocalDate getBirthday() { +        return birthday; +    } + +    public void setBirthday(LocalDate birthday) { +        this.birthday = birthday; +    } + +    public LocalDate getAnniversary() { +        return anniversary; +    } + +    public void setAnniversary(LocalDate anniversary) { +        this.anniversary = anniversary; +    } + +    public LocalDate getDeceased() { +        return deceased; +    } + +    public void setDeceased(LocalDate deceased) { +        this.deceased = deceased; +    } + +    public LocalDate getMembershipStartDate() { +        return membershipStartDate; +    } + +    public void setMembershipStartDate(LocalDate membershipStartDate) { +        this.membershipStartDate = membershipStartDate; +    } + +    public LocalDate getMembershipEndDate() { +        return membershipEndDate; +    } + +    public void setMembershipEndDate(LocalDate membershipEndDate) { +        this.membershipEndDate = membershipEndDate; +    } + +    public boolean isBaptized() { +        return baptized; +    } + +    public void setBaptized(boolean baptized) { +        this.baptized = baptized; +    } + +    public IndividualReference getCreatedBy() { +        return createdBy; +    } + +    public void setCreatedBy(IndividualReference createdBy) { +        this.createdBy = createdBy; +    } + +    public LocalDateTime getCreatedTime() { +        return createdTime; +    } + +    public void setCreatedTime(LocalDateTime createdTime) { +        this.createdTime = createdTime; +    } + +    public IndividualReference getModifiedBy() { +        return modifiedBy; +    } + +    public void setModifiedBy(IndividualReference modifiedBy) { +        this.modifiedBy = modifiedBy; +    } + +    public LocalDateTime getModifiedTime() { +        return modifiedTime; +    } + +    public void setModifiedTime(LocalDateTime modifiedTime) { +        this.modifiedTime = modifiedTime; +    } + +    public List<Address> getAddresses() { +        return addresses; +    } + +    public void setAddresses(List<Address> addresses) { +        this.addresses = addresses; +    } + +    public List<Phone> getPhones() { +        return phones; +    } + +    public void setPhones(List<Phone> phones) { +        this.phones = phones; +    } + +    public CustomFieldCollection<CustomTextFieldValue> getCustomTextFields() { +        return customTextFields; +    } + +    public void setCustomTextFields(CustomFieldCollection<CustomTextFieldValue> customTextFields) { +        this.customTextFields = customTextFields; +    } + +    public CustomFieldCollection<CustomDateFieldValue> getCustomDateFields() { +        return customDateFields; +    } + +    public void setCustomDateFields(CustomFieldCollection<CustomDateFieldValue> customDateFields) { +        this.customDateFields = customDateFields; +    } + +    public CustomFieldCollection<CustomPulldownFieldValue> getCustomPulldownFields() { +        return customPulldownFields; +    } + +    public void setCustomPulldownFields(CustomFieldCollection<CustomPulldownFieldValue> customPulldownFields) { +        this.customPulldownFields = customPulldownFields; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/IndividualReference.java b/src/main/java/com/p4square/ccbapi/model/IndividualReference.java new file mode 100644 index 0000000..1b25b90 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/IndividualReference.java @@ -0,0 +1,34 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlValue; + +/** + * Reference to an individual by id and name. + */ +@XmlAccessorType(XmlAccessType.NONE) +public class IndividualReference { +    @XmlAttribute(name="id") +    private int individualId; + +    @XmlValue +    private String name; + +    public int getIndividualId() { +        return individualId; +    } + +    public void setIndividualId(int individualId) { +        this.individualId = individualId; +    } + +    public String getName() { +        return name; +    } + +    public void setName(String name) { +        this.name = name; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/MaritalStatus.java b/src/main/java/com/p4square/ccbapi/model/MaritalStatus.java new file mode 100644 index 0000000..d9e5921 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/MaritalStatus.java @@ -0,0 +1,28 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlEnumValue; + +/** + * Enumeration of the possible values for the marital status field of an individual in CCB. + */ +public enum MaritalStatus { +    @XmlEnumValue("Single") SINGLE("s"), +    @XmlEnumValue("Married") MARRIED("m"), +    @XmlEnumValue("Widowed") WIDOWED("w"), +    @XmlEnumValue("Divorced") DIVORCED("d"), +    @XmlEnumValue("Separated") SEPARATED("p"), +    @XmlEnumValue("") NOT_SELECTED(" "); + +    private final String code; + +    MaritalStatus(String code) { +        this.code = code; +    } + +    /** +     * @return A one character string representing the enum value. +     */ +    public String getCode() { +        return this.code; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/Phone.java b/src/main/java/com/p4square/ccbapi/model/Phone.java new file mode 100644 index 0000000..eb66eb1 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/Phone.java @@ -0,0 +1,41 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.*; + +/** + * Phone Number and Type pair. + */ +@XmlRootElement(name="phone") +@XmlAccessorType(XmlAccessType.NONE) +public class Phone { +    @XmlType(namespace="Phone") +    public enum Type { +        @XmlEnumValue("contact") CONTACT, +        @XmlEnumValue("home") HOME, +        @XmlEnumValue("work") WORK, +        @XmlEnumValue("mobile") MOBILE, +        @XmlEnumValue("emergency") EMERGENCY; +    } + +    @XmlAttribute(name="type") +    private Type type; + +    @XmlValue +    private String number; + +    public Type getType() { +        return type; +    } + +    public void setType(Type type) { +        this.type = type; +    } + +    public String getNumber() { +        return number; +    } + +    public void setNumber(String number) { +        this.number = number; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/PulldownSelection.java b/src/main/java/com/p4square/ccbapi/model/PulldownSelection.java new file mode 100644 index 0000000..d473849 --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/PulldownSelection.java @@ -0,0 +1,34 @@ +package com.p4square.ccbapi.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlValue; + +/** + * A pull down field option. + */ +@XmlAccessorType(XmlAccessType.NONE) +public class PulldownSelection { +    @XmlAttribute(name="id") +    private int id; + +    @XmlValue +    private String label; + +    public int getId() { +        return id; +    } + +    public void setId(final int id) { +        this.id = id; +    } + +    public String getLabel() { +        return label; +    } + +    public void setLabel(final String label) { +        this.label = label; +    } +} diff --git a/src/main/java/com/p4square/ccbapi/model/package-info.java b/src/main/java/com/p4square/ccbapi/model/package-info.java new file mode 100644 index 0000000..154bded --- /dev/null +++ b/src/main/java/com/p4square/ccbapi/model/package-info.java @@ -0,0 +1,16 @@ +/** + * This package contains models for CCB API requests and responses. + */ +@XmlJavaTypeAdapters({ +        @XmlJavaTypeAdapter(type=LocalDate.class, value=LocalDateXmlAdapter.class), +        @XmlJavaTypeAdapter(type=LocalDateTime.class, value=LocalDateTimeXmlAdapter.class), +}) +package com.p4square.ccbapi.model; + +import com.p4square.ccbapi.LocalDateTimeXmlAdapter; +import com.p4square.ccbapi.LocalDateXmlAdapter; + +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters; +import java.time.LocalDate; +import java.time.LocalDateTime; diff --git a/src/test/java/com/p4square/ccbapi/CCBAPIClientTest.java b/src/test/java/com/p4square/ccbapi/CCBAPIClientTest.java new file mode 100644 index 0000000..e722e9a --- /dev/null +++ b/src/test/java/com/p4square/ccbapi/CCBAPIClientTest.java @@ -0,0 +1,229 @@ +package com.p4square.ccbapi; + +import com.p4square.ccbapi.exception.CCBErrorResponseException; +import com.p4square.ccbapi.exception.CCBRetryableErrorException; +import com.p4square.ccbapi.model.GetCustomFieldLabelsResponse; +import com.p4square.ccbapi.model.GetIndividualProfilesRequest; +import com.p4square.ccbapi.model.GetIndividualProfilesResponse; +import org.junit.Before; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.time.LocalDate; +import java.util.Collections; +import java.util.Map; + +import org.easymock.EasyMock; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests for the CCBAPIClient. + */ +public class CCBAPIClientTest { + +    private HTTPInterface mockHttpClient; +    private CCBAPIClient client; + +    @Before +    public void setUp() throws Exception { +        mockHttpClient = EasyMock.mock(HTTPInterface.class); +        client = new TestCCBAPIClient(new URI("https://localhost:8080/api.php"), mockHttpClient); +    } + +    @Test +    public void testClose() throws Exception { +        // Set expectation. +        mockHttpClient.close(); +        EasyMock.replay(mockHttpClient); + +        // Test close. +        client.close(); + +        // Verify results. +        EasyMock.verify(mockHttpClient); +    } + +    @Test +    public void testGetCustomFieldLabelsSuccess() throws Exception { +        // Set expectation. +        URI expectedURI = new URI("https://localhost:8080/api.php?srv=custom_field_labels"); +        InputStream is = getClass().getResourceAsStream("model/ccb_custom_field_labels_response.xml"); +        EasyMock.expect(mockHttpClient.sendPostRequest(expectedURI, Collections.<String, String>emptyMap())) +                .andReturn(is); +        EasyMock.replay(mockHttpClient); + +        // Test custom_field_labels. +        GetCustomFieldLabelsResponse response = client.getCustomFieldLabels(); + +        // Verify results. +        EasyMock.verify(mockHttpClient); +        assertNull(response.getErrors()); +        assertEquals(27, response.getCustomFields().size()); +        assertEquals("udf_ind_text_1", response.getCustomFields().get(0).getName()); +        assertEquals("Favorite Movie", response.getCustomFields().get(0).getLabel()); +        assertEquals(false, response.getCustomFields().get(0).isAdminOnly()); +    } + +    @Test +    public void testGetCustomFieldLabelsError() throws Exception { +        // Set expectation. +        URI expectedURI = new URI("https://localhost:8080/api.php?srv=custom_field_labels"); +        InputStream is = getClass().getResourceAsStream("model/ccb_error_response.xml"); +        EasyMock.expect(mockHttpClient.sendPostRequest(expectedURI, Collections.<String, String>emptyMap())) +                .andReturn(is); +        EasyMock.replay(mockHttpClient); + +        try { +            // Call getCustomFieldLabels() and expect an exception. +            client.getCustomFieldLabels(); +            fail("No exception thrown."); + +        } catch (CCBErrorResponseException e) { +            // Assert error was received. +            EasyMock.verify(mockHttpClient); +            assertEquals(1, e.getErrors().size()); +            assertEquals(2, e.getErrors().get(0).getNumber()); +        } +    } + +    @Test(expected = CCBRetryableErrorException.class) +    public void testGetCustomFieldLabelsTimeout() throws Exception { +        // Setup mocks. +        EasyMock.expect(mockHttpClient.sendPostRequest(EasyMock.anyObject(URI.class), EasyMock.anyObject(Map.class))) +                .andThrow(new CCBRetryableErrorException("Retryable error")); +        EasyMock.replay(mockHttpClient); + +        // Call getCustomFieldLabels() and expect an exception. +        client.getCustomFieldLabels(); +    } + +    @Test(expected = IOException.class) +    public void testGetCustomFieldLabelsException() throws Exception { +        // Setup mocks. +        EasyMock.expect(mockHttpClient.sendPostRequest(EasyMock.anyObject(URI.class), EasyMock.anyObject(Map.class))) +                .andThrow(new IOException()); +        EasyMock.replay(mockHttpClient); + +        // Call getCustomFieldLabels() and expect an exception. +        client.getCustomFieldLabels(); +    } + +    @Test +    public void testGetIndividualProfilesById() throws Exception { +        // Set expectation. +        URI expectedURI = new URI("https://localhost:8080/api.php?srv=individual_profile_from_id&individual_id=48"); +        InputStream is = getClass().getResourceAsStream("model/ccb_individual_profile_response.xml"); +        EasyMock.expect(mockHttpClient.sendPostRequest(expectedURI, Collections.<String, String>emptyMap())) +                .andReturn(is); +        EasyMock.replay(mockHttpClient); + +        // Test individual_profile_from_id. +        GetIndividualProfilesRequest request = new GetIndividualProfilesRequest().withIndividualId(48); +        GetIndividualProfilesResponse response = client.getIndividualProfiles(request); + +        // Verify results. +        EasyMock.verify(mockHttpClient); +        assertNull(response.getErrors()); +        assertEquals(1, response.getIndividuals().size()); +        assertEquals(48, response.getIndividuals().get(0).getId()); +    } + +    @Test +    public void testGetIndividualProfilesByLogin() throws Exception { +        // Set expectation. +        URI expectedURI = new URI("https://localhost:8080/api.php?" +                + "srv=individual_profile_from_login_password&password=pass&login=user"); +        InputStream is = getClass().getResourceAsStream("model/ccb_individual_profile_response.xml"); +        EasyMock.expect(mockHttpClient.sendPostRequest(expectedURI, Collections.<String, String>emptyMap())) +                .andReturn(is); +        EasyMock.replay(mockHttpClient); + +        // Test individual_profile_from_login_password. +        GetIndividualProfilesRequest request = new GetIndividualProfilesRequest().withLoginPassword("user", "pass"); +        GetIndividualProfilesResponse response = client.getIndividualProfiles(request); + +        // Verify results. +        EasyMock.verify(mockHttpClient); +        assertNull(response.getErrors()); +        assertEquals(1, response.getIndividuals().size()); +        assertEquals(48, response.getIndividuals().get(0).getId()); +    } + +    @Test +    public void testGetIndividualProfilesByRoutingAndAccount() throws Exception { +        // Set expectation. +        URI expectedURI = new URI("https://localhost:8080/api.php?" +                + "srv=individual_profile_from_micr&account_number=4567&routing_number=1234"); +        InputStream is = getClass().getResourceAsStream("model/ccb_individual_profile_response.xml"); +        EasyMock.expect(mockHttpClient.sendPostRequest(expectedURI, Collections.<String, String>emptyMap())) +                .andReturn(is); +        EasyMock.replay(mockHttpClient); + +        // Test individual_profile_from_micr. +        GetIndividualProfilesRequest request = new GetIndividualProfilesRequest().withMICR("1234", "4567"); +        GetIndividualProfilesResponse response = client.getIndividualProfiles(request); + +        // Verify results. +        EasyMock.verify(mockHttpClient); +        assertNull(response.getErrors()); +        assertEquals(1, response.getIndividuals().size()); +        assertEquals(48, response.getIndividuals().get(0).getId()); +    } + +    @Test +    public void testGetAllIndividualProfiles() throws Exception { +        // Set expectation. +        URI expectedURI = new URI("https://localhost:8080/api.php?srv=individual_profiles"); +        InputStream is = getClass().getResourceAsStream("model/ccb_individual_profile_response.xml"); +        EasyMock.expect(mockHttpClient.sendPostRequest(expectedURI, Collections.<String, String>emptyMap())) +                .andReturn(is); +        EasyMock.replay(mockHttpClient); + +        // Test individual_profiles without any options. +        GetIndividualProfilesRequest request = new GetIndividualProfilesRequest(); +        GetIndividualProfilesResponse response = client.getIndividualProfiles(request); + +        // Verify results. +        EasyMock.verify(mockHttpClient); +        assertNull(response.getErrors()); +        assertEquals(1, response.getIndividuals().size()); +        assertEquals(48, response.getIndividuals().get(0).getId()); +    } + +    @Test +    public void testGetAllIndividualProfilesWithOptions() throws Exception { +        // Set expectation. +        URI expectedURI = new URI("https://localhost:8080/api.php?srv=individual_profiles" +                + "&per_page=15&include_inactive=true&modified_since=2016-03-19&page=5"); +        InputStream is = getClass().getResourceAsStream("model/ccb_individual_profile_response.xml"); +        EasyMock.expect(mockHttpClient.sendPostRequest(expectedURI, Collections.<String, String>emptyMap())) +                .andReturn(is); +        EasyMock.replay(mockHttpClient); + +        // Test individual_profiles with all the options. +        GetIndividualProfilesRequest request = new GetIndividualProfilesRequest() +                .withModifiedSince(LocalDate.parse("2016-03-19")) +                .withIncludeInactive(true) +                .withPage(5) +                .withPerPage(15); +        GetIndividualProfilesResponse response = client.getIndividualProfiles(request); + +        // Verify results. +        EasyMock.verify(mockHttpClient); +        assertNull(response.getErrors()); +        assertEquals(1, response.getIndividuals().size()); +        assertEquals(48, response.getIndividuals().get(0).getId()); +    } + +    /** +     * Simple extension of CCBAPIClient to swap out the HTTPInterface with a mock. +     */ +    private final class TestCCBAPIClient extends CCBAPIClient { +        public TestCCBAPIClient(final URI apiUri, final HTTPInterface mockHttpClient) { +            super(apiUri, mockHttpClient); +        } +    } +}
\ No newline at end of file diff --git a/src/test/java/com/p4square/ccbapi/CCBXmlBinderTest.java b/src/test/java/com/p4square/ccbapi/CCBXmlBinderTest.java new file mode 100644 index 0000000..184777a --- /dev/null +++ b/src/test/java/com/p4square/ccbapi/CCBXmlBinderTest.java @@ -0,0 +1,100 @@ +package com.p4square.ccbapi; + +import com.p4square.ccbapi.exception.CCBParseException; +import com.p4square.ccbapi.model.*; +import org.junit.Before; +import org.junit.Test; + +import java.io.InputStream; + +import static org.junit.Assert.*; + +/** + * Tests for the CCBXmlBinder. + */ +public class CCBXmlBinderTest { + +    private CCBXmlBinder binder; + +    @Before +    public void setUp() { +        binder = new CCBXmlBinder(); +    } + +    /** +     * Expect CCBXmlBinder to throw an exception if there is no response element. +     */ +    @Test(expected = CCBParseException.class) +    public void testMalformedNoResponseEntity() throws Exception { +        runTest("ccb_malformed_response_no_entity.xml", GetCustomFieldLabelsResponse.class); +    } + +    /** +     * Expect CCBXmlBinder to throw an exception if the XML is malformed. +     */ +    @Test(expected = CCBParseException.class) +    public void testMalformedXML() throws Exception { +        runTest("ccb_malformed_xml.xml", GetCustomFieldLabelsResponse.class); +    } + +    /** +     * Assert CCBXmlBinder correctly parses an error response. +     */ +    @Test +    public void testErrorResponse() throws Exception { +        CCBAPIResponse response = runTest("model/ccb_error_response.xml", GetCustomFieldLabelsResponse.class); + +        assertNotNull(response.getErrors()); +        assertEquals(1, response.getErrors().size()); + +        CCBErrorResponse error = response.getErrors().get(0); +        assertEquals(2, error.getNumber()); +        assertEquals("Service Permission", error.getType()); +        assertEquals("Invalid username or password.", error.getDescription()); +    } + +    /** +     * Assert CCBXmlBinder correctly parses a more elaborate response. +     */ +    @Test +    public void testGetCustomFieldLabelsResponse() throws Exception { +        GetCustomFieldLabelsResponse response = runTest("model/ccb_custom_field_labels_response.xml", +                                                        GetCustomFieldLabelsResponse.class); + +        assertNull("Response should not have errors", response.getErrors()); + +        assertNotNull(response.getCustomFields()); +        assertEquals(27, response.getCustomFields().size()); + +        CustomField field = response.getCustomFields().get(0); +        assertEquals("udf_ind_text_1", field.getName()); +        assertEquals("Favorite Movie", field.getLabel()); +        assertEquals(false, field.isAdminOnly()); + +        field = response.getCustomFields().get(1); +        assertEquals("udf_ind_text_2", field.getName()); +        assertEquals("Another Field", field.getLabel()); +        assertEquals(true, field.isAdminOnly()); +    } + +    /** +     * Helper to test the response stored in a file. +     * +     * @param filename The name of the xml file containing the response. +     * @param clazz The class to bind to. +     * @param <T> The type of the return value. +     * @return The parsed response. +     * @throws Exception If something fails. +     */ +    private <T extends CCBAPIResponse> T runTest(final String filename, final Class<T> clazz) throws Exception { +        InputStream in = getClass().getResourceAsStream(filename); +        if (in == null) { +            throw new AssertionError("Could not find file " + filename); +        } +        try { +            return binder.bindResponseXML(in, clazz); +        } finally { +            in.close(); +        } +    } +}
\ No newline at end of file diff --git a/src/test/java/com/p4square/ccbapi/model/GetCustomFieldLabelsResponseTest.java b/src/test/java/com/p4square/ccbapi/model/GetCustomFieldLabelsResponseTest.java new file mode 100644 index 0000000..549b8e9 --- /dev/null +++ b/src/test/java/com/p4square/ccbapi/model/GetCustomFieldLabelsResponseTest.java @@ -0,0 +1,39 @@ +package com.p4square.ccbapi.model; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests for parsing GetCustomFieldLabelsResponse. + */ +public class GetCustomFieldLabelsResponseTest extends XmlBinderTestBase { + +    /** +     * Assert that all of the fields bind appropriately in a GetCustomFieldLabelsResponse. +     */ +    @Test +    public void testGetCustomFieldLabelsResponse() throws Exception { +        final GetCustomFieldLabelsResponse response = parseFile("ccb_custom_labels_response.xml", +                GetCustomFieldLabelsResponse.class); + +        assertNull("Response should not have errors", response.getErrors()); + +        assertNotNull(response.getCustomFields()); +        assertEquals(27, response.getCustomFields().size()); + +        // Check the first field. +        CustomField field = response.getCustomFields().get(0); +        assertEquals("udf_ind_text_1", field.getName()); +        assertEquals("Favorite Movie", field.getLabel()); +        assertEquals(false, field.isAdminOnly()); + +        // And the second. +        field = response.getCustomFields().get(1); +        assertEquals("udf_ind_text_2", field.getName()); +        assertEquals("Another Field", field.getLabel()); +        assertEquals(true, field.isAdminOnly()); + +        // And that's probably enough for now... +    } +}
\ No newline at end of file diff --git a/src/test/java/com/p4square/ccbapi/model/GetIndividualProfilesResponseTest.java b/src/test/java/com/p4square/ccbapi/model/GetIndividualProfilesResponseTest.java new file mode 100644 index 0000000..743a9f7 --- /dev/null +++ b/src/test/java/com/p4square/ccbapi/model/GetIndividualProfilesResponseTest.java @@ -0,0 +1,60 @@ +package com.p4square.ccbapi.model; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Tests for parsing GetIndividualProfilesResponse. + */ +public class GetIndividualProfilesResponseTest extends XmlBinderTestBase { + +    /** +     * Assert that all of the fields bind appropriately for a single profile response. +     */ +    @Test +    public void testGetIndividualProfilesResponse() throws Exception { +        final GetIndividualProfilesResponse response = parseFile("ccb_individual_profile_response.xml", +                GetIndividualProfilesResponse.class); + +        assertNull("Response should not have errors", response.getErrors()); +        assertNotNull(response.getIndividuals()); +        assertEquals(1, response.getIndividuals().size()); + +        final IndividualProfile profile = response.getIndividuals().get(0); + +        // IDs +        assertEquals(48, profile.getId()); +        assertEquals(123, profile.getSyncId()); +        assertEquals(456, profile.getOtherId()); + +        // Family +        assertEquals(36, profile.getFamily().getFamilyId()); +        assertEquals("The Bob Family", profile.getFamily().getName()); +        assertEquals("https://cdn3.ccbchurch.com/preSTABLE/images/group-default.gif", profile.getFamilyImageUrl()); +        assertEquals(FamilyPosition.PRIMARY_CONTACT, profile.getFamilyPosition()); +        assertEquals(1, profile.getFamilyMembers().size()); + +        // Mrs. Bob +        assertEquals(49, profile.getFamilyMembers().get(0).getIndividualReference().getIndividualId()); +        assertEquals("Mrs. Bob", profile.getFamilyMembers().get(0).getIndividualReference().getName()); +        assertEquals(FamilyPosition.SPOUSE, profile.getFamilyMembers().get(0).getFamilyPosition()); + +        // Names +        assertEquals("Mr.", profile.getSalutation()); +        assertEquals("Larry", profile.getFirstName()); +        assertEquals("", profile.getMiddleName()); +        assertEquals("Bob", profile.getLastName()); +        assertEquals("", profile.getSuffix()); +        assertEquals("Larabar", profile.getLegalFirstName()); +        assertEquals("Larry Bob", profile.getFullName()); + +        // Other Attributes +        assertEquals("https://cdn3.ccbchurch.com/preSTABLE/images/profile-default.gif", profile.getImageUrl()); +        assertEquals("tsebastian@churchcommunitybuilder.com", profile.getEmail()); +        assertEquals("", profile.getAllergies()); +        assertEquals(true, profile.isConfirmedNoAllergies()); +        assertEquals(Gender.MALE, profile.getGender()); +        assertEquals("1990-04-05", profile.getBirthday().toString()); +    } +}
\ No newline at end of file diff --git a/src/test/java/com/p4square/ccbapi/model/XmlBinderTestBase.java b/src/test/java/com/p4square/ccbapi/model/XmlBinderTestBase.java new file mode 100644 index 0000000..2422d39 --- /dev/null +++ b/src/test/java/com/p4square/ccbapi/model/XmlBinderTestBase.java @@ -0,0 +1,39 @@ +package com.p4square.ccbapi.model; + +import java.io.InputStream; +import com.p4square.ccbapi.CCBXmlBinder; +import org.junit.Before; + +/** + * Created by jesterpm on 3/14/16. + */ +public class XmlBinderTestBase { + +    private CCBXmlBinder binder; + +    @Before +    public void setUp() { +        binder = new CCBXmlBinder(); +    } + +    /** +     * Helper to test the response stored in a file. +     * +     * @param filename The name of the xml file containing the response. +     * @param clazz The class to bind to. +     * @param <T> The type of the return value. +     * @return The parsed response. +     * @throws Exception If something fails. +     */ +    protected <T extends CCBAPIResponse> T parseFile(final String filename, final Class<T> clazz) throws Exception { +        InputStream in = getClass().getResourceAsStream(filename); +        if (in == null) { +            throw new AssertionError("Could not find file " + filename); +        } +        try { +            return binder.bindResponseXML(in, clazz); +        } finally { +            in.close(); +        } +    } +} diff --git a/src/test/resources/com/p4square/ccbapi/ccb_malformed_response_no_entity.xml b/src/test/resources/com/p4square/ccbapi/ccb_malformed_response_no_entity.xml new file mode 100644 index 0000000..30644eb --- /dev/null +++ b/src/test/resources/com/p4square/ccbapi/ccb_malformed_response_no_entity.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ccb_api> +    <request> +        <parameters> +            <argument value="custom_field_labels" name="srv"/> +        </parameters> +    </request> +</ccb_api> diff --git a/src/test/resources/com/p4square/ccbapi/ccb_malformed_xml.xml b/src/test/resources/com/p4square/ccbapi/ccb_malformed_xml.xml new file mode 100644 index 0000000..97b109f --- /dev/null +++ b/src/test/resources/com/p4square/ccbapi/ccb_malformed_xml.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ccb_api> +    <request> +        <parameters> +            <argument value="custom_field_labels" name="srv"/> +        </parameters> +    </request> +    <response> + diff --git a/src/test/resources/com/p4square/ccbapi/model/ccb_custom_field_labels_response.xml b/src/test/resources/com/p4square/ccbapi/model/ccb_custom_field_labels_response.xml new file mode 100644 index 0000000..0fcc709 --- /dev/null +++ b/src/test/resources/com/p4square/ccbapi/model/ccb_custom_field_labels_response.xml @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ccb_api> +     <request> +          <parameters> +               <argument value="custom_field_labels" name="srv"/> +          </parameters> +     </request> +     <response> +          <service>custom_field_labels</service> +          <service_action>execute</service_action> +          <availability>public</availability> +          <custom_fields count="28"> +               <custom_field> +                    <name>udf_ind_text_1</name> +                    <label>Favorite Movie</label> +                    <admin_only>false</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_2</name> +                    <label>Another Field</label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_3</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_4</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_5</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_6</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_7</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_8</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_9</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_10</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_11</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_text_12</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_date_1</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_date_2</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_date_3</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_date_4</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_date_5</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_date_6</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_pulldown_1</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_pulldown_2</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_pulldown_3</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_pulldown_4</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_pulldown_5</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_ind_pulldown_6</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_grp_pulldown_1</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_grp_pulldown_2</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +               <custom_field> +                    <name>udf_grp_pulldown_3</name> +                    <label></label> +                    <admin_only>true</admin_only> +               </custom_field> +          </custom_fields> +     </response> +</ccb_api>
\ No newline at end of file diff --git a/src/test/resources/com/p4square/ccbapi/model/ccb_error_response.xml b/src/test/resources/com/p4square/ccbapi/model/ccb_error_response.xml new file mode 100644 index 0000000..ced57fa --- /dev/null +++ b/src/test/resources/com/p4square/ccbapi/model/ccb_error_response.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ccb_api> +     <request> +          <parameters> +               <argument value="custom_field_labels" name="srv"/> +          </parameters> +     </request> +     <response> +          <errors> +               <error number="002" type="Service Permission">Invalid username or password.</error> +          </errors> +     </response> +</ccb_api> diff --git a/src/test/resources/com/p4square/ccbapi/model/ccb_individual_profile_response.xml b/src/test/resources/com/p4square/ccbapi/model/ccb_individual_profile_response.xml new file mode 100644 index 0000000..fd3d0f7 --- /dev/null +++ b/src/test/resources/com/p4square/ccbapi/model/ccb_individual_profile_response.xml @@ -0,0 +1,149 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ccb_api> +    <request> +        <parameters> +            <argument value="individual_profile_from_id" name="srv"/> +            <argument value="48" name="individual_id"/> +        </parameters> +    </request> +    <response> +        <service>individual_profile_from_id</service> +        <service_action>execute</service_action> +        <availability>public</availability> +        <individuals count="1"> +            <individual id="48"> +                <sync_id>123</sync_id> +                <other_id>456</other_id> +                <giving_number>ABC123</giving_number> +                <campus id="1">Church of Cucumbers</campus> +                <family id="36">The Bob Family</family> +                <family_image>https://cdn3.ccbchurch.com/preSTABLE/images/group-default.gif</family_image> +                <family_position>Primary Contact</family_position> +                <family_members> +                     <family_member> +                          <individual id="49">Mrs. Bob</individual> +                          <family_position>Spouse</family_position> +                     </family_member> +                </family_members> +                <first_name>Larry</first_name> +                <last_name>Bob</last_name> +                <middle_name></middle_name> +                <legal_first_name>Larabar</legal_first_name> +                <full_name>Larry Bob</full_name> +                <salutation>Mr.</salutation> +                <suffix></suffix> +                <image>https://cdn3.ccbchurch.com/preSTABLE/images/profile-default.gif</image> +                <email>tsebastian@churchcommunitybuilder.com</email> +                <allergies></allergies> +                <confirmed_no_allergies>true</confirmed_no_allergies> +                <addresses> +                    <address type="mailing"> +                        <street_address>1234 Village St.</street_address> +                        <city>Denver</city> +                        <state>CO</state> +                        <zip>12345</zip> +                        <country code="US">United States</country> +                        <line_1>1234 Village St.</line_1> +                        <line_2>Denver, CO 12345</line_2> +                        <latitude></latitude> +                        <longitude></longitude> +                    </address> +                    <address type="home"> +                        <street_address></street_address> +                        <city></city> +                        <state></state> +                        <zip></zip> +                        <country code=""> </country> +                        <line_1></line_1> +                        <line_2></line_2> +                    </address> +                    <address type="work"> +                        <street_address></street_address> +                        <city></city> +                        <state></state> +                        <zip></zip> +                        <country code=""> </country> +                        <line_1></line_1> +                        <line_2></line_2> +                    </address> +                    <address type="other"> +                        <street_address></street_address> +                        <city></city> +                        <state></state> +                        <zip></zip> +                        <country code=""> </country> +                        <line_1></line_1> +                        <line_2></line_2> +                    </address> +                </addresses> +                <phones> +                    <phone type="contact">(098) 765-4321</phone> +                    <phone type="home"></phone> +                    <phone type="work"></phone> +                    <phone type="mobile"></phone> +                </phones> +                <mobile_carrier id="0"> </mobile_carrier> +                <gender>M</gender> +                <marital_status></marital_status> +                <birthday>1990-04-05</birthday> +                <anniversary></anniversary> +                <baptized>false</baptized> +                <deceased></deceased> +                <membership_type id=""> </membership_type> +                <membership_date></membership_date> +                <membership_end></membership_end> +                <receive_email_from_church>true</receive_email_from_church> +                <default_new_group_messages>Group Default</default_new_group_messages> +                <default_new_group_comments>Group Default</default_new_group_comments> +                <default_new_group_digest>Group Default</default_new_group_digest> +                <default_new_group_sms>Never</default_new_group_sms> +                <privacy_settings> +                    <profile_listed>true</profile_listed> +                    <mailing_address id="2">My Friends</mailing_address> +                    <home_address id="2">My Friends</home_address> +                    <contact_phone id="3">My Friends and Group Members</contact_phone> +                    <home_phone id="3">My Friends and Group Members</home_phone> +                    <work_phone id="2">My Friends</work_phone> +                    <mobile_phone id="2">My Friends</mobile_phone> +                    <emergency_phone id="2">My Friends</emergency_phone> +                    <birthday id="4">Everyone</birthday> +                    <anniversary id="4">Everyone</anniversary> +                    <gender id="4">Everyone</gender> +                    <marital_status id="2">My Friends</marital_status> +                    <user_defined_fields id="2">My Friends</user_defined_fields> +                    <allergies id="1">Church Leadership</allergies> +                </privacy_settings> +                <active>true</active> +                <creator id="1">Larry Cucumber</creator> +                <modifier id="1">Larry Cucumber</modifier> +                <created>2012-09-25 15:29:15</created> +                <modified>2012-09-25 15:30:50</modified> +                <user_defined_text_fields> +                     <user_defined_text_field> +                          <name>udf_text_12</name> +                          <label>Test</label> +                          <text>Test Value</text> +                          <admin_only>false</admin_only> +                     </user_defined_text_field> +                </user_defined_text_fields> +                <user_defined_date_fields> +                     <user_defined_date_field> +                          <name>udf_date_6</name> +                          <label>Test Date</label> +                          <date>2016-03-16</date> +                          <admin_only>false</admin_only> +                     </user_defined_date_field> +                </user_defined_date_fields> +                <user_defined_pulldown_fields> +                     <user_defined_pulldown_field> +                          <name>udf_pulldown_6</name> +                          <label>Level</label> +                          <selection id="3">Disciple</selection> +                          <admin_only>false</admin_only> +                     </user_defined_pulldown_field> +                </user_defined_pulldown_fields> + +            </individual> +        </individuals> +    </response> +</ccb_api>
\ No newline at end of file | 
