September 2014

Sun Mon Tue Wed Thu Fri Sat
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        








« Source now available for the .NET Framework 3.5 | Main | Using F# Asynchronous Workflows to simplify concurrent programming in AutoCAD »

January 23, 2008

Turning AutoCAD into an RSS reader with F#

OK, OK, you are probably thinking "why would anyone ever want to use AutoCAD as an RSS reader?". The answer is, of course, "they wouldn't". The point of the next few posts is not actually to enable AutoCAD to be used to read RSS, but to show how it is possible to use F# and .NET to extract information from RSS feeds and create corresponding AutoCAD entities.

The reason I came onto this subject will also become more clear when you see my next post: I have been researching Asynchronous Workflows in F# - an uber-cool mechanism for managing concurrent, asynchronous tasks - and this seemed like a valid place to start. The problem I was looking for was one where I could simultaneously query and manipulate data from multiple sources, and then use that data to create AutoCAD entities. So, ultimately, the choice of RSS was both logical and completely irrelevant. :-)

Today I'm going to present code that works synchronously: in a single thread we are going to query website after website to download individual RSS feeds and to process them, extracting information on the various posts listed in the RSS, and create HyperLink objects in AutoCAD attached to DBText entities. These will be laid out such that - if you really, really wanted to - you could use these entities to open the various posts in your internet browser.

The reason I chose F# was really the ability to succinctly launch and coordinate asynchronous tasks - something you'll see in the next post, of course. While I could have used C# or VB.NET, F# is also well suited to dealing with lists of data - such as we'll be extracting from the various RSS feeds.

I used F# 1.9.3.7 to run this code: you will certainly need this version to run the code in the following post, as the asynchronous HTTP request functionality is new to the 1.9.3.7 release.

A few additional notes on the implementation... The below code somehow manages to support various RSS standards: Atom, RSS 1.0, RSS 2.0. But some of it feels like a bit of a "hack". The code queries for the titles, links and descriptions contained in each feed, and does some programmatic manipulation to end up - in the cases I've tested - with equal numbers of each. Feeds that use Feedburner, for instance, contain various types of link, which made this very tricky, but the below code appears to work for most cases. The point of this exercise is not to implement an "all singing, all dancing" implementation for RSS consumption: I simply did what was needed to get a number of different blogs working. If a particular feed you add doesn't work, you will just get a single entry created inside AutoCAD. Please don't expect me to debug why it doesn't work for that feed, as that was never the point of this exercise (and I wasted far too long getting to this point, believe me :-).

Here's the F# code:

// Use lightweight F# syntax


#light


// Declare a specific namespace and module name


module MyNamespace.MyApplication


// Import managed assemblies


#I @"C:\Program Files\Autodesk\AutoCAD 2008"


#r "acdbmgd.dll"

#r "acmgd.dll"


open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Geometry

open System.Xml

open System.Collections

open System.Collections.Generic

open System.IO

open System.Net

open Microsoft.FSharp.Control.CommonExtensions


// The RSS feeds we wish to get. The first two values are

// only used if our code is not able to parse the feed's XML


let feeds =

  [ ("Through the Interface",

    "http://blogs.autodesk.com/through-the-interface",

    "http://through-the-interface.typepad.com/through_the_interface/rss.xml");


    ("Don Syme's F# blog",

    "http://blogs.msdn.com/dsyme/",

    "http://blogs.msdn.com/dsyme/rss.xml");


    ("Shaan Hurley's Between the Lines",

    "http://autodesk.blogs.com/between_the_lines",

    "http://autodesk.blogs.com/between_the_lines/rss.xml");


    ("Scott Sheppard's It's Alive in the Lab",

    "http://blogs.autodesk.com/labs",

    "http://labs.blogs.com/its_alive_in_the_lab/rss.xml");


    ("Lynn Allen's Blog",

    "http://blogs.autodesk.com/lynn",

    "http://lynn.blogs.com/lynn_allens_blog/index.rdf");


    ("Heidi Hewett's AutoCAD Insider",

    "http://blogs.autodesk.com/autocadinsider",

    "http://heidihewett.blogs.com/my_weblog/index.rdf") ]


// Fetch the contents of a web page, synchronously


let httpSync (url:string) =

  let req = WebRequest.Create(url)

  use resp = req.GetResponse()

  use stream = resp.GetResponseStream()

  use reader = new StreamReader(stream)

  reader.ReadToEnd()


// Load an RSS feed's contents into an XML document object

// and use it to extract the titles and their links

