diff options
Diffstat (limited to 'src')
43 files changed, 2804 insertions, 0 deletions
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 |