/** * ============================================================================= * * ORCID (R) Open Source * http://orcid.org * * Copyright (c) 2012-2014 ORCID, Inc. * Licensed under an MIT-Style License (MIT) * http://orcid.org/open-source-license * * This copyright and license information (including a link to the full license) * shall be included in its entirety in all copies or substantial portion of * the software. * * ============================================================================= */ package org.orcid.core.salesforce.dao.impl; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.Resource; import javax.ws.rs.core.MediaType; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; import org.orcid.core.exception.SalesForceUnauthorizedException; import org.orcid.core.salesforce.adapter.SalesForceAdapter; import org.orcid.core.salesforce.dao.SalesForceDao; import org.orcid.core.salesforce.model.Consortium; import org.orcid.core.salesforce.model.Contact; import org.orcid.core.salesforce.model.ContactRole; import org.orcid.core.salesforce.model.Integration; import org.orcid.core.salesforce.model.Member; import org.orcid.core.salesforce.model.MemberDetails; import org.orcid.core.salesforce.model.Opportunity; import org.orcid.core.salesforce.model.SlugUtils; import org.orcid.core.utils.JsonUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Value; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.UniformInterfaceException; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.filter.LoggingFilter; import com.sun.jersey.api.representation.Form; public class SalesForceDaoImpl implements SalesForceDao, InitializingBean { private static final Logger LOGGER = LoggerFactory.getLogger(SalesForceDaoImpl.class); @Value("${org.orcid.core.salesForce.clientId}") private String clientId; @Value("${org.orcid.core.salesForce.clientSecret}") private String clientSecret; @Value("${org.orcid.core.salesForce.username}") private String username; @Value("${org.orcid.core.salesForce.password}") private String password; @Value("${org.orcid.core.salesForce.tokenEndPointUrl:https://test.salesforce.com/services/oauth2/token}") private String tokenEndPointUrl; @Value("${org.orcid.core.salesForce.apiBaseUrl:https://cs10.salesforce.com}") private String apiBaseUrl; @Value("${org.orcid.core.salesForce.clientLoggingEnabled:false}") private boolean clientLoggingEnabled; @Resource private SalesForceAdapter salesForceAdapter; private Client client; private String accessToken; @Override public List<Member> retrieveConsortia() { return retry(accessToken -> retrieveConsortiaFromSalesForce(accessToken)); } @Override public List<Member> retrieveMembers() { return retry(accessToken -> retrieveMembersFromSalesForce(accessToken)); } @Override public List<Member> retrieveMembersByWebsite(String websiteUrl) { return retry(accessToken -> retrieveMembersByWebsiteFromSalesForce(accessToken, websiteUrl)); } @Override public Consortium retrieveConsortium(String consortiumId) { return retry(accessToken -> retrieveConsortiumFromSalesForce(accessToken, consortiumId)); } @Override public MemberDetails retrieveDetails(String memberId, String consortiumLeadId) { validateSalesForceId(memberId); if (consortiumLeadId != null) { validateSalesForceId(consortiumLeadId); } return retry(accessToken -> retrieveDetailsFromSalesForce(accessToken, memberId, consortiumLeadId)); } @Override public List<Contact> retrieveAllContactsByAccountId(String accountId) { return retry(accessToken -> retrieveAllContactsFromSalesForceByAccountId(accessToken, accountId)); } @Override public List<Contact> retrieveContactsWithRolesByAccountId(String accountId) { return retry(accessToken -> retrieveContactsWithRolesFromSalesForceByAccountId(accessToken, accountId)); } @Override public List<ContactRole> retrieveContactRolesByContactIdAndAccountId(String contactId, String accountId) { return retry(accessToken -> retrieveContactRolesFromSalesForceByContactIdAndAccountId(accessToken, contactId, accountId)); } @Override public String retrievePremiumConsortiumMemberTypeId() { return retry(accessToken -> retrievePremiumConsortiumMemberTypeIdFromSalesForce(accessToken)); } @Override public String retrieveConsortiumMemberRecordTypeId() { return retry(accessToken -> retrieveConsortiumMemberRecordTypeIdFromSalesForce(accessToken)); } @Override public String createContact(Contact contact) { return retry(accessToken -> createContactInSalesForce(accessToken, contact)); } @Override public String createContactRole(ContactRole contact) { return retry(accessToken -> createContactRoleInSalesForce(accessToken, contact)); } @Override public void removeContactRole(String contactRoleId) { retryConsumer(accessToken -> removeContactRoleInSalesForce(accessToken, contactRoleId)); } @Override public String createMember(Member member) { return retry(accessToken -> createMemberInSalesForce(accessToken, member)); } @Override public void updateMember(Member member) { retryConsumer(accessToken -> updateMemberInSalesForce(accessToken, member)); } @Override public String createOpportunity(Opportunity opportunity) { return retry(accessToken -> createOpportunityInSalesForce(accessToken, opportunity)); } @Override public void updateOpportunity(Opportunity opportunity) { retryConsumer(accessToken -> updateOpportunityInSalesForce(accessToken, opportunity)); } @Override public String getAccessToken() { if (accessToken == null) { accessToken = getFreshAccessToken(); } return accessToken; } @Override public String validateSalesForceId(String salesForceId) { if (!salesForceId.matches("[a-zA-Z0-9]+")) { // Could be malicious, so give no further info. throw new IllegalArgumentException(); } return salesForceId; } private String escapeStringInput(String input) { if (input == null) { return null; } return input.replace("'", "\\'"); } private String createContactInSalesForce(String accessToken, Contact contact) { LOGGER.info("About to create contact in SalesForce"); String accountId = contact.getAccountId(); validateSalesForceId(accountId); WebResource resource = createObjectsResource("/Contact/"); JSONObject contactJson = salesForceAdapter.createSaleForceRecordFromContact(contact); ClientResponse response = doPostRequest(resource, contactJson, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 201, "Error creating contact in SalesForce"); return result.optString("id"); } private String createContactRoleInSalesForce(String accessToken, ContactRole contactRole) { LOGGER.info("About to create contact role in SalesForce"); validateSalesForceId(contactRole.getAccountId()); validateSalesForceId(contactRole.getContactId()); WebResource resource = createObjectsResource("/Membership_Contact_Role__c/"); JSONObject contactJson = salesForceAdapter.createSaleForceRecordFromContactRole(contactRole); ClientResponse response = doPostRequest(resource, contactJson, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 201, "Error creating contact role in SalesForce"); return result.optString("id"); } private void removeContactRoleInSalesForce(String accessToken, String contactRoleId) { LOGGER.info("About to remove contact role in SalesForce"); validateSalesForceId(contactRoleId); WebResource resource = createObjectsResource("/Membership_Contact_Role__c/", contactRoleId); ClientResponse response = doDeleteRequest(resource, accessToken); checkAuthorization(response); checkResponse(response, 204, "Error removing contact role in SalesForce"); } private String createMemberInSalesForce(String accessToken, Member member) { LOGGER.info("About to create member in SalesForce"); WebResource resource = createObjectsResource("/Account/"); JSONObject memberJson = salesForceAdapter.createSaleForceRecordFromMember(member); ClientResponse response = doPostRequest(resource, memberJson, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 201, "Error creating member in SalesForce"); return result.optString("id"); } private void updateMemberInSalesForce(String accessToken, Member member) { LOGGER.info("About update member in SalesForce"); String accountId = member.getId(); validateSalesForceId(accountId); WebResource resource = createObjectsResource("/Account/", accountId).queryParam("_HttpMethod", "PATCH"); JSONObject memberJson = salesForceAdapter.createSaleForceRecordFromMember(member); // SalesForce doesn't allow the Id in the body memberJson.remove("Id"); ClientResponse response = doPostRequest(resource, memberJson, accessToken); checkAuthorization(response); checkResponse(response, 204, "Error updating member in SalesForce"); return; } private String createOpportunityInSalesForce(String accessToken, Opportunity opportunity) { LOGGER.info("About to create opportunity in SalesForce"); WebResource resource = createObjectsResource("/Opportunity/"); JSONObject opportunityJson = salesForceAdapter.createSaleForceRecordFromOpportunity(opportunity); ClientResponse response = doPostRequest(resource, opportunityJson, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 201, "Error creating opportunity in SalesForce"); return result.optString("id"); } private void updateOpportunityInSalesForce(String accessToken, Opportunity opportunity) { LOGGER.info("About to flag opportunity as closed in SalesForce"); WebResource resource = createObjectsResource("/Opportunity/", opportunity.getId()).queryParam("_HttpMethod", "PATCH"); JSONObject memberJson = salesForceAdapter.createSaleForceRecordFromOpportunity(opportunity); // SalesForce doesn't allow the Id in the body memberJson.remove("Id"); ClientResponse response = doPostRequest(resource, memberJson, accessToken); checkAuthorization(response); checkResponse(response, 204, "Error updating opportunity in SalesForce"); } private String validateSalesForceIdsAndConcatenate(Collection<String> salesForceIds) { salesForceIds.stream().forEach(e -> validateSalesForceId(e)); return "'" + String.join("','", salesForceIds) + "'"; } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private List<Member> retrieveMembersFromSalesForce(String accessToken) throws SalesForceUnauthorizedException { LOGGER.info("About get list of members from SalesForce"); List<Member> membersList = new ArrayList<>(); JSONObject jsonObject = retrieveMembersObject(accessToken); String nextRecordsUrl = null; do { membersList.addAll(salesForceAdapter.createMembersListFromJson(jsonObject)); nextRecordsUrl = JsonUtils.extractString(jsonObject, "nextRecordsUrl"); if (nextRecordsUrl != null) { jsonObject = retrieveMembersNextObject(accessToken, nextRecordsUrl); } } while (nextRecordsUrl != null); return membersList; } private JSONObject retrieveMembersObject(String accessToken) { WebResource resource = createQueryResource( "SELECT Account.Id, Account.Name, Account.Public_Display_Name__c, Account.Website, Account.BillingCountry, Account.Research_Community__c, (SELECT Consortia_Lead__c from Opportunities WHERE IsClosed=TRUE AND IsWon=TRUE AND Membership_End_Date__c>TODAY ORDER BY Membership_Start_Date__c DESC), Account.Public_Display_Description__c, Account.Logo_Description__c, Account.Public_Display_Email__c from Account WHERE Active_Member__c=TRUE"); ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); return checkResponse(response, 200, "Error getting member list from SalesForce"); } private JSONObject retrieveMembersNextObject(String accessToken, String nextRecordsUrl) { WebResource nextResource = creatNextRecordsResource(nextRecordsUrl); ClientResponse nextResponse = doGetRequest(nextResource, accessToken); return checkResponse(nextResponse, 200, "Error getting next results for member list from SalesForce"); } private WebResource creatNextRecordsResource(String nextRecordsUrl) { WebResource resource = client.resource(apiBaseUrl).path(nextRecordsUrl); return resource; } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private List<Member> retrieveMembersByWebsiteFromSalesForce(String accessToken, String websiteUrl) throws SalesForceUnauthorizedException { LOGGER.info("About get list of members from SalesForce by website"); WebResource resource = createQueryResource( String.format("SELECT Account.Id, Account.Website from Account WHERE Active_Member__c=TRUE and Account.Website = '%s'", escapeStringInput(websiteUrl))); ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting members by website from SalesForce"); return salesForceAdapter.createMembersListFromJson(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private List<Member> retrieveConsortiaFromSalesForce(String accessToken) throws SalesForceUnauthorizedException { LOGGER.info("About get list of consortia from SalesForce"); WebResource resource = createQueryResource( "SELECT Id, Name, Public_Display_Name__c, Website, Research_Community__c, BillingCountry, Public_Display_Description__c, Logo_Description__c, (SELECT Opportunity.Id FROM Opportunities WHERE IsClosed=TRUE AND IsWon=TRUE AND Membership_End_Date__c>TODAY ORDER BY Membership_Start_Date__c DESC) from Account WHERE Id IN (SELECT Consortia_Lead__c FROM Opportunity) AND Active_Member__c=TRUE"); ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting consortia list from SalesForce"); return salesForceAdapter.createMembersListFromJson(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private Consortium retrieveConsortiumFromSalesForce(String accessToken, String consortiumId) throws SalesForceUnauthorizedException { LOGGER.info("About get consortium from SalesForce"); validateSalesForceId(consortiumId); WebResource resource = createQueryResource( "SELECT (SELECT Id, Account.Name, Account.Public_Display_Name__c FROM ConsortiaOpportunities__r WHERE IsClosed=TRUE AND IsWon=TRUE AND Membership_End_Date__c>TODAY ORDER BY Membership_Start_Date__c DESC) from Account WHERE Id='%s'", consortiumId); ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting consortium from SalesForce"); return salesForceAdapter.createConsortiumFromJson(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private MemberDetails retrieveDetailsFromSalesForce(String accessToken, String memberId, String consortiumLeadId) throws SalesForceUnauthorizedException { MemberDetails details = new MemberDetails(); String parentOrgName = retrieveParentOrgNameFromSalesForce(accessToken, consortiumLeadId); details.setParentOrgName(parentOrgName); details.setParentOrgSlug(SlugUtils.createSlug(consortiumLeadId, parentOrgName)); details.setIntegrations(retrieveIntegrationsFromSalesForce(accessToken, memberId)); return details; } private String retrieveParentOrgNameFromSalesForce(String accessToken, String consortiumLeadId) { if (consortiumLeadId == null) { return null; } WebResource resource = createQueryResource("SELECT Public_Display_Name__c from Account WHERE Id='%s'", consortiumLeadId); ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting parent org name from SalesForce"); return salesForceAdapter.extractParentOrgNameFromJson(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private List<Integration> retrieveIntegrationsFromSalesForce(String accessToken, String memberId) throws SalesForceUnauthorizedException { WebResource resource = createQueryResource( "SELECT (SELECT Integration__c.Name, Integration__c.Description__c, Integration__c.Integration_Stage__c, Integration__c.Integration_URL__c from Account.Integrations__r) from Account WHERE Id='%s'", memberId); ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting integrations list from SalesForce"); return salesForceAdapter.createIntegrationsListFromJson(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private List<Contact> retrieveAllContactsFromSalesForceByAccountId(String accessToken, String accountId) throws SalesForceUnauthorizedException { LOGGER.info("About get list of all contacts from SalesForce"); validateSalesForceId(accountId); WebResource resource1 = createQueryResource("Select Id, AccountId, Name, Email, ORCID_iD_Path__c From Contact Where AccountId='%s'", accountId); WebResource resource = resource1; ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting all contacts from SalesForce"); return salesForceAdapter.createContactsFromJson(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private List<Contact> retrieveContactsWithRolesFromSalesForceByAccountId(String accessToken, String accountId) throws SalesForceUnauthorizedException { LOGGER.info("About get list of contacts from SalesForce"); validateSalesForceId(accountId); WebResource resource = createQueryResource( "Select (Select Id, Contact__c, Contact__r.FirstName, Contact__r.LastName, Contact__r.Email, Member_Org_Role__c From Membership_Contact_Roles__r) From Account a Where Id='%s'", accountId); ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting contacts from SalesForce"); return salesForceAdapter.createContactsWithRolesFromJson(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private List<ContactRole> retrieveContactRolesFromSalesForceByContactIdAndAccountId(String accessToken, String contactId, String accountId) throws SalesForceUnauthorizedException { LOGGER.info("About get list of contact roles from SalesForce"); validateSalesForceId(contactId); validateSalesForceId(accountId); WebResource resource1 = createQueryResource( "Select Id, Contact__c, Member_Org_Role__c From Membership_Contact_Role__c Where Contact__c = '%s' And Organization__c='%s'", contactId, accountId); WebResource resource = resource1; ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting contacts from SalesForce"); return salesForceAdapter.createContactRolesFromJson(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private String retrievePremiumConsortiumMemberTypeIdFromSalesForce(String accessToken) throws SalesForceUnauthorizedException { LOGGER.info("About get premium consortium member type ID from SalesForce"); WebResource resource1 = createQueryResource("Select Id From Member_Type__c Where Name = 'Premium Consortium Member'"); WebResource resource = resource1; ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting premium consortium member type ID from SalesForce"); return salesForceAdapter.extractIdFromFirstRecord(result); } /** * * @throws SalesForceUnauthorizedException * If the status code from SalesForce is 401, e.g. access token * expired. * */ private String retrieveConsortiumMemberRecordTypeIdFromSalesForce(String accessToken) throws SalesForceUnauthorizedException { LOGGER.info("About get consortium member record type ID from SalesForce"); WebResource resource1 = createQueryResource("Select Id, Name, SobjectType From RecordType Where SobjectType = 'Opportunity' And Name = 'Consortium Member'"); WebResource resource = resource1; ClientResponse response = doGetRequest(resource, accessToken); checkAuthorization(response); JSONObject result = checkResponse(response, 200, "Error getting consortium member record type ID from SalesForce"); return salesForceAdapter.extractIdFromFirstRecord(result); } private <T> T retry(Function<String, T> function) { try { return function.apply(getAccessToken()); } catch (SalesForceUnauthorizedException e) { LOGGER.debug("Unauthorized to access SalesForce, trying function again.", e); return function.apply(getFreshAccessToken()); } } private WebResource createObjectsResource(String path) { WebResource resource = client.resource(apiBaseUrl).path("services/data/v20.0/sobjects" + path); return resource; } private WebResource createObjectsResource(String path, String id) { validateSalesForceId(id); WebResource resource = client.resource(apiBaseUrl).path("services/data/v20.0/sobjects" + path + id); return resource; } private WebResource createQueryResource(String query) { return client.resource(apiBaseUrl).path("services/data/v20.0/query").queryParam("q", query); } private WebResource createQueryResource(String query, String... ids) { for (String id : ids) { validateSalesForceId(id); } String formattedQuery = String.format(query, ids); return client.resource(apiBaseUrl).path("services/data/v20.0/query").queryParam("q", formattedQuery); } private void retryConsumer(Consumer<String> consumer) { try { consumer.accept(getAccessToken()); } catch (SalesForceUnauthorizedException e) { LOGGER.debug("Unauthorized to access SalesForce, trying consumer again.", e); consumer.accept(getFreshAccessToken()); } } private String getFreshAccessToken() { LOGGER.info("About get SalesForce access token"); WebResource resource = client.resource(tokenEndPointUrl); Form form = new Form(); form.add("grant_type", "password"); form.add("client_id", clientId); form.add("client_secret", clientSecret); form.add("username", username); form.add("password", password); ClientResponse response = resource.accept(MediaType.APPLICATION_JSON_TYPE).type(MediaType.APPLICATION_FORM_URLENCODED_TYPE).post(ClientResponse.class, form); if (response.getStatus() == 200) { try { return response.getEntity(JSONObject.class).getString("access_token"); } catch (ClientHandlerException | UniformInterfaceException | JSONException e) { throw new RuntimeException("Unable to extract access token from response", e); } } else { throw new RuntimeException("Error getting access token from SalesForce, status code = " + response.getStatus() + ", reason = " + response.getStatusInfo().getReasonPhrase() + ", body = " + response.getEntity(String.class)); } } private ClientResponse doGetRequest(WebResource resource, String accessToken) { return resource.header("Authorization", "Bearer " + accessToken).accept(MediaType.APPLICATION_JSON_TYPE).get(ClientResponse.class); } private ClientResponse doPostRequest(WebResource resource, JSONObject bodyJson, String accessToken) { ClientResponse response = resource.header("Authorization", "Bearer " + accessToken).type(MediaType.APPLICATION_JSON_TYPE).post(ClientResponse.class, bodyJson); return response; } private ClientResponse doDeleteRequest(WebResource resource, String accessToken) { ClientResponse response = resource.header("Authorization", "Bearer " + accessToken).type(MediaType.APPLICATION_JSON_TYPE).delete(ClientResponse.class); return response; } private void checkAuthorization(ClientResponse response) { if (response.getStatus() == 401) { throw new SalesForceUnauthorizedException("Unauthorized reponse from SalesForce, status code = " + response.getStatus() + ", reason = " + response.getStatusInfo().getReasonPhrase() + ", body= " + response.getEntity(String.class)); } } private JSONObject checkResponse(ClientResponse response, int requiredStatus, String errorMessage) { if (response.getStatus() != requiredStatus) { throw new RuntimeException(errorMessage + ", status code = " + response.getStatus() + ", reason = " + response.getStatusInfo().getReasonPhrase() + ", body = " + response.getEntity(String.class)); } if (requiredStatus == 204) { return null; } return response.getEntity(JSONObject.class); } @Override public void afterPropertiesSet() throws Exception { client = Client.create(); if (clientLoggingEnabled) { client.addFilter(new LoggingFilter()); } } }