PI Coresight as an RSS feed reader?

 

Using the new PI Coresight extensibility model (still in CTP) it is possible to have PI Coresight serve up your favorite RSS feeds. In this blog post, I will walk you through creating the new symbol, validating the URL (from PI AF), getting the RSS feed, and populating the display.

 

The first step is storing your RSS feeds in PI AF, because everything is better in PI AF . Below is a screen shot of the attribute, of string type, set to be the New York Times RSS feed.

 

The next step is to begin with the basic symbol presentation layer. Since we will be creating the entire list of feeds in code, we only need a container div in the presentation layer. I will add this file to the extensibility directory in PI Coresight, INSTALLATION_FOLDER\Scripts\app\editor\symbols\ext, and name it sym-rssfeed-template.html.

 

<div id="container">
</div>

 

After this, we can begin working on the implementation layer of the symbol. First, we will create the JavaScript file for this symbol, in the same directory as above, and call it sym-rssfeed.js. In this file, we will add the shell for a PI Coresight custom symbol.

 

(function (CS) { 
     'use strict';
     var def = {
          typeName: 'rssfeed',
          displayName: 'RSS Feed',
          datasourceBehavior: CS.DatasourceBehaviors.Single,
          getDefaultConfig: function() {
               return {
                    DataShape: 'Value',
                    Height: 250,
                    Width: 500
               };
          }
     };
     CS.symbolCatalog.register(def);
})(window.Coresight); 

 

Here we are creating the definition and setting it to accept single data sources, and be based on the Value shaper. This will allow us to create this symbol from a single AF attribute and get the value of that attribute. At this point, after an IIS reset, your symbol should be added to the symbol selector menu, but will not be functional.

 

We need to update the symbol definition to add the initialization function, and write the actual initialization function. Below is the updated definition object, as well as the shell of the initialization function.

     var def = {
          typeName: 'rssfeed',
          displayName: 'RSS Feed',
          datasourceBehavior: CS.DatasourceBehaviors.Single,
          getDefaultConfig: function() {
               return {
                    DataShape: 'Value',
                    Height: 250,
                    Width: 500
               };
          },
          init: init
     };

     function init() {
          function dataUpdate(data) {

          }

          return { dataUpdate: dataUpdate };
     }

 

Inside the initialization function, we have added a function to handle all of the data updates sent to the symbol. Inside the update function we want to first make sure that we have data, then we want to make sure that the data passed to us, in this a URL, is valid.

 

     function init(scope, elem) {

          var feedURL;

          function dataUpdate(data) {
              if(data) {
                  if(isValidURL(data.Value)) {
                      feedURL = data.Value;
                  } else {
                      //todo handle the error
                  }
              }
          }

          function isValidURL(url) {
              var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
              return URL_REGEXP.test(url)
          }

          return { dataUpdate: dataUpdate }; 
     }

 

The validation code is a regex borrowed from Angular that tests to see if the string is in the proper URL format, but we also want to do something with the error, if one occurs. This is where we can tap into PI Coresight's error logging system. To do this, we will need to inject the log service into the symbol definition and add errors to the log when they occur.

 

     var def = {
          typeName: 'rssfeed',
          displayName: 'RSS Feed',
          datasourceBehavior: CS.DatasourceBehaviors.Single,
          getDefaultConfig: function() {
               return {
                    DataShape: 'Value',
                    Height: 250,
                    Width: 500
               };
          },
          init: init,
          inject: ['log']
     };

     function init(scope, elem, log) {

          var feedURL;

          function dataUpdate(data) {
              if(data) {
                  if(isValidURL(data.Value)) {
                      feedURL = data.Value;
                  } else {
                      log.add('RSS Feed', log.Severity.Error, 'Invalid URL for RSS feed (' + data.Value + ')');
                  }
              }
          }

          function isValidURL(url) {
              var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
              return URL_REGEXP.test(url)
          }

          return { dataUpdate: dataUpdate }; 
     }

 

In the current version of PI Coresight logging this error message will only write it out to the browser console window. In future versions, these error will be added to an error log that can be viewed in PI Coresight.

 

Now that we are getting the URL in the proper format, it is time to begin loading the URL and getting the list of news items listed here. The first problem we will run into when trying to make an ajax call to the RSS URL is a CORS issue. To work around this, we will use a public proxying site called https://crossorigin.me/ . To load the RSS feed, we will be using jQuery ajax calls. In order to add to the presentation layer, we first need to cache off the container element that will be used to hold the RSS links. At the top of the init function, where feedURL is defined, we need to add the container variable.

 

var container = elem.find('#container');

 

Next, we will create a helper function to get the feed data and populate the presentation layer, that is add the links to the container above.

 

