Skip to content

Commit

Permalink
Initial commit of work from 2012 NPSP Sprint
Browse files Browse the repository at this point in the history
  • Loading branch information
Evan Callahan committed May 22, 2012
1 parent 634722d commit 564c77d
Show file tree
Hide file tree
Showing 41 changed files with 3,026 additions and 0 deletions.
540 changes: 540 additions & 0 deletions src/classes/Cicero.cls

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/classes/Cicero.cls-meta.xml
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>25.0</apiVersion>
<status>Active</status>
</ApexClass>
268 changes: 268 additions & 0 deletions src/classes/CiceroTest.cls

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions src/classes/CiceroTest.cls-meta.xml
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>25.0</apiVersion>
<status>Active</status>
</ApexClass>
156 changes: 156 additions & 0 deletions src/classes/GeocoderUsService.cls
@@ -0,0 +1,156 @@
public with sharing class GeocoderUsService extends GeocodingService {
/*-----------------------------------------------------------------------------------------------
* interface to geocoder.us web service
* give it any address, get back latitude, longitude, and a parsed address
*
* if you do not provide a username and password for the geocoder service, the class uses the
* free version which is for non-commercial use only. please note that you can only use the free
* version once every 15 seconds from a given IP address
*
* @author Evan Callahan, Todd Reasinger
-----------------------------------------------------------------------------------------------*/

private static final Integer DEFAULT_TIMEOUT = 2000;

// endpoints
private static final String publicEndpoint = 'rpc.geocoder.us/service/csv';
private static final String authEndpoint = 'geocoder.us/member/service/csv/geocode';

protected String username;
protected String password;
public Integer timeout = DEFAULT_TIMEOUT;

// properties
public string response { get; private set; }
public string error { get; private set; }
public List<Geo_Data__c> locations { get; private set; }

// need to special case the test
final string testResp = '47.618967,-122.348993,123 4th Ave N,Seattle,WA,98109';

// track this so we don't call the services once we are shut out
boolean outOfGeocoderRequests = false;

// constructors
public GeocoderUsService() {
}

public override Geo_Data__c[] getGeodata(String address) {

// if we do not already have credentials, query for them
if (credentials == null) {
geoDataCredentials__c[] providers =
[SELECT API_Key__c, Name, Password__c, Request_Timeout__c, User_Id__c, endpoint__c
FROM geoDataCredentials__c
WHERE name like 'GeocoderUs%' and IsActive__c = true
ORDER BY priority__c LIMIT 1];
if (!providers.isEmpty())
credentials = providers[0];
}

if (credentials != null) {
username = credentials.user_id__c;
password = credentials.password__c;
if (credentials.request_timeout__c != null)
timeout = (Integer)credentials.Request_Timeout__c;
}

// initialize
locations = new List<Geo_Data__c>();
response = error = null;

if (address != null && address.trim() != '' && !outOfGeocoderRequests) {
String configuredEndpoint = (credentials != null && credentials.endpoint__c != null) ? credentials.endpoint__c : (username == null ? authEndpoint : publicEndpoint);
String endpoint = ((username != null && password != null) ?
(EncodingUtil.URLEncode(username, 'UTF-8') + ':' +
EncodingUtil.URLEncode(password, 'UTF-8') + '@' ) : '')
+ configuredEndpoint;

Http h = new Http();
HttpRequest req = new HttpRequest();
req.setEndpoint('http://' + endpoint + '?address=' + EncodingUtil.URLEncode(address, 'UTF-8'));
req.setMethod('GET');
req.setTimeout(timeout);

try {
HttpResponse resp;
Integer status;
if (Test.isRunningTest()) {
response = testResp;
status = 200;
} else {
resp = h.send(req);
response = resp.getBody();
status = resp.getStatusCode();
}
if (status == 200) {
if (response != null) {
for (string addr : response.split('\n')) {
string[] parsed = addr.split(',');
if (parsed.size() == 6) {

Geo_Data__c gd = new Geo_Data__c();
gd.geoLat__c = Decimal.valueOf(parsed[0]);
gd.geoLong__c = Decimal.valueOf(parsed[1]);
gd.Street__c = parsed[2];
gd.City__c = parsed[3];
gd.State__c = parsed[4];
gd.Zip_Postal_Code__c = parsed[5];
locations.add(gd);

}
}
if (locations.isEmpty())
error = 'Response from geocoding service: ' + response;
} else {
error = 'No response from geocoding service.';
}
} else {
error = 'Unexpected response from geocoding service (STATUS ' + string.valueOf(status) + '): \n' + response;
}

} catch( System.Exception e) {
if (e.getMessage().startsWith('Unauthorized endpoint')) {
error = 'Before using the geocoder.us service, an administrator must go to Setup => Security => Remote Site Settings ' +
'and add the following endpoint: http://' + ((username != null) ? authEndpoint : publicEndpoint);
} else {
error = 'Error communicating with geocoding service: ' + e.getMessage();
outOfGeocoderRequests = (error.contains('Read timed out'));
}
} finally {
if (error != null)
system.debug(loggingLevel.ERROR, error);
}
}

return locations;
}

public override Boolean providesDatatype(PROVIDER_DATA_TYPES datatype) {
return (datatype == PROVIDER_DATA_TYPES.GEOCODING);
}

public override PROVIDER_DATA_TYPES[] getAvailableDatatypes() {
return new PROVIDER_DATA_TYPES[]{ PROVIDER_DATA_TYPES.GEOCODING };
}

public override Geo_Data__c getGeodata(Decimal lat, Decimal lng) {
return null;
}

// for public version, you can make calls only once every 15 seconds
public override integer getMaxCallsForBatch() { return (username != null && password != null) ? 9 : 1; }
public override integer getMinDelay() { return (username != null && password != null) ? null : 15; }

public override integer getAvailableCallouts() { return null; } // no way to know the remaining credits

public static testMethod void testGeocoder() {
insert new geoDataCredentials__c(name = 'GeocoderUsService', User_Id__c = 'test', priority__c = 1, IsActive__c = true);

GeocoderUsService gc = new GeocoderUsService();
gc.getGeodata('123 4th Ave, Seattle, WA');
system.debug(loggingLevel.WARN, gc.locations);
system.assertEquals(47.618967, gc.locations[0].geoLat__c);
}

}
5 changes: 5 additions & 0 deletions src/classes/GeocoderUsService.cls-meta.xml
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>24.0</apiVersion>
<status>Active</status>
</ApexClass>
173 changes: 173 additions & 0 deletions src/classes/GeocodingService.cls
@@ -0,0 +1,173 @@
public with sharing abstract class GeocodingService {
/*-----------------------------------------------------------------------------------------------
* abstract interface to geodata services
* each implementation implements methods to get geodata from an address or lat/long pair
* credentials for services, and priority, are stored in custom setting
*
* @author Evan Callahan, Todd Reasinger
-----------------------------------------------------------------------------------------------*/

public enum PROVIDER_DATA_TYPES {GEOCODING, REVERSE_GEOCODING, NATIONAL_LEGISLATIVE, STATE_LEGISLATIVE,
COUNTY_NAME, NEIGHBORHOOD, WATERSHED, SCHOOL_DISTRICT, CENSUS_DISTRICT}

protected geoDataCredentials__c credentials;
protected PROVIDER_DATA_TYPES[] requestedDatatypes;

public class GeocodingException extends Exception {}

public static GeocodingService getProvider(PROVIDER_DATA_TYPES[] dataTypes) {

GeocodingService service = null;
geoDataCredentials__c[] providers =
[SELECT API_Key__c, Name, Password__c, Request_Timeout__c, User_Id__c, endpoint__c
FROM geoDataCredentials__c
WHERE IsActive__c = true
ORDER BY priority__c];
System.debug(LoggingLevel.ERROR, providers);

for (geoDataCredentials__c c : providers) {
System.debug(LoggingLevel.WARN, 'Getting service for ' + c.name);
Type t = Type.forName(c.name);
if (t != null) service = (GeocodingService) t.newInstance();

if (t == null || service == null) {
System.debug(LoggingLevel.ERROR, 'Error trying to create geocoding service for ' + c.name);
}

if (service != null && dataTypes != null) {
// use this service ONLY if it provides ALL requested data types
for (PROVIDER_DATA_TYPES pdt : dataTypes) {
System.debug(LoggingLevel.WARN, 'does it provide ' + pdt);
if (!service.providesDatatype(pdt)) {
System.debug(LoggingLevel.WARN, 'no');
service = null;
continue;
}
}
}
if (service != null) {
service.credentials = c;
service.requestedDatatypes = dataTypes;
break;
}
}
return service;
}

protected GeocodingService() {}

/*
* Methods that must be implemented by any provider.
*/
public abstract Boolean providesDatatype(PROVIDER_DATA_TYPES datatype);
public abstract PROVIDER_DATA_TYPES[] getAvailableDatatypes();
public abstract Geo_Data__c[] getGeodata(String addr);
public abstract Geo_Data__c getGeodata(Decimal lat, Decimal lng);
public abstract integer getMaxCallsForBatch(); // this tells us whether we can do 1 per apex transaction or up to 10
public abstract integer getMinDelay(); // this tells us the number of seconds we need to wait before making more calls
public abstract integer getAvailableCallouts(); // returns the total number of callout "credits," or null if unlimited

/*
* Utility method that combines data into a single geodata.
*/
public void mergeData(Geo_data__c masterData, Geo_data__c mergeData) {
if (masterData.geoDataTopCandidate__c == null) masterData.geoDataTopCandidate__c = mergeData.geoDataTopCandidate__c;
if (masterData.Contact__c == null) masterData.Contact__c = mergeData.Contact__c;
if (masterData.geoAddressComplete__c == null) masterData.geoAddressComplete__c = mergeData.geoAddressComplete__c;
if (masterData.geoAddressScorePercent__c == null) masterData.geoAddressScorePercent__c = mergeData.geoAddressScorePercent__c;
if (masterData.geoLat__c == null) masterData.geoLat__c = mergeData.geoLat__c;
if (masterData.geoLong__c == null) masterData.geoLong__c = mergeData.geoLong__c;
if (masterData.Street__c == null) masterData.Street__c = mergeData.Street__c;
if (masterData.City__c == null) masterData.City__c = mergeData.City__c;
if (masterData.State__c == null) masterData.State__c = mergeData.State__c;
if (masterData.County__c == null) masterData.County__c = mergeData.County__c;
if (masterData.Country_Short_Code__c == null) masterData.Country_Short_Code__c = mergeData.Country_Short_Code__c;
if (masterData.Country_Long_Name__c == null) masterData.Country_Long_Name__c = mergeData.Country_Long_Name__c;
if (masterData.Zip_Postal_Code__c == null) masterData.Zip_Postal_Code__c = mergeData.Zip_Postal_Code__c;
if (masterData.Neighborhood__c == null) masterData.Neighborhood__c = mergeData.Neighborhood__c;
if (masterData.Federal_District__c == null) masterData.Federal_District__c = mergeData.Federal_District__c;
if (masterData.Federal_Display_Name__c == null) masterData.Federal_Display_Name__c = mergeData.Federal_Display_Name__c;
if (masterData.Upper_District__c == null) masterData.Upper_District__c = mergeData.Upper_District__c;
if (masterData.Lower_District__c == null) masterData.Lower_District__c = mergeData.Lower_District__c;
if (masterData.Upper_Display_Name__c == null) masterData.Upper_Display_Name__c = mergeData.Upper_Display_Name__c;
if (masterData.Lower_Display_Name__c == null) masterData.Lower_Display_Name__c = mergeData.Lower_Display_Name__c;
}

public static String packAddress(String street, String city, String state, String postalCode) {
String addr = isNotEmpty(street) ? street : '';
addr += isNotEmpty(city) ? (isNotEmpty(addr) ? (', ' + city) : city) : '';
addr += isNotEmpty(state) ? (isNotEmpty(addr) ? (', ' + state) : state) : '';
//addr += isNotEmpty(postalCode) ? (isNotEmpty(addr) ? (', ' + postalCode) : postalCode) : '';
addr += isNotEmpty(postalCode) ? (isNotEmpty(addr) ? (' ' + postalCode) : postalCode) : '';
return addr;
}

private static Boolean isNotEmpty(String s) {
return (s != null && s != '');
}

/*
* Implementation used for testing.
*/
private class TestGeocodingService extends GeocodingService {
private PROVIDER_DATA_TYPES[] requestedDatatypes;

public override Boolean providesDatatype(PROVIDER_DATA_TYPES datatype) {
return datatype == PROVIDER_DATA_TYPES.GEOCODING;
}

public override PROVIDER_DATA_TYPES[] getAvailableDatatypes() {
return new PROVIDER_DATA_TYPES[]{ PROVIDER_DATA_TYPES.GEOCODING };
}

public override Geo_Data__c[] getGeodata(String addr) {
Geo_Data__c gd = new Geo_data__c();
gd.geoAddressComplete__c = addr;
return new Geo_Data__c[]{gd};
}

public override Geo_Data__c getGeodata(Decimal lat, Decimal lng) {
Geo_Data__c gd = new Geo_data__c();
gd.geoLat__c = lat;
gd.geoLong__c = lng;
return gd;
}

public override integer getMaxCallsForBatch() { return 9; }
public override integer getMinDelay() { return null; }
public override integer getAvailableCallouts() { return null; }
}

private static final String TEST_ADDRESS = '123 Main Street, Pittsburgh, PA';
public testMethod static void testGeocodingService() {

geoDataCredentials__c invalidCreds = new geoDataCredentials__c();
invalidCreds.Name = 'noClass';
invalidCreds.IsActive__c = true;
invalidCreds.Priority__c = 1;
insert invalidCreds;

geoDataCredentials__c creds = new geoDataCredentials__c();
creds.Name = TestGeocodingService.class.getName();
creds.IsActive__c = true;
creds.Priority__c = 2;
insert creds;

GeocodingService s = getProvider( new PROVIDER_DATA_TYPES[]{ PROVIDER_DATA_TYPES.NATIONAL_LEGISLATIVE });
System.assertEquals(null, s);
s = getProvider(new PROVIDER_DATA_TYPES[]{ PROVIDER_DATA_TYPES.GEOCODING });
System.assertNotEquals(null, s);
Geo_data__c[] data = s.getGeodata(TEST_ADDRESS);
System.assert(data != null && data.size() == 1);
System.assertEquals(TEST_ADDRESS, data[0].geoAddressComplete__c);
Geo_data__c oneData = s.getGeodata(1.01, -3.45);
System.assert(oneData != null);
}

static testMethod void testPackAddress() {
System.assertEquals('123 Main, San Francisco, CA', packAddress('123 Main', 'San Francisco', 'CA', null));
System.assertEquals('San Francisco, CA', packAddress(null, 'San Francisco', 'CA', ''));
System.assertEquals('94105', packAddress(null, '', null, '94105'));
}

}
5 changes: 5 additions & 0 deletions src/classes/GeocodingService.cls-meta.xml
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>25.0</apiVersion>
<status>Active</status>
</ApexClass>

0 comments on commit 564c77d

Please sign in to comment.