/* * Copyright © 2013. Palomino Labs (http://palominolabs.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.palominolabs.crm.sf.soap; import com.codahale.metrics.MetricRegistry; import com.palominolabs.crm.sf.core.Id; import com.palominolabs.crm.sf.soap.jaxwsstub.apex.ApexPortType; import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.MetadataPortType; import com.palominolabs.crm.sf.soap.jaxwsstub.partner.Soap; import org.slf4j.ext.XLogger; import org.slf4j.ext.XLoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; @SuppressWarnings("AccessToStaticFieldLockedOnInstance") @ThreadSafe final class ConnectionBundleImpl implements ConnectionBundle { private static final XLogger logger = XLoggerFactory.getXLogger(ConnectionBundleImpl.class); /** * Semaphore that all Connections from this bundle use */ @Nonnull private final CallSemaphore callSemaphore; @Nonnull private final BindingRepository bindingRepository; private final boolean sandboxOrg; /** * Used to make sure that new credentials are for the same org. It is set when the first connection is made, and * checked thereafter. */ @GuardedBy("this") @Nullable private Id orgId = null; /** * username to connect as -- must be set with updateCredentials. */ @GuardedBy("this") @Nonnull private String username; /** * user's sf passwd -- must be set with updateCredentials. */ @GuardedBy("this") @Nonnull private String password; /** * Null if there is no current session, etc to use. */ @Nullable @GuardedBy("this") private BindingConfig bindingConfig = null; private final MetricRegistry metricRegistry; private ConnectionBundleImpl(@Nonnull String username, @Nonnull String password, int maxConcurrentApiCalls, @Nonnull BindingRepository bindingRepository, boolean sandboxOrg, MetricRegistry metricRegistry) { this.sandboxOrg = sandboxOrg; this.bindingRepository = bindingRepository; this.metricRegistry = metricRegistry; this.callSemaphore = new CallSemaphore(); this.updateCredentials(username, password, maxConcurrentApiCalls); } /** * Get a new ConnectionBundle for a standard SF org (not a sandbox). * * @param bindingRepository the repository to use for bindings * @param username the username to log in with * @param password the password to log in with * @param maxConcurrentApiCalls the maximum number of api calls that can be made concurrently * @param metricRegistry metric registry * * @return a fully configured ConnectionBundle */ static ConnectionBundleImpl getNew(@Nonnull BindingRepository bindingRepository, @Nonnull String username, @Nonnull String password, int maxConcurrentApiCalls, MetricRegistry metricRegistry) { return new ConnectionBundleImpl(username, password, maxConcurrentApiCalls, bindingRepository, false, metricRegistry); } /** * Get a new ConnectionBundle for a sandbox SF org. * * @param bindingRepository the repository to use for bindings * @param username the username to log in with * @param password the password to log in with * @param maxConcurrentApiCalls the maximum number of api calls that can be made concurrently * @param metricRegistry metric registry * * @return a new ConnectionBundle for a sandbox org * * @see ConnectionBundleImpl#ConnectionBundleImpl(String, String, int, BindingRepository, boolean, MetricRegistry) */ static ConnectionBundleImpl getNewForSandbox(@Nonnull BindingRepository bindingRepository, @Nonnull String username, @Nonnull String password, int maxConcurrentApiCalls, MetricRegistry metricRegistry) { return new ConnectionBundleImpl(username, password, maxConcurrentApiCalls, bindingRepository, true, metricRegistry); } synchronized void updateCredentials(@Nonnull String newUsername, @Nonnull String newPassword, int maxConcurrentApiCalls) { if (newUsername == null) { throw new NullPointerException("Username can't be null"); } if (newPassword == null) { throw new NullPointerException("Password can't be null"); } logger.trace("Updating maxApiCalls to " + maxConcurrentApiCalls); this.callSemaphore.setMaxPermits(maxConcurrentApiCalls); if (newUsername.equals(this.username) && newPassword.equals(this.password)) { logger.trace("Got new credentials that were equal to the old credentials"); } else { logger.trace("Updating username to <" + newUsername + "> from <" + this.username + ">"); this.bindingConfig = null; this.username = newUsername; this.password = newPassword; } } @Nonnull @Override public synchronized PartnerConnection getPartnerConnection() { // com.sun.xml.ws.transport.http.client.HttpTransportPipe.dump = true; return PartnerConnectionImpl.getNew(this.callSemaphore, this, metricRegistry); } @Nonnull synchronized String getUsername() { return this.username; } /** * @return a metadata connection */ @Nonnull @Override public synchronized MetadataConnection getMetadataConnection() { return new MetadataConnectionImpl(this.callSemaphore, this, metricRegistry); } @Nonnull @Override public synchronized ApexConnection getApexConnection() { return new ApexConnectionImpl(this.callSemaphore, this, metricRegistry); } @Nonnull synchronized ConfiguredBinding<MetadataPortType> getMetadataBinding() throws ApiException { final BindingConfig data = this.getBindingConfig(); return new ConfiguredBinding<MetadataPortType>(this.bindingRepository.getMetadataBinding(data), data); } synchronized void acceptReleasedMetadataBinding(@Nonnull MetadataPortType binding) { this.bindingRepository.releaseMetadataBinding(binding); } @Nonnull synchronized ConfiguredBinding<ApexPortType> getApexBinding() throws ApiException { final BindingConfig data = this.getBindingConfig(); return new ConfiguredBinding<ApexPortType>(this.bindingRepository.getApexBinding(data), data); } synchronized void acceptReleasedApexBinding(@Nonnull ApexPortType binding) { this.bindingRepository.releaseApexBinding(binding); } /** * @return a configured binding ready for use by a PartnerConnectionImpl * * @throws ApiException if this requires a login which then fails */ @Nonnull synchronized ConfiguredBinding<Soap> getPartnerBinding() throws ApiException { final BindingConfig data = getBindingConfig(); return new ConfiguredBinding<Soap>(this.bindingRepository.getPartnerBinding(data), data); } synchronized void acceptReleasedPartnerBinding(@Nonnull Soap binding) { this.bindingRepository.releasePartnerBinding(binding); } /** * Clears the stored session id and sets a new one. * * @throws ApiException if the attempt to get a new session failed */ synchronized void reportBadSessionId() throws ApiException { this.bindingConfig = null; logger.info("Attempting to get new session id"); this.getBindingConfig(); } /** * Get the current BindingConfig, creating one if necessary. * * @return a valid BindingConfig object for this org * * @throws ApiException if a login was required but was unsuccessful */ @Override @Nonnull public synchronized BindingConfig getBindingConfig() throws ApiException { if (this.bindingConfig == null) { // need to login to get a session id BindingConfig newBindingConfig; newBindingConfig = this.bindingRepository .getBindingConfigData(this.username, this.password, this.callSemaphore, this.sandboxOrg); String sessionId = newBindingConfig.getSessionId(); // never been set -- this is the first connection if (this.orgId == null) { this.orgId = newBindingConfig.getOrgId(); logger.trace("Setting the bundle's org id to <" + this.orgId + "> from its first connection"); } else { // if the connections org id isn't what has been previously set, explode if (!this.orgId.equals(newBindingConfig.getOrgId())) { throw new IllegalStateException( "Somehow got a binding with a different organization Id: expected <" + this.orgId + ">, got <" + newBindingConfig.getOrgId() + ">. Did you update the credentials to those of a different org?"); } } logger.trace("User " + this.username + ", session id " + sessionId + " on metadata server" + newBindingConfig.getMetadataServerUrl()); this.bindingConfig = newBindingConfig; } // we now know there is at least one valid session id return this.bindingConfig; } }