function getFeedData() {
     $.ajax('https://crossorigin.me/' + feedURL).then(function(result) {
          // create the unordered list
          var $ul = $('<ul>');

          // find and loop through each of the RSS itesm
          var items = $(result).find('item');
          for(var i = 0; i < items.length; i++) {
               var $item = $(items[i]);
               // create a hyper link based on the RSS item
               var $a = $('<a>').attr('href', $item.find('link').text()).text($item.find('title').text());
               // add the link to a list item
               var $li = $('<li>').append($a);
               // add the list item to the unordered list
               $ul.append($li);
          }
          // clear any previous links
          container.children().remove();
          // add the unordered list to the container
          container.append($ul);
     });
}

 

The code above has comment to explain what it is doing, but in a general sense, it is grabbing the list of RSS feeds and adding each one as a link to a list and then adding that full list to the container.

The final piece of the puzzle is to call the getFeedData function from our dataUpdate function.

 

function dataUpdate(data) {
    if(data) {
        if(isValidURL(data.Value)) {
            feedURL = data.Value;
        } else {
            log.add('RSS Feed', log.Severity.Error, 'Invalid URL for RSS feed (' + data.Value + ')');
        }

        if(data.Path) {
            getFeedData();
        }
    }
}

 

In the code above we are leveraging PI Coresight's caching of meta data to only requery the RSS feed every minute, rather than every 5 seconds on normal data updates. This is done by only calling getFeedData if the server is returning the Path.

At this point, we have a fully functioning RSS feed loading symbol, but the styling of it leaves a lot to be desired. As we can see the in the screen shot below, the list exceeds the symbol boundaries; the items are centered, rather than left aligned; the colors are hard to read. To fix this, we will be adding some CSS styling to the list items we are creating.

First, we need to update the HTML container height to always be 100%.

 

<div id="container" style="height:100%">
</div>

 

Finally, we need to update the stylings in getFeedData.

 

function getFeedData() {
     $.ajax('https://crossorigin.me/' + feedURL).then(function(result) {
          // create the unordered list
          var $ul = $('<ul>').css({ height: '100%', 'overflow-y': 'scroll', 'text-align': 'left'});

          // find and loop through each of the RSS itesm
          var items = $(result).find('item');
          for(var i = 0; i < items.length; i++) {
               var $item = $(items[i]);
               // create a hyper link based on the RSS item
               var $a = $('<a>').attr('href', $item.find('link').text()).text($item.find('title').text()).css('color', 'white');
               // add the link to a list item
               var $li = $('<li>').append($a).css('margin-bottom', '5px');
               // add the list item to the unordered list
               $ul.append($li);
          }
          // clear any previous links
          container.children().remove();
          // add the unordered list to the container
          container.append($ul);
     });
}

 

In the code above, we set CSS styles on the unordered list, the hyperlink, and the list items.  The resulting symbol is shown below.

 

 

The final code for the presentation later, i.e. sym-rssfeed-template.html:

<div id="container" style="height:100%">
</div>

 

The final code for the implementation layer, i.e. sym-rssfeed.js:

(function (CS) { 
     'use strict';
     var def = {
          typeName: 'rssfeed',
          displayName: 'RSS Feed',
          datasourceBehavior: CS.DatasourceBehaviors.Single,
          getDefaultConfig: function() {
               return {
                    DataShape: 'Value',
                    Height: 250,
                    Width: 500
               };
          },
          init: init,
          inject: ['log']
     };

     function init(scope, elem, log) {

          var feedURL;
          var container = elem.find('#container');

          function dataUpdate(data) {
              if(data) {
                  if(isValidURL(data.Value)) {
                      feedURL = data.Value;
                  } else {
                      log.add('RSS Feed', log.Severity.Error, 'Invalid URL for RSS feed (' + data.Value + ')');
                  }

                  if(data.Path) {
                      getFeedData();
                  }
              }
          }

          function getFeedData() {
               $.ajax('https://crossorigin.me/' + feedURL).then(function(result) {
                    // create the unordered list
                    var $ul = $('<ul>').css({ height: '100%', 'overflow-y': 'scroll', 'text-align': 'left'});

                    // find and loop through each of the RSS itesm
                    var items = $(result).find('item');
                    for(var i = 0; i < items.length; i++) {
                         var $item = $(items[i]);
                         // create a hyper link based on the RSS item
                         var $a = $('<a>').attr('href', $item.find('link').text()).text($item.find('title').text()).css('color', 'white');
                         // add the link to a list item
                         var $li = $('<li>').append($a).css('margin-bottom', '5px');
                         // add the list item to the unordered list
                         $ul.append($li);
                    }
                    // clear any previous links
                    container.children().remove();
                    // add the unordered list to the container
                    container.append($ul);
               });
          }

          function isValidURL(url) {
              var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
              return URL_REGEXP.test(url)
          }

          return { dataUpdate: dataUpdate }; 
     }

     CS.symbolCatalog.register(def);

})(window.Coresight); 

 

The full code can also be found on our GitHub page.

 

Thanks to Michael Ferster the technical review of this article.