We recommend printing this document so you have a hard-copy to refer to.
MacUpdate

Dynamically embedding ratings with RSS

Your RSS file URI:

Unable to generate URI

Contents

General Information

MacUpdate utilizes RSS to make it easy for developers to keep track of the ratings for the products they have registered. This provides rating information in a generic way, allowing individual developers great flexibility and independence from any particular solution after dealing with the initial XML contents (which is itself well-defined because of the RSS standard's insistence that files be well-formed and valid).1 XML parsing is supported by several languages and programming paradigms, letting the developer choose the manner and environment in which to proceed; this also means that RSS information can easily be included in almost any dynamically generated website within the paradigm and practices of the existing site (i.e., without requiring incorporation of a separate, additional language or module).

There are many ways the rating information for a particular product (or products) can be incorporated into an existing site. The recommendations and examples that follow are suggestions, minimal solutions, and cover a handful of common cases. They are by no means comprehensive with respect to the possibilities for handling RSS information, even within the context of the languages they represent.

Following the RSS 2.0 specification, information for each product is contained within an item element. For our purposes, the children of each such item are given as

elementsemantics for MU ratings RSS
title The name of the product, according to how it is listed on MacUpdate. There are no version numbers in this string, unless such numbering is constitutive.
link A fully qualified URI, referencing the info page on MacUpdate for the product.
description The calculated rating value for the product. This is a floating point number rounded at two decimal places.
pubDate Date and time when the rating was calculated; complies with RFC 2822.

The basic practice, then, is in most cases to parse the RSS file into some representative structure (an object, a hashtable, etc.), determine what the desired presentation is (i.e., what HTML you want to generate to present the results), write a corresponding function, method, subroutine, etc. to produce that HTML when provided an element of the representative structure, and then place the generative call (and necessary supporting code) in the site pages where output is desired. Below are explanations and examples utilizing PHP and Perl

Caching

In most cases, it is probably desirous to cache your ratings RSS file and periodically update it to include new rating information. Consider: using your RSS link to obtain the file at each and every processing point means retrieving the file remotely each and every time any page needs to parse it; if you include ratings embeddings on multiple pages, the server needs to obtain copies of the RSS file for every view of every page—this could potentially be a lot of redundancy (for your servers and for ours!). Most likely your needs can be served perfectly well by periodically (say, every few hours) obtaining a new, updated copy of your RSS file, storing it locally, and using the local version within your processing.

[prompt]> curl -o /path/to/store/rss/yourRSS.xml \ http://www.macupdate.com/rss/ratings.php?id=#...#
Listing 1: Command-line cURL command to retrieve RSS file and write it to /path/to/store/rss.
#!/usr/bin/perl use LWP::Simple; use XML::RSS; use strict; my $base = '/Users/matt/Sites/html/rss'; my $rssFilename = 'yourRSS.xml'; my $file = "http://www.macupdate.com/rss/ratings.php?id=2:78a15f81822c3e5d3df18d4e0b99220c"; my $rss = new XML::RSS (version => '2.0'); my $data = get($file); $rss->parse($data); # convert 2.0 to 1.0 # $rss->{'output'} = '1.0'; # you could also manually open a file handle # and write to the file handle with # $rss->as_string; $rss->save("$base/$rssFilename");
Listing 2: Perl script to retrieve RSS file, suitable for additional processing before writing output.

The above listings show two ways of performing such caching. Both are callable, e.g., from cron (for more information, see the manpage for crontab on Unix systems). The first example uses cURL to retrieve the contents; you should change the first filepath (/path/to/store/rss/) to match the location at which you want to cache the file, and name the file accordingly (yourRSS.xml is just our suggestion). The URI that follows should be the URI at the top of this file, which includes your ID key necessary to generate the proper file (NB: if no such URI appears at the top of this file, or if the page says it cannot be generated, check to make sure that you're logged in appropriately; this link is only available to registered, logged-in developers). The second listing is a brief Perl script which obtains the RSS file and writes it out to another file; you should modify the script to reflect the destination location, filename, and RSS URI accordingly. If the desired behavior is just to obtain a copy of the RSS file, cURL probably provides the simpler solution; however, the Perl solution allows for any additional processing or ancillary behavior that might be necessary (for example, if you already use RSS in other areas of your site but use a different specification—say, from the RDF family—the XML::RSS module provides for converting between specifications). Caveat emptor.

Legal Disclaimer

Source may be freely copied (links to complete files are included below) and modified for the purposes of embedding this information into an external website; files may not be redistributed or utilized in other ways without prior and express written permission of MacUpdate.

PHP

Probably the simplest way of incorporating RSS-based ratings into a site is by using PHP. As a developer, you will find a link to your RSS file above; in order to retrieve the contents, you can take advantage of the built-in PHP XML processing (based on Clark's expat processor).2

First, you may want to download a copy of the MURatingsParser class (we assume for the purpose of this example that you'll download this into the same directory that the PHP page in which you plan to include the rating information is located). Then, in the page you've written, the following code can be used to manipulate the contents of the RSS file into a PHP object.3

<?php require_once("MURatingsParser.php"); // following path is an example, and non-functional // NB: If you're using a locally cached version, change this // filepath to reflect the location of that local file. $RSS_PATH = "http://www.macupdate.com/rss/ratings.php?id=5873:78a15f81822c3e5d3df18d4e0b99220c"; // create the object, including the path to your RSS file $theParser = new MURatingsParser($RSS_PATH); // this may also be done in two steps, omitting the constructer argument: // $theParser = new MURatingsParser(); // $theParser->file = $RSS_PATH; // process the RSS file specified above (when the object was created) $theParser->parseFile(); ?>
Listing 3: Instantiating and intializing a MURatingsParser.

You should also define a function which is responsible for the way you want the results to appear on the page. The listing below contains an example of such a function (you are free to use this function—you'll find it in the download section below—or to modify it as necessary to suit your purposes). As defined in this listing, the function is intended to be called with its argument as a single item from the parser object defined above.4

<?php // write out rating info for the given item function write_item($item) { $rating = $item['description']; $formattedRating = number_format($rating, 1); $roundedRating = round(($rating * 2), 0) / 2; $stars = "http://www.macupdate.com/images/" . (($roundedRating >= 1) ? "stars{$roundedRating}.gif" : "stars0.gif"); $output = "<div style='width: 125px; font-family: Geneva, Helvetica, sans-serif; border: dotted 1px #cccccc;'>\n"; $output .= "<div><a href='{$item['link']}'><img alt='$roundedRating stars' src='$stars' border='0'></a> <span style='font-size: 8pt;'>($formattedRating)</span></div>\n"; $output .= "<div style='text-align: center;'><a href='{$item['link']}' style='text-decoration: none; font-weight: bold; font-size: 10pt; color: black;'>MacUpdate</a></div>\n"; $output .= "</div>\n\n"; return $output; } ?>
Listing 4: Simple definition of a function to display ratings information for an single item.

With this function definition, we can look at a handful of ways to use a MURatingsParser object that has been initialized and contains the contents of a given RSS file. For the purposes of the following examples, assume that we've instantiated and initialized the contents of an MURatingsParser as above, and so we have a non-trivial object called $theParser. Because our definition of write_item() returns a string instead of writing it out (e.g., with echo), we must write out the return value ourselves if we want to display the results where we make the function call.5

In the first example, we retrieve an item from our MURatingsParser object via the method ratingForTitle(), which expects a string that is the name of a product for which there is rating information in the RSS file.6 The item matching that title is returned from the object, and given to write_item(), which produces a string of HTML (according to the definition of the function), which is then written into the page (via echo) at that spot (to delay output of the HTML produced by the function, e.g. to place it somewhere else, viz. note 5). The output from this simple implementation looks like this (note, this only writes the rating value as a number and as a star sequence, and a link to the MacUpdate info page for the item):

4.5 stars (4.6)
MacUpdate

The next example uses the method ratingsList() to retrive a list of all the items in the object. The optional argument (which defaults to true) forces the returned array to use the titles of the items for the indices of the array, instead of generating numerical indices; this allows us to access individual elements of the returned list by title string instead of by position in the array. This example, then, produces exactly the same output as the first example, although it also assigns to a script variable the entire list of items; this may be useful if we wanted to call write_item() on more than one item.

If we wanted to display information for every item (e.g., the results of write_item()), the following two examples show the canonical way. In particular, because ratingsList() always returns a list of elements (even if there is only a single item), it is a suitable array value for foreach, which provides a straightforward way to do something to each element in the returned array. Because we have explicitly specified that we want the array to be indexed by title (by passing true to ratingsList()), we can iterate over the keys and values of the array (to make it more convenient to access the title of each element).7 In the first of these examples, we simply iterate through the entire array, writing a DIV with each item name (between the H4 tags) and the result of calling write_item() on that item (as above). For the second iteration example, we check the rating value to make sure it's greater than 3, and only output the results of write_item() when this is the case. Of course, it is possible to perform arbitrarily complicated processing on items, either to determine whether they should be displayed or not, or to transform results in any way desired.

<?php // examples $PROGRAM_TO_DISPLAY = "Your Program Name"; // assume the definition of $theParser (a MURatingsParser object) as in the listing above // write rating info for the program specified by $PROGRAM_TO_DISPLAY, above echo write_item($theParser->ratingForTitle($PROGRAM_TO_DISPLAY)); // get all the items from $theParser $itemList = $theParser->ratingsList(); echo write_item($itemList[$PROGRAM_TO_DISPLAY]); // this is exactly the same as example above // write rating info for each program in the RSS file (i.e., each item of $theParser) // (each item written into its own DIV): foreach ($theParser->ratingsList(true) as $title => $item) { // the argument to ratingsList() builds list with titles as keys echo "<div><h4>$title</h4>\n"; echo write_item($item); echo "</div>\n\n"; } // write rating info for each program where rating > 3: foreach ($theParser->ratingsList(true) as $title => $item) { if ($item['description'] > 3) { echo write_item($item); } } ?>
Listing 5: A few ways to use an MURatingsParser object and the definition of write_item() from the listing above.

These are only a few of the ways to work with the contents of your RSS file in PHP. As mentioned above, hopefully these examples are helpful, but they are not normative. Ideally, the structure of MURatingsParser is flexible enough that it lends itself to whatever your particular programming style or site design guidelines are. Feel free to customize its behavior, as well as to define write_item() (or whatever display mechanism is most appropriate for you) however it best meets your design goals.

Files

If you intend to use MURatingsParser to handle the processing of your RSS file, you should download a copy of that file and include it in each page that you want to use an instantiation in (via require_once or include_once statements). If there are multiple pages within which you would like to display the results of a write_item() call, you might wish to define the function in an external file and include it in each page as appropriate. Toward that end, we offer a PHP file containing just the example definition as given above (which you're free to modify); you can download this file and include it on each page (this will save you from needing to define the function again on every page in which you'd like to use it).

Remember, we offer MURatingsParser as a convenience. There are many ways to handle XML in PHP, and some might be more appropriate for your particular needs. As always, if our parser object does not seem to support your needs, you are encouraged to consult the PHP XML documentation.

Perl

In order to simplify handling of RSS in Perl (and so as not to reinvent perfectly good wheels), we suggest (and the following examples assume) utilizing several commonly-available modules.8 In particular, we utilize LWP::Simple for retrieval of the RSS file (this module contains all the behavior necessary for fetching the contents of the file—if you require more robust functionality, we encourage you to consider the full LWP module); actual parsing is handled by the XML::RSS module, which is capable of reading and producing RSS files—this makes it especially well-suited for periodically caching your dynamically-generated RSS file (see above). For the purposes of our example implementation of write_item, we also take advantage of Math::Round, although such functionality is not necessary and may just as easily be accounted for in other ways that depend only on the Perl core.

In addition to use-ing each of the modules mentioned above, for the purposes of the examples that follow, we assume use strict. You may, or may not, use such a directive at your discretion, but the following examples are all valid within the strict parsing rules so as to ensure the widest range of compatability.

The first step to incorporating the ratings results into a Perl-generated page is to create a new parser, get the contents of your dynamically-generated RSS file, and parse the RSS file. The following listing shows the Perl for performing that sequence of actions.

#!/usr/bin/perl use LWP::Simple; use XML::RSS; use strict; # the actual value of this variable should be the URI for your RSS # file as given on the developer home page ("etc" should be # your unique ID string) # NOTE: if you're using a locally cached version, change this URI # to point to the location of the local file (e.g., /var/tmp/rss/ratings.rss) my $rssfile = "http://www.macupdate.com/rss/ratings.rss?id=etc"; # instantiate a new parser, and get the contents of your RSS file my $rss = new XML::RSS (version => '2.0'); my $data = get($rssfile); # parse the RSS file data $rss->parse($data);
Listing 6: Create an XML::RSS parser, and parse the contents of your RSS file.

Once you've parsed the contents of the RSS file, we recommend handling output in a way similar to the PHP examples above. In particular, we suggest defining a subroutine (say) write_item, responsible for displaying the contents of a particuar item as determined by the parser object. The XML::RSS object stores items as a hash whose keys are the names of the constitutive elements (see the table above), so a display subroutine should probably expect to recieve a reference to a hash, from which it can access the particular items we wish to display. In the example that follows, note the use of Math::Round for part of the calculations.

#!/usr/bin/perl sub write_item ($item) { my $item = shift; my $rating = $item->{'description'}; my $formattedRating = sprintf("%.1f", $rating); # display rating with 1 decimal pt my $starsRating = round($rating * 2) / 2; # calculated nearest-half rating my $stars = ($starsRating >= 1) ? "http://www.macupdate.com/images/stars${starsRating}.gif" : "http://www.macupdate.com/images/stars0.gif"; my $link = $item->{'link'}; # link to info page on macupdate.com my $resultString = "<div style='width: 125px; font-family: Geneva, Helvetica, sans-serif; border: dotted 1px #cccccc;'>\n"; $resultString .= "<div><a href='$link'><img alt='$starsRating stars' src='$stars' border='0'></a> <span style='font-size: 8pt;'>($formattedRating)</span></div>\n"; $resultString .= "<div style='text-align: center;'><a href='$link' style='text-decoration: none; font-weight: bold; font-size: 10pt; color: black;'>MacUpdate</a></div>\n"; $resultString .= "($item->{'title'})</div>\n\n"; return $resultString; }
Listing 7: Simple display of a given item from the parser.

It is worth noting that this example returns a string result, suitable for writing as a fragment into an HTML page. It does not write the results into the page itself; you must print the results of this call yourself to include them in the page contents (see note 5 for more, mutatis mutandis).

In order to facilitate getting the information for a particular item, we include in the next listing another subroutine, itemForTitle, which returns the element of the items hash from the parser object for which the RSS title element matches the argument. This behaves in a way analogous to the method ratingForTitle() on the PHP class MURatingsParser above. Along with this utility definition, we provide a couple of examples utilizing write_item as defined above.

#!/usr/bin/perl use strict; sub itemsForTitle { my $title = shift; return grep { $_->{'title'} eq $title } @{$rss->{'items'}}; } my $PROGRAM_TO_DISPLAY = "Your Program Name"; # write info for the first item (note the print statement!) matching name my @itemList = itemsForTitle($PROGRAM_TO_DISPLAY); print write_item(shift @itemList); # write info for each item foreach my $item (@{$rss->{'items'}}) { print write_item($item); } # write info for each item w/rating > 3 foreach my $item (@{$rss->{'items'}}) { if ($item->{'description'} > 3) { print write_item($item); } }
Listing 8: Examples, and definition of itemForTitle.

The manner in which you utilize the examples above will depend in a large way on how you use Perl to generate content. The most straightforward way (and the way that we follow in our example below) is to include everything in the listings above in the Perl script where you would like to generate the result text. If you plan to include results from the RSS file in multiple pages, you may wish instead to include the definitions of the subroutines in a separate file that can be included on the pages for which you want to generate output, separately from the Perl that actually gets the contents of the RSS file and parses the RSS into a workable representation.

You might also wish to consider the section on general implementation details, above. Particularly if you plan to include calculated statistics on multiple pages, you should look at the section on caching and periodically updating the RSS file, so that you don't have to generate and parse the information every time you need it (i.e., on every page load).

Downloads

PHP
MURatingsParser [.php] [.gz] [.zip] Class file for parsing ratings RSS feed in PHP
Complete Example [.php] [.gz] [.zip] Example implementation of write_item(), as well as usage. Suitable as a starting point for a page which displays parsed content.
write_item() [.php] [.gz] [.zip] Example implementation of write_item(). You may use this as is, or as a starting point for creating a function to display parsed contents. This file can be included in any page on which you want to display results from MURatingsParser objects, allowing each such page to leverage the same function definition.
All examples [.tgz] [.zip] All example files in a tar'd or zipped archive.
Perl
Complete Example [.pl] [.gz] [.zip] Example implementation of write_item subroutine, with item-access and usage.
write_item [.pl] [.gz] [.zip] You may use this example implementation of write_item as-is, or as a starting point for your own custom presentation of RSS rating content.
All examples [.tgz] [.zip] All example files in a tar'd or zipped archive.
cron script [.pl] [.gz] [.zip] Script to fetch remote RSS file and cache locally. Must be edited before use, to specify appropriate values.

Notes

1 (back): An XML document may be neither well-formed nor valid (although this case is pathological), well-formed, or well-formed and valid. Technically, an XML document is well-formed if (and only if) its initial element follows only the production rules for an initial element, as given by the XML specification (an additional restriction, that parsed entities must be well-formed, is somewhat beyond the scope here; for more information, consult the specification). That is, a document is well-formed when, for any element of the document tree, that element is well-formed (i.e., it is constructed properly and, if the element is not empty, its children are constructed properly) and each child of that element is well-formed. Thus, a well-formed XML document must contain a single root element (cf the HTML tag in an HTML document); well-formedness dictates the acceptible construction of the set of children for that root element: what constitutes a correctly-constructed empty tag (e.g., IMG), or the contents of a correctly constructed paired-tag (e.g., DIV), how additional content must be structured within such a tag, etc.

In addition, a document is valid when it meets several additional logical constraints on the physical structure which is enforced by well-formedness. Technically, for an XML document to be valid it must have a declaration (a DTD) and the elements of the document must comply with the constraints established therein. For example, in valid HTML a UL element may contain as children only LI elements; a UL element containing at least one LI element is valid, but one containing no children, or containing at least one child of a different type (say, P), is not valid. Similarly, a TABLE element may contain a THEAD and at least one TBODY element; such a table is valid when the THEAD comes before the TBODY but not vice versa. Valid elements contain only children allowed by the specification, according to the order established by the specification for such an element. Valid documents, then, consist only of valid elements.

Because the RSS specification forces that compliant documents be well-formed and valid, the specification makes certain guarantees on the structure of a document. This means that, when parsing an RSS document, we can make certain structural assumptions that we would otherwise be unable to make (if we weren't assured that the document is valid, which implies well-formedness); web browsers, for example, must anticipate a high level of malformed content, and must be able to handle such content gracefully. On the other hand, we can assume that any valid RSS document consists of channels, and that each potentially contains a handful of preliminary information, followed by a non-empty series of item elements. Furthermore, we can depend on particular content within each item (a title, for example, is required). In addition, the contents of many such elements are tightly constrained (e.g., the pubDate element must contain a date which complies with RFC 2822, so if the element exists, it must have content and that content must assume a particular structure which drastically simplifies the conversion of the string into useful information—say, to obtain the time from the string). As a developer receiving an RSS document, you can assume the presence and location of particular information, instead of attempting to handle a multiplicity of formats and potential values. Classically, this reduces the complexity of the code, as well as the likelihood of problems.

2 (back): This assumes that your local copy of PHP has been compiled with XML functionality enabled (the default, using a bundled expat). You can check for the presence of such functionality by examining your configuration information (the easiest way is by examining the output of phpinfo()). More information about XML support, and about building an XML-enabled PHP, can be found in the PHP manual.

3 (back): We assume PHP 4, and thus the class behavior specific to that version. MURatingsParser is written according to the behavior of objects in version 4. This shouldn't cause problems to users of later versions, but might be useful to those planning to use this source as a starting point in developing their own RSS handling behavior. For more information on object behavior in PHP, see especially the PHP documentation for Version 4 objects and Version 5 objects (resp).

4 (back): This level of genericity is probably appropriate in most cases. In such cases where it is not, the user is encouraged to modify the calling structure of the function as necessary. For example, there may be cases in which it makes more sense to pass as an argument the entire parser object, or just the value of the rating for the particular item, or references to additional user-defined structures, etc. The nature of the example listing above is advisory, not normative.

5 (back): Web designers with relatively little experience using PHP may find this capability somewhat redundant; write_item() returns a string instead of writing its output in place specifically to facilitate including the results of a particular call (or calls) within a larger string which may be constructed in its entirety before being output, or which may be output in an entirely different place from where it is constructed. Technically, any output which can be constructed this way may also be generated entirely via a sequence of echo or print statements, but frequently it is cleaner and much easier to follow when output is built up and then larger strings written out a fewer number of times. Performance sticklers will also note that frequently there is a computational advantage to constructing large and complicated strings in memory followed by a single write, rather than enduring the overhead of writing to the page many times for smaller pieces of the string (there is less overhead appending to a string than writing into the output stream, so minimizing such writes saves that additional overhead time). Of course, if you'd rather write_item() actually write to the page, you're free to redefine it accordingly (Hint: in the definition above, change the statement return $output; to echo $output;).

6 (back): Names are case-insensitive (i.e., arguments are case-folded before comparison), although no processing is currently done on whitespace. If you expect results where you receive none, it may be due to spacing issues. The product names in the RSS file come from MacUpdate, and so should be identical to the way the product name is displayed throughout the site. You may wish to verify your naming by checking it against the info page for that particular product on the site (you can find a direct link to the info page for a particular product as the contents of the link element for that item in the RSS file). You can also verify the appropriate string to use by examining the title element of the corresponding item. Lastly, MURatingsParser includes another method named ratingsList(), which returns an array of all the items for the calling object. This array could be iterated over to see the titles; alternatively, by passing true to the method call (which is the default) you force the returned array to be indexed by the titles (see above for usage examples). Of course, if something is named incorrectly, or if you find typos, you shouldn't hesitate to contact us and we'll be happy to correct it.

7 (back): Note that this is strictly a matter of convenience. In particular, given an iteration of such a foreach statement that binds a particular element of the array to $i, we can obtain the title of that element as $i['title'] regardless of whether we are iterating (via foreach) over values or key-value pairs. For more information, see the PHP manual on foreach.

8 (back): The easiest (and usually preferrable) way to install any modules which may not already be available in your Perl system is via CPAN. In most cases you may simply run the shell (from the command line, perl -MCPAN "shell") and, at the CPAN prompt, type install Module::Name, whereupon CPAN will take care of the heavy lifting required to install packages. As always, consult the documentation if you're not familiar with CPAN or its usage.