diff --git a/src/main/java/com/impactupgrade/nucleus/client/MinistryByTextClient.java b/src/main/java/com/impactupgrade/nucleus/client/MinistryByTextClient.java deleted file mode 100644 index c845fdb67..000000000 --- a/src/main/java/com/impactupgrade/nucleus/client/MinistryByTextClient.java +++ /dev/null @@ -1,160 +0,0 @@ -package com.impactupgrade.nucleus.client; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.impactupgrade.nucleus.dao.HibernateDao; -import com.impactupgrade.nucleus.entity.Organization; -import com.impactupgrade.nucleus.environment.Environment; -import com.impactupgrade.nucleus.environment.EnvironmentConfig; -import com.impactupgrade.nucleus.model.CrmContact; -import com.impactupgrade.nucleus.util.HttpClient; -import com.impactupgrade.nucleus.util.OAuth2; -import org.json.JSONObject; - -import javax.ws.rs.core.GenericType; -import java.util.ArrayList; -import java.util.List; - -import static com.impactupgrade.nucleus.util.HttpClient.get; -import static com.impactupgrade.nucleus.util.HttpClient.post; -import static javax.ws.rs.core.MediaType.APPLICATION_JSON; - -// TODO: To eventually become a mbt-java-client open source lib? -public class MinistryByTextClient { - - protected static String AUTH_ENDPOINT = "https://login-qa.ministrybytext.com/connect/token"; - protected static String API_ENDPOINT_BASE = "https://api-qa.ministrybytext.com/"; - - protected final Environment env; - protected HibernateDao organizationDao; - - private final OAuth2.Context oAuth2Context; - - public MinistryByTextClient(Environment env) { - this.env = env; - this.organizationDao = new HibernateDao<>(Organization.class); - - Organization org = getOrganization(); - JSONObject envJson = org.getEnvironmentJson(); - JSONObject mbtJson = envJson.getJSONObject("mbt"); - - this.oAuth2Context = new OAuth2.ClientCredentialsContext( - env.getConfig().mbt.clientId, env.getConfig().mbt.clientSecret, - mbtJson.getString("accessToken"), mbtJson.getLong("expiresAt"), mbtJson.getString("refreshToken"), AUTH_ENDPOINT); - } - - // TODO: move the code to util/common parent class? - protected Organization getOrganization() { - return organizationDao.getQueryResult( - "from Organization o where o.nucleusApiKey=:apiKey", - query -> { - query.setParameter("apiKey", env.getConfig().apiKey); - } - ).get(); - } - - public List getGroups(String campusId) { - return get(API_ENDPOINT_BASE + "campuses/" + campusId + "/groups", headers(), new GenericType<>() {}); - } - - public Group createGroup(String name, String description) { - Group group = new Group(); - group.name = name; - group.description = description; - return post(API_ENDPOINT_BASE + "campuses/" + env.getConfig().mbt.campusId + "/groups", group, APPLICATION_JSON, headers(), Group.class); - } - - // TODO: confirm this upserts by phone num -- if it creates dups, we'll need to fetch first - public Subscriber upsertSubscriber(CrmContact crmContact, String groupId) { - Subscriber subscriber = toMBTSubscriber(crmContact); - return post(API_ENDPOINT_BASE + "campuses/" + env.getConfig().mbt.campusId + "/groups/" + groupId + "/subscribers", subscriber, APPLICATION_JSON, headers(), Subscriber.class); - } - - protected Subscriber toMBTSubscriber(CrmContact crmContact) { - Subscriber subscriber = new Subscriber(); - subscriber.firstName = crmContact.firstName; - subscriber.lastName = crmContact.lastName; - subscriber.mobileNo = crmContact.phoneNumberForSMS(); - - // TODO: address, using mailing or billing - // TODO: relations - - return subscriber; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Group { - public String id; - public String name; - public String description; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Subscriber { - public String id; - public String firstName; - public String lastName; - public String mobileNo; - public SubscriberAddress address = new SubscriberAddress(); - public List relations = new ArrayList<>(); - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class SubscriberAddress { - public String address1; - public String address2; - public String state; - public String postalCode; - public String countryCode; - public String mobileCountryCode; - public Integer age; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class SubscriberRelation { - public String name; - public String relationship; - } - - // TODO: move the code to util/common parent class? - protected HttpClient.HeaderBuilder headers() { - String currentAccessToken = oAuth2Context.accessToken(); - if (currentAccessToken != oAuth2Context.refresh().accessToken()) { - Organization org = getOrganization(); - JSONObject envJson = org.getEnvironmentJson(); - JSONObject mbtJson = envJson.getJSONObject("mbt"); - - mbtJson.put("accessToken", oAuth2Context.accessToken()); - mbtJson.put("expiresAt", oAuth2Context.expiresAt() != null ? oAuth2Context.expiresAt() : null); - mbtJson.put("refreshToken", oAuth2Context.refreshToken()); - org.setEnvironmentJson(envJson); - organizationDao.update(org); - } - - return HttpClient.HeaderBuilder.builder().authBearerToken(oAuth2Context.accessToken()); - } - - //TODO: remove once done with testing - public static void main(String[] args) { - Environment env = new Environment() { - @Override - public EnvironmentConfig getConfig() { - EnvironmentConfig envConfig = new EnvironmentConfig(); - envConfig.mbt.clientId = "GVU05RE7VMACNUU96ME8"; - envConfig.mbt.clientSecret = "CPMw462MQdLTW6MDDlagUWlOhxXXJgDRc0D8cBHuUqhO=g6ELo"; - envConfig.mbt.campusId = "cf774a3b-4910-4b16-b6b0-608f80d216a4"; - return envConfig; - } - }; - MinistryByTextClient mbtClient = new MinistryByTextClient(env); - - CrmContact crmContact = new CrmContact(); - crmContact.id = "12345"; - crmContact.firstName = "Brett"; - crmContact.lastName = "Meyer"; - crmContact.mobilePhone = "260-349-5732"; - mbtClient.upsertSubscriber(crmContact, "c64ecadf-bbfa-4cd4-8f19-a64e5d661b2b"); - - // To check same access token is used - mbtClient.upsertSubscriber(crmContact, "c64ecadf-bbfa-4cd4-8f19-a64e5d661b2b"); - } -} diff --git a/src/main/java/com/impactupgrade/nucleus/client/OrgConfiguredClient.java b/src/main/java/com/impactupgrade/nucleus/client/OrgConfiguredClient.java new file mode 100644 index 000000000..4073afe45 --- /dev/null +++ b/src/main/java/com/impactupgrade/nucleus/client/OrgConfiguredClient.java @@ -0,0 +1,42 @@ +package com.impactupgrade.nucleus.client; + +import com.impactupgrade.nucleus.dao.HibernateDao; +import com.impactupgrade.nucleus.entity.Organization; +import com.impactupgrade.nucleus.environment.Environment; +import com.impactupgrade.nucleus.util.OAuth2; +import org.json.JSONObject; + +public class OrgConfiguredClient { + + protected final Environment env; + protected final HibernateDao organizationDao; + + public OrgConfiguredClient(Environment env) { + this.env = env; + this.organizationDao = new HibernateDao<>(Organization.class); + } + + protected JSONObject getEnvJson() { + return getOrganization(env.getConfig().apiKey).getEnvironmentJson(); + } + + protected void updateEnvJson(String clientConfigKey, OAuth2.Context context) { + Organization org = getOrganization(env.getConfig().apiKey); + JSONObject envJson = org.getEnvironmentJson(); + JSONObject clientConfigJson = envJson.getJSONObject(clientConfigKey); + + clientConfigJson.put("accessToken", context.accessToken()); + clientConfigJson.put("expiresAt", context.expiresAt() != null ? context.expiresAt() : null); + clientConfigJson.put("refreshToken", context.refreshToken()); + + org.setEnvironmentJson(envJson); + organizationDao.update(org); + } + + private Organization getOrganization(String apiKey) { + return organizationDao.getQueryResult( + "from Organization o where o.nucleusApiKey=:apiKey", + query -> query.setParameter("apiKey", apiKey) + ).get(); + } +} diff --git a/src/main/java/com/impactupgrade/nucleus/client/RaiselyClient.java b/src/main/java/com/impactupgrade/nucleus/client/RaiselyClient.java index c758e4c93..142edf883 100644 --- a/src/main/java/com/impactupgrade/nucleus/client/RaiselyClient.java +++ b/src/main/java/com/impactupgrade/nucleus/client/RaiselyClient.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.impactupgrade.nucleus.dao.HibernateDao; -import com.impactupgrade.nucleus.entity.Organization; import com.impactupgrade.nucleus.environment.Environment; import com.impactupgrade.nucleus.environment.EnvironmentConfig; import com.impactupgrade.nucleus.util.HttpClient; @@ -15,38 +13,23 @@ import static com.impactupgrade.nucleus.util.HttpClient.get; -public class RaiselyClient { +public class RaiselyClient extends OrgConfiguredClient { private static final String RAISELY_API_URL = "https://api.raisely.com/v3"; private static final String AUTH_URL = RAISELY_API_URL + "/login"; - protected final Environment env; - protected HibernateDao organizationDao; - private final OAuth2.Context oAuth2Context; public RaiselyClient(Environment env) { - this.env = env; - this.organizationDao = new HibernateDao<>(Organization.class); + super(env); - Organization org = getOrganization(); - JSONObject envJson = org.getEnvironmentJson(); - JSONObject raiselyJson = envJson.getJSONObject("raisely"); + JSONObject raiselyJson = getEnvJson().getJSONObject("raisely"); this.oAuth2Context = new OAuth2.UsernamePasswordContext( env.getConfig().raisely.username, env.getConfig().raisely.password, Map.of("requestAdminToken", "true"), raiselyJson.getString("accessToken"), raiselyJson.getLong("expiresAt"), raiselyJson.getString("refreshToken"), AUTH_URL); } - protected Organization getOrganization() { - return organizationDao.getQueryResult( - "from Organization o where o.nucleusApiKey=:apiKey", - query -> { - query.setParameter("apiKey", env.getConfig().apiKey); - } - ).get(); - } - //*Note this uses the donation ID from the Stripe metadata. Different from the donation UUID /* * From Raisely Support: @@ -73,19 +56,11 @@ public RaiselyClient.Donation getDonation(String donationId) { } protected HttpClient.HeaderBuilder headers() { - String currentAccessToken = oAuth2Context.accessToken(); - if (currentAccessToken != oAuth2Context.refresh().accessToken()) { - Organization org = getOrganization(); - JSONObject envJson = org.getEnvironmentJson(); - JSONObject raisely = envJson.getJSONObject("raisely"); - - raisely.put("accessToken", oAuth2Context.accessToken()); - raisely.put("expiresAt", oAuth2Context.expiresAt() != null ? oAuth2Context.expiresAt() : null); - raisely.put("refreshToken", oAuth2Context.refreshToken()); - org.setEnvironmentJson(envJson); - organizationDao.update(org); + String accessToken = oAuth2Context.accessToken(); + if (oAuth2Context.refresh().accessToken() != accessToken) { + // tokens updated - need to update config in db + updateEnvJson("raisely", oAuth2Context); } - return HttpClient.HeaderBuilder.builder().authBearerToken(oAuth2Context.accessToken()); } diff --git a/src/main/java/com/impactupgrade/nucleus/client/SpokeClient.java b/src/main/java/com/impactupgrade/nucleus/client/SpokeClient.java index ed17344d6..8e0c38f2d 100644 --- a/src/main/java/com/impactupgrade/nucleus/client/SpokeClient.java +++ b/src/main/java/com/impactupgrade/nucleus/client/SpokeClient.java @@ -20,38 +20,23 @@ import static javax.ws.rs.core.MediaType.APPLICATION_JSON; // TODO: To eventually become a spoke-phone-java-client open source lib? -public class SpokeClient { +public class SpokeClient extends OrgConfiguredClient { protected static String AUTH_ENDPOINT = "https://auth.spokephone.com/oauth/token"; protected static String API_ENDPOINT_BASE = "https://integration.spokephone.com/"; - protected final Environment env; - protected HibernateDao organizationDao; - private final OAuth2.Context oAuth2Context; public SpokeClient(Environment env) { - this.env = env; - this.organizationDao = new HibernateDao<>(Organization.class); + super(env); - Organization org = getOrganization(); - JSONObject envJson = org.getEnvironmentJson(); - JSONObject spokeJson = envJson.getJSONObject("spoke"); + JSONObject spokeJson = getEnvJson().getJSONObject("spoke"); this.oAuth2Context = new OAuth2.ClientCredentialsContext( env.getConfig().spoke.clientId, env.getConfig().spoke.clientSecret, spokeJson.getString("accessToken"), spokeJson.getLong("expiresAt"), spokeJson.getString("refreshToken"), AUTH_ENDPOINT); } - protected Organization getOrganization() { - return organizationDao.getQueryResult( - "from Organization o where o.nucleusApiKey=:apiKey", - query -> { - query.setParameter("apiKey", env.getConfig().apiKey); - } - ).get(); - } - public List getPhonebooks() { return get(API_ENDPOINT_BASE + "phonebooks", headers(), new GenericType<>() {}); } @@ -100,6 +85,15 @@ protected Contact toSpokeContact(CrmContact crmContact, String crmName) { return contact; } + protected HttpClient.HeaderBuilder headers() { + String accessToken = oAuth2Context.accessToken(); + if (oAuth2Context.refresh().accessToken() != accessToken) { + // tokens updated - need to update config in db + updateEnvJson("spoke", oAuth2Context); + } + return HttpClient.HeaderBuilder.builder().authBearerToken(oAuth2Context.accessToken()); + } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Phonebook { public String id; @@ -129,23 +123,6 @@ public static class ContactRequest { public String countryIso; } - protected HttpClient.HeaderBuilder headers() { - String currentAccessToken = oAuth2Context.accessToken(); - if (currentAccessToken != oAuth2Context.refresh().accessToken()) { - Organization org = getOrganization(); - JSONObject envJson = org.getEnvironmentJson(); - JSONObject spokeJson = envJson.getJSONObject("spoke"); - - spokeJson.put("accessToken", oAuth2Context.accessToken()); - spokeJson.put("expiresAt", oAuth2Context.expiresAt() != null ? oAuth2Context.expiresAt() : null); - spokeJson.put("refreshToken", oAuth2Context.refreshToken()); - org.setEnvironmentJson(envJson); - organizationDao.update(org); - } - - return HttpClient.HeaderBuilder.builder().authBearerToken(oAuth2Context.accessToken()); - } - //TODO: remove once done with testing public static void main(String[] args) { Environment env = new Environment() { diff --git a/src/main/java/com/impactupgrade/nucleus/client/VirtuousClient.java b/src/main/java/com/impactupgrade/nucleus/client/VirtuousClient.java index 2fa091537..8a063c8fd 100644 --- a/src/main/java/com/impactupgrade/nucleus/client/VirtuousClient.java +++ b/src/main/java/com/impactupgrade/nucleus/client/VirtuousClient.java @@ -3,8 +3,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.Strings; -import com.impactupgrade.nucleus.dao.HibernateDao; -import com.impactupgrade.nucleus.entity.Organization; import com.impactupgrade.nucleus.environment.Environment; import com.impactupgrade.nucleus.environment.EnvironmentConfig; import com.impactupgrade.nucleus.util.HttpClient; @@ -27,25 +25,19 @@ import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; -public class VirtuousClient { +public class VirtuousClient extends OrgConfiguredClient { private static final String VIRTUOUS_API_URL = "https://api.virtuoussoftware.com/api"; private static final int DEFAULT_OFFSET = 0; private static final int DEFAULT_LIMIT = 100; - protected final Environment env; - protected HibernateDao organizationDao; - private String apiKey; private final OAuth2.Context oAuth2Context; public VirtuousClient(Environment env) { - this.env = env; - this.organizationDao = new HibernateDao<>(Organization.class); + super(env); - Organization org = getOrganization(); - JSONObject envJson = org.getEnvironmentJson(); - JSONObject virtuousJson = envJson.getJSONObject("virtuous"); + JSONObject virtuousJson = getEnvJson().getJSONObject("virtuous"); this.apiKey = env.getConfig().virtuous.secretKey; this.oAuth2Context = new OAuth2.UsernamePasswordContext( @@ -53,24 +45,15 @@ public VirtuousClient(Environment env) { virtuousJson.getString("accessToken"), virtuousJson.getLong("expiresAt"), virtuousJson.getString("refreshToken"), env.getConfig().virtuous.tokenServerUrl); } - protected Organization getOrganization() { - return organizationDao.getQueryResult( - "from Organization o where o.nucleusApiKey=:apiKey", - query -> { - query.setParameter("apiKey", env.getConfig().apiKey); - } - ).get(); + // Contact + public Contact createContact(Contact contact) { + contact = post(VIRTUOUS_API_URL + "/Contact", contact, APPLICATION_JSON, headers(), Contact.class); + if (contact != null) { + env.logJobInfo("Created contact: {}", contact); + } + return contact; } - // Contact - public Contact createContact(Contact contact) { - contact = post(VIRTUOUS_API_URL + "/Contact", contact, APPLICATION_JSON, headers(), Contact.class); - if (contact != null) { - env.logJobInfo("Created contact: {}", contact); - } - return contact; - } - public Contact getContactById(Integer id) { return getContact(VIRTUOUS_API_URL + "/Contact/" + id); } @@ -277,19 +260,11 @@ private HttpClient.HeaderBuilder headers() { //If the verification code and user credentials are correct, you will receive a token as seen in the Token authentication above. //To request a new Token after the user enters the verification code, add an OTP header: //curl -d "grant_type=password&username=YOUR_EMAIL&password=YOUR_PASSWORD&otp=YOUR_OTP" -X POST https://api.virtuoussoftware.com/Token - String currentAccessToken = oAuth2Context.accessToken(); - if (currentAccessToken != oAuth2Context.refresh().accessToken()) { - Organization org = getOrganization(); - JSONObject envJson = org.getEnvironmentJson(); - JSONObject virtuousJson = envJson.getJSONObject("virtuous"); - - virtuousJson.put("accessToken", oAuth2Context.accessToken()); - virtuousJson.put("expiresAt", oAuth2Context.expiresAt() != null ? oAuth2Context.expiresAt() : null); - virtuousJson.put("refreshToken", oAuth2Context.refreshToken()); - org.setEnvironmentJson(envJson); - organizationDao.update(org); + String accessToken = oAuth2Context.accessToken(); + if (oAuth2Context.refresh().accessToken() != accessToken) { + // tokens updated - need to update config in db + updateEnvJson("virtuous", oAuth2Context); } - return HttpClient.HeaderBuilder.builder().authBearerToken(oAuth2Context.accessToken()); } diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/MinistryByTextCommunicationService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/MinistryByTextCommunicationService.java index ef0454c06..972f26478 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/MinistryByTextCommunicationService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/MinistryByTextCommunicationService.java @@ -2,12 +2,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.base.Strings; +import com.impactupgrade.nucleus.dao.HibernateDao; +import com.impactupgrade.nucleus.entity.Organization; import com.impactupgrade.nucleus.environment.Environment; import com.impactupgrade.nucleus.environment.EnvironmentConfig; import com.impactupgrade.nucleus.model.CrmContact; import com.impactupgrade.nucleus.util.HttpClient; +import com.impactupgrade.nucleus.util.OAuth2; +import org.json.JSONObject; -import javax.ws.rs.core.Form; import javax.ws.rs.core.GenericType; import javax.ws.rs.core.Response; import java.util.ArrayList; @@ -17,7 +20,6 @@ import static com.impactupgrade.nucleus.util.HttpClient.get; import static com.impactupgrade.nucleus.util.HttpClient.isOk; -import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; public class MinistryByTextCommunicationService extends AbstractCommunicationService { @@ -25,8 +27,8 @@ public class MinistryByTextCommunicationService extends AbstractCommunicationSer protected static String AUTH_ENDPOINT = "https://login.ministrybytext.com/connect/token"; protected static String API_ENDPOINT_BASE = "https://api.ministrybytext.com/"; - protected String accessToken; - protected Calendar accessTokenExpiration; + protected Environment env; + protected HibernateDao organizationDao; @Override public String name() { @@ -38,6 +40,32 @@ public boolean isConfigured(Environment env) { return env.getConfig().ministrybytext != null && !env.getConfig().ministrybytext.isEmpty(); } + @Override + public void init(Environment env) { + this.env = env; + this.organizationDao = new HibernateDao<>(Organization.class); + } + + protected Organization getOrganization() { + return organizationDao.getQueryResult( + "from Organization o where o.nucleusApiKey=:apiKey", + query -> query.setParameter("apiKey", env.getConfig().apiKey) + ).get(); + } + + protected void updateEnvJson(String clientConfigKey, OAuth2.Context context) { + Organization org = getOrganization(); + JSONObject envJson = org.getEnvironmentJson(); + JSONObject configJson = envJson.getJSONObject(clientConfigKey); + + configJson.put("accessToken", context.accessToken()); + configJson.put("expiresAt", context.expiresAt() != null ? context.expiresAt() : null); + configJson.put("refreshToken", context.refreshToken()); + + org.setEnvironmentJson(envJson); + organizationDao.update(org); + } + @Override public void syncContacts(Calendar lastSync) throws Exception { for (EnvironmentConfig.MBT mbtConfig : env.getConfig().ministrybytext) { @@ -85,35 +113,18 @@ protected Subscriber upsertSubscriber(CrmContact crmContact, EnvironmentConfig.M } protected HttpClient.HeaderBuilder headers(EnvironmentConfig.MBT mbtConfig) { - if (isAccessTokenInvalid()) { - env.logJobInfo("Getting new access token..."); - HttpClient.TokenResponse tokenResponse = getAccessToken(mbtConfig); - accessToken = tokenResponse.accessToken; - Calendar onehour = Calendar.getInstance(); - onehour.add(Calendar.SECOND, tokenResponse.expiresIn); - accessTokenExpiration = onehour; + JSONObject mbtJson = getOrganization().getEnvironmentJson().getJSONObject("mbt"); + + OAuth2.Context oAuth2Context = new OAuth2.ClientCredentialsContext( + mbtConfig.clientId, mbtConfig.clientSecret, + mbtJson.getString("accessToken"), mbtJson.getLong("expiresAt"), mbtJson.getString("refreshToken"), AUTH_ENDPOINT); + + String accessToken = oAuth2Context.accessToken(); + if (oAuth2Context.refresh().accessToken() != accessToken) { + // tokens updated - need to update env json config + updateEnvJson("mbt", oAuth2Context); } - System.out.println(accessToken); - return HttpClient.HeaderBuilder.builder().authBearerToken(accessToken); - } - - protected boolean isAccessTokenInvalid() { - Calendar now = Calendar.getInstance(); - return Strings.isNullOrEmpty(accessToken) || now.after(accessTokenExpiration); - } - - protected HttpClient.TokenResponse getAccessToken(EnvironmentConfig.MBT mbtConfig) { - // TODO: Map.of should be able to be used instead of Form (see VirtuousClient), but getting errors about no writer - return post( - AUTH_ENDPOINT, - new Form() - .param("client_id", mbtConfig.clientId) - .param("client_secret", mbtConfig.clientSecret) - .param("grant_type", "client_credentials"), - APPLICATION_FORM_URLENCODED, - HttpClient.HeaderBuilder.builder(), - HttpClient.TokenResponse.class - ); + return HttpClient.HeaderBuilder.builder().authBearerToken(oAuth2Context.accessToken()); } // Having to modify this due to MBT's limited API. There's no upsert concept, and we want to avoid having to retrieve