// Hopefully these always match (this could be coded more

// defensively)


let titlesAndLinks (name, url, xml) =

  let xdoc = new XmlDocument()

  xdoc.LoadXml(xml)


  let titles =

    [ for n in xdoc.SelectNodes("//*[name()='title']")

        -> n.InnerText ]

  let links =

    [ for n in xdoc.SelectNodes("//*[name()='link']") ->

        let inn = n.InnerText

        if  inn.Length > 0 then

          inn

        else

          let href = n.Attributes.GetNamedItem("href").Value

          let rel = n.Attributes.GetNamedItem("rel").Value

          if href.Contains("feedburner") then

              ""

          else

            href ]


  let descs =

    [ for n in xdoc.SelectNodes

        ("//*[name()='description' or name()='content' or name()='subtitle']")

          -> n.InnerText ]


  // A local function to filter out duplicate entries in

  // a list, maintaining their current order.

  // Another way would be to use:

  //    Set.of_list lst |> Set.to_list

  // but that results in a sorted (probably reordered) list.


  let rec nub lst =

    match lst with

    | a::[] -> [a]

    | a::b ->

      if a = List.hd b then

        nub b

      else

        a::nub b

    | [] -> []


  // Filter the links to get (hopefully) the same number

  // and order as the titles and descriptions


  let real = List.filter (fun (x:string) -> x.Length > 0) 

  let lnks = real links |> nub


  // Return a link to the overall blog, if we don't have

  // the same numbers of titles, links and descriptions


  let lnum = List.length lnks

  let tnum = List.length titles

  let dnum = List.length descs


  if tnum = 0 || lnum = 0 || lnum <> tnum || dnum <> tnum then

    [(name,url,url)]

  else

    List.zip3 titles lnks descs


// For a particular (name,url) pair,

// create an AutoCAD HyperLink object


let hyperlink (name,url,desc) =

  let hl = new HyperLink()

  hl.Name <- url

  hl.Description <- desc

  (name, hl)


// Download an RSS feed and return AutoCAD HyperLinks for its posts


let hyperlinksSync (name, url, feed) =

  let xml = httpSync feed

  let tl = titlesAndLinks (name, url, xml)

  List.map hyperlink tl


// Now we declare our command


[<CommandMethod("rss")>]

let createHyperlinksFromRss() =


  // Let's get the usual helpful AutoCAD objects


  let doc =

    Application.DocumentManager.MdiActiveDocument

  let db = doc.Database


  // "use" has the same effect as "using" in C#


  use tr =

    db.TransactionManager.StartTransaction()


  // Get appropriately-typed BlockTable and BTRs


  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable

  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

      OpenMode.ForWrite)

    :?> BlockTableRecord


  // Add text objects linking to the provided list of

  // HyperLinks, starting at the specified location


  // Note the valid use of tr and ms, as they are in scope


  let addTextObjects pt lst =

    // Use a for loop, as we care about the index to

    // position the various text items


    let len = List.length lst

    for index = 0 to len - 1 do

      let txt = new DBText()

      let (name:string,hl:HyperLink) = List.nth lst index

      txt.TextString <- name

      let offset =

        if index = 0 then

          0.0

        else

          1.0


      // This is where you can adjust:

      //  the initial outdent (x value)

      //  and the line spacing (y value)


      let vec =

        new Vector3d

          (1.0 * offset,

          -0.5 * (Int32.to_float index),

          0.0)

      let pt2 = pt + vec

      txt.Position <- pt2

      ms.AppendEntity(txt) |> ignore

      tr.AddNewlyCreatedDBObject(txt,true)

      txt.Hyperlinks.Add(hl) |> ignore


  // Here's where we use the varous functions

  // we've defined


  let links =

    List.map hyperlinksSync feeds


  // Add the resulting objects to the model-space 


  let len = List.length links

  for index = 0 to len - 1 do


    // This is where you can adjust:

    //  the column spacing (x value)

    //  the vertical offset from origin (y axis)


    let pt =

      new Point3d

        (15.0 * (Int32.to_float index),

        30.0,

        0.0)

    addTextObjects pt (List.nth links index)


  tr.Commit()

Here's a portion of what gets created when you run the "rss" command:

AutoCAD does RSS   

That's it for today - in the next post we'll look at how to use asynchronous workflows to run RSS extraction tasks in parallel.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e200e54fee8c848833

Listed below are links to weblogs that reference Turning AutoCAD into an RSS reader with F#:

blog comments powered by Disqus

Feed/Share

10 Random Posts