The Old and the New: SOAP and Ember.js

We don’t always get to choose the technologies we have to deal with everyday. I, for one, would love to be using an iPhone 9 right now, but it seems Apple hasn’t yet invented it. Despite it being 2014, some of us have to deal with SOAP every day and yet yearn to use some of the new technologies out there, like Ember.js.

We at Bendyworks believe in the power of leveraging existing solutions, replacing them only when worthwhile. So it goes with many SOAP implementations out there; lots of business value is tied up in these services, and it’s easy to think new technology is out of one’s reach when having SOAP in the backend.

Fear not! What follows is a relatively simple way to connect a shiny, new Ember.js front-end to a SOAP backend. The solution presented only handles SOAP in the most straightforward way. Full interaction with a complicated SOAP service is left as an exercise to the reader.

Ember Setup

Our project will use a very simple SOAP service provided by webservicex.net that returns a list of elements on the periodic table. This service is so simple that its GetAtoms endpoint only returns a list of element names. Secondary endpoints provide atomic weights, atomic numbers, and symbols and will not be covered in this article. It doesn’t make for a very cool demo, but it will certainly prove our point.

We’ll use ember-cli to set up our project:

$ ember new atomic-soap && cd atomic-soap
$ ember generate resource atoms

The Model

Since our data source only provides a name, we’ll update our model as appropriate:

// app/models/atom.js
import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string')
});

Routing

We want to auto-transition to the atoms resource we already made, and we’ll eventually want to show something about the element, assuming we had more information about it. Here’s what our IndexRoute looks like:

// app/routes/index.js
import Ember from 'ember';

export default Ember.Route.extend({
  beforeModel: function() {
    this.transitionTo('atoms');
  }
});

And here’s what our Router looks like:

// app/router.js
import Ember from 'ember';

var Router = Ember.Router.extend({
  location: AtomicSoapENV.locationType
});

Router.map(function() {
  this.resource('atoms', function() {
    this.route('atom', {path: '/:name' });
  });
});

export default Router;

Finally, we’ll want to load our atoms from the store, so let’s set up an AtomsRoute:

// app/routes/atoms.js
import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return this.store.findAll('atom')
  }
});

Templates

We could get fancy and bring in Bootstrap or Foundation, but we’ll keep it simple here. We only need two templates: atoms.hbs and atoms/atom.hbs.

{{!-- app/templates/atoms.hbs --}}
<h5>Atoms</h5>
{{outlet}}
<ul>
  {{#each}}
  <li>{{#link-to 'atoms.atom' name}}{{name}}{{/link-to}}</li>
  {{/each}}
</ul>


{{!-- app/templates/atoms/atom.hbs --}}
The element name is {{name}}.

Bring in the SOAP!

Everything up until now has been pretty standard Ember.js resource stuff. Even fetching the data from the Ember store has been the same. Now, we change things up a bit to handle a SOAP response. We do this with an adapter, which fetches the data, and a serializer, which converts the response into something Ember-Data can handle.

NB: Because of cross-site browser security, we need to proxy the request/response to the external SOAP service. In an intranet situation, you’d probably instead ask the SOAP service maintainer to just insert an appropriate Access-Control-Allow-Origin header. For our purposes, we’ll simply run ember server --proxy http://www.webservicex.net.

SOAP Adapter

In Ember Data, an adapter is an object that turns a domain request like findAll into, usually, an AJAX request. The FixtureAdapter is one exception, since it just grabs the data from fixtures instead of hitting the network. Our SOAP Adapter in this case needs only to convert a findAll call to SOAP request. Rather than implementing all of SOAP by fetching a WSDL and parsing it, we’re just going to assume we know the correct request to make.

// app/adapters/atom.js
import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  headers: {
    'SOAPAction': 'http://www.webserviceX.NET/GetAtoms',
    'Content-Type': 'text/xml;charset=UTF-8'
  },

  /*jshint multistr: true */
  getAtomsRequestData: '<?xml version="1.0" encoding="UTF-8"?> \
  <env:Envelope xmlns:tns="http://www.webserviceX.NET" \
                xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"> \
    <env:Body> \
      <tns:GetAtoms></tns:GetAtoms> \
    </env:Body> \
  </env:Envelope>',

  findAll: function() {
    return this.ajax('http://0.0.0.0:4200/periodictable.asmx',
                     'POST',
                     { data: this.getAtomsRequestData });
  },

  ajaxOptions: function(url, type, options) {
    // pretend to be a 'GET' to avoid certain nastiness in default impl
    var hash = DS.RESTAdapter.prototype.ajaxOptions.call(this, url, 'GET', options);
    hash.type = 'POST';
    hash.dataType = 'xml';
    return hash;
  }
});

This code is fairly self-explanatory. One might consider getAtomsRequestData to be an opportunity for abstraction or refactoring, but I want to keep this example clear.

One thing to note is that we don’t have to configure our app to use this adapter, since the filename and loader work together to assume that the exported code from app/adapters/atom.js should be used for the Atom model.

SOAP Serializer

The Ember Data subsystem will fire off the AJAX request and feed its response to a serializer. Similar to our adapter, we don’t need to configure anything as long as we name things correctly.

By manually inspecting the data in the response, we see that the data we care about is entity-escaped within the SOAP Body. So we have to extract the Body as text, parse it again as XML, then transform the resulting structure into an array of hashes:

// app/serializers/atom.js
import DS from 'ember-data';
import Ember from 'ember';

export default DS.JSONSerializer.extend({
  extractArray: function(store, type, xmlDoc) {
    var $ = Ember.$;
    var xml = $(xmlDoc).find('GetAtomsResponse GetAtomsResult').text();
    var innerXml = $($.parseXML(xml)).find('NewDataSet Table');

    return innerXml.map(function(idx, el) {
      return {
        id: idx,
        name: $(el).find('ElementName').text()
      };
    }).get();
  }
});

Conclusion

That’s it! We now have a simple Ember app that’s backed by SOAP. We’re not doing much yet, but we have a solid foundation that doesn’t feel like a total hack. In fact, thanks to the Adapter/Serializer patterns in Ember, this solution feels quite elegant. You can browse the repo on GitLab.

Developing further features will take two different forms. First, we’d want to extract common functionality from our serializer and our adapter. Second, we may want to look into implementing code that parses the WSDL file. I would recommend doing the former before the latter, since extracting common functionality should inform the design of a WSDL library.

For example, I would begin by implementing a SOAP adapter for an individual atom, since the service also provides individual endpoints for getting an element’s atomic weight, atomic number, and symbol. Then, I would find similarities between the adapters and begin extraction of common functionality.

One thing to note if you choose to implement this exact example: the SOAP service at webservicesx.net doesn’t always successfully return data; sometimes it returns a timeout error. If you get such an error, try refreshing… you’ll eventually get good data.


Category: Development
Tags: Ember, SOAP, Tutorial