Salesforce.com Partner SOAP API JAX-WS Tutorial Part 2
Posted by Marshall Pierce on 2009/07/09 (32)
This is Part 2 in a series. See , , and to get up to speed. By the end of this tutorial, you’ll know how to instantiate some of the classes you created in Part 1 and how to log in to Salesforce.com with the Partner API. Update: I’ve released an that presents a better interface to the Partner API (as well as other Salesforce APIs). Getting started Follow the steps in to generate the code to use the Partner API. We’ll assume the code was generated with the package ‘sfstub’. You should also create a Developer Salesforce account, which you can do by following the “Free Developer Edition” link on the left of the page at . Preparing to log in The first step is to get a stub that you can then use to execute API calls. This is one of the classes that was autogenerated for you by JAX-WS. You can see what class to use by looking at the bottom of the WSDL. <!-- Soap Service Endpoint --> <service name="SforceService"> <documentation>Sforce SOAP API</documentation> <portbinding="tns:SoapBinding" name="Soap"> <soap:address location=" </port> </service> JAX-WS will have generated a SforceService class and a Soap class in the sfstub package to correspond to the service and port elements in the WSDL. The service only needs to be instantiated once for the lifetime of your app. It’s also fairly expensive to create, so make sure to cache your SforceService instance for reuse. It’s worth noting, however, that the generated code for the SforceService class contains the literal file path to the WSDL that was used to generate it. This will cause problems when you try and deploy your completed jar to somewhere other than your development machine since you very likely won’t have the WSDL file in the same place on the file system. Instead, you would presumably package the WSDL into the jar. Thus, instead of having a URL like “file:/home/yourname/eclipse/mysalesforceapp/partner.wsdl”, you want a URL like “/resources/partner.wsdl”. The no-arg constructorfor SforceService defaults to using the URL for the file path to the WSDL file, so you should instead use the other constructor that allows you to specify which URL should be used. You’ll also have to specify the QName for the second argument. Simply use the same QName invocation that the default constructor for SforceService uses. You’ll end up with something like this: String path = "/partner-15.wsdl"; URL url = YourClass.class.getResource(path); if (url == null) { throw new SomeException("Couldn't find sf partner wsdl for path " path); } service = new SforceService(url, new QName("urn:partner.soap.sforce.com", "SforceService")); Once you’ve got your SforceService instance, you can use it to create a port. Think of the port as the actual connection: it’s what you will use to make API calls. Soap port = service.getSoap(); Now you’ve got a port, but you still need to log in to get a session id so you can query(),etc. To log in through the partner API, you need a username and password as well as a partner key. The partner key is provided to you by Salesforce.com if you are a certified partner. If you’re not certified, you can . Once you have the three pieces of information you need, you’re ready to log in. You’ll need to specify your partner key. This is a little complicated since it involves setting an outbound header. CallOptions callOpts = new CallOptions(); callOpts.setClient(YOUR_PARTNER_KEY); WSBindingProvider wsBindingProvider = (WSBindingProvider) port; JAXBContext jaxbContext; try { // use the package you created your stub classes in jaxbContext = JAXBContext.newInstance("sfstub"); } catch (JAXBException e) { throw new SomeException( "Could not get the JAXB context for the stub package", e); } wsBindingProvider.setOutboundHeaders(Headers.create((JAXBRIContext) jaxbContext, callOpts)); First, create a Login object (which represents a loginRequest message in the WSDL) and set the user credentials in it. Login loginParam = new Login(); loginParam.setPassword(password); loginParam.setUsername(username); Making the login() call Now you’re ready to make the actual login() API call. I’m going to be using the method parameter and return style that you’ll see when you use a JAXB binding file. I’ll explain the other style after this example. LoginResponse response; try { response = port.login(loginParam); } catch (InvalidIdFault_Exception e) { throw new SomeException("Invalid Id", e, e .getFaultInfo()); } catch (LoginFault_Exception e) { throw new SomeException("Bad credentials for user '" username "'", e, e.getFaultInfo()); } catch (UnexpectedErrorFault_Exception e) {throw new SomeException("Unexpected error", e, e .getFaultInfo()); } catch (WebServiceException e) { throw new SomeException("Web Service exception", e); } If you used -B-XautoNameResolution, you wouldn’t need a Login object. Instead, you would directly pass the username and password to the login() call. port.login(username, password); If this applies to you, do that instead of port.login(loginParam); that you see in the example. The LoginResponse object represents the incoming loginResponse message for our outbound loginRequest. The login() method throws three checked exceptions: InvalidIdFault_Exception, LoginFault_Exception and UnexpectedErrorFault_Exception. Those three are all declared as faults in the WSDL that can occur for the login method, so the generated code declares those in the method signature for login(). Most API calls can throw UnexpectedErrorFault_Exception. There are several other fault types declaredin the WSDL as well. In some of the calls to our hypothetical custom exception class SomeException, I’m calling getFaultInfo() to get a constructor parameter. This method returns an ApiFault object that contains a fault code and fault message. The fault code comes from . The fault message is generally a human-readable explanation. Keeping track of this information in whatever exception class(es) you use for faults thrown by the API will ease debugging. The last exception being caught is . This is thrown by the JAX-WS stack when something goes wrong in the stack itself. Some examples of things that could throw this exception (or its subclasses) are networking failures and XML encoding errors. This is an unchecked exception, so the compiler will not warn you if you don’t catch it. Be careful to catch it whenever you make an API call on the port. Post-Login details Changing the endpoint When making the initial login request, JAX-WS will automatically use the endpoint specified in theWSDL. Presumably as a load-balancing measure, Salesforce.com will then provide another endpoint to use for all further communication once you’ve logged in. To get the new endpoint, we first need to get the LoginResultType object from the LoginResponse. This is because the loginResponse WSDL message is declared to contain a loginResult element that actually contains the data of the response. This pattern of having an object for the message that contains another object with the data is used throughout the various API calls LoginResultType loginResult = response.getResult(); If you chose to use my example JAXB customization for the generated class names in the previous tutorial, you should use LoginResultType like I have above. If you chose to use -B-XautoNameResolution instead, the login() call would return a LoginResult directly without an intermediate LoginResponse. (If the WSDL changes in the future to cause a name conflict for “loginResult”, you could theoretically need touse LoginResult2 if that’s the way the name conflict was resolved. As of my testing with API v16, though, there isn’t a name conflict, so regular old LoginResult works fine with -B-XautoNameResolution.) In general, if you’re using a JAXB binding file, you’ll need to wrap method arguments and unwrap method return values with their call-specific wrapper objects, whereas if you’re using -B-XautoNameResolution, you will not need to. Now that we have the login result data, we can set the new endpoint. Map<string, Object> reqContext = wsBindingProvider.getRequestContext(); reqContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, loginResult.getServerUrl()); Enabling GZip compression Using GZip to compress the XML SOAP data will drastically reduce the bandwidth used. We can do this by setting another request context item. Map<string, List<string>> httpHeaders = new HashMap<string, List<string>>(); httpHeaders.put("Content-Encoding",Collections.singletonList("gzip")); httpHeaders.put("Accept-Encoding", Collections.singletonList("gzip")); reqContext.put(MessageContext.HTTP_REQUEST_HEADERS, httpHeaders); Setting the Session Id Finally, we need to set the session id header so that future API calls will be tied to the user we just logged in as. We can do this in a way similar to how we set the partner key earlier. List<header> headers = new ArrayList<header>(); SessionHeader sessionHeader = new SessionHeader(); sessionHeader.setSessionId(loginResult.getSessionId()); headers.add(Headers.create((JAXBRIContext) jaxbContext, sessionHeader)); headers.add(Headers.create((JAXBRIContext) jaxbContext, callOpts)); wsBindingProvider.setOutboundHeaders(headers); This time, we want to set two headers since we need both the partner key and the session id. At this point, you now have a fully logged in, ready-to-use port. In of this series, I’ll cover how todownload data from Salesforce.com using the port.