Monday, August 9, 2010

WebCenter REST + Ruby = Awesome!

These days, I've been studying the WebCenter REST APIs and was happy to see the power of these APIs. The team invested a lot of time building these APIs, specially the Link model (HATEOAS) which is the heart of it.

HATEOAS is a heavy concept that needs to be understood, so you can fully take advantage of REST APIs. It is out of scope of this article, but I found a good reference from Ryan Kinderman with lots of links to other resources. BTW, I thought I knew REST until I read Ryan's post :)

Another cool thing I've been reading/studying these days is Ruby! I have to say it is awesome! The amount of things you can do with such little coding is impressive. Coming from Java & Objective C, the first time you look at Ruby code you don't quite get the syntax. But, after reading just a little about it, you see how you can do things pretty easy and with less coding than other languages.

Ok - time to mix things together now :)

So, WebCenter REST API is great! And, Ruby is perfect for running quick tests and get the ball rolling. Assuming you're a bit familiar with REST and Ruby, let's write a simple client to get the Person's activity stream from WebCenter Server. I also assume you have access to a WebCenter instance -- don't have one??? It is really easy to install :-)

On this sample, I'm using the following:
  • Ruby 1.9.2 -- actually used RVM to setup my Ubuntu machine
  • Mechanize 1.0.0 -- install with gem install mechanize - this helps with the HTTP requests, maintain session, cookies, etc.
  • JSON 1.4.6 -- install with gem install json

Connection

The first thing is to setup a wrapper for the connection. This class will handle the communication with the WebCenter REST server, perform authentication, and send the GET requests. In future, it should also wrap the other types of requests - POST, PUT and DELETE.
require 'rubygems'
require 'pp'
require 'mechanize'

USER_AGENT_ALIAS = 'Linux Mozilla' #provided by mechanize

class WcConn
  attr_accessor :wc_user, :wc_pass

  # Create the object
  def initialize(wc_user, wc_pass)
    @wc_user = wc_user
    @wc_pass = wc_pass

    #init mechanize fmk
    @mechanize_agent = Mechanize.new do |agent|
      agent.user_agent_alias = USER_AGENT_ALIAS
      agent.request_headers = {'accept' => 'application/json'}
    end
  end

  ##Login -- and retrieve resourceIndex
  def wc_login(uri)
    t1 = Time.now
    puts "--wc_login [#{uri}]"

    @mechanize_agent.auth(@wc_user,@wc_pass)
    logged_in_page = @mechanize_agent.get(uri)

    puts "--wc_login took [#{Time.now-t1}]s"
    #return the page, so processing can continue
    logged_in_page
  end

  ## access a page
  def goto_page(uri)
    t1 = Time.now
    puts "--goto_page [#{uri}]"
  
    page = @mechanize_agent.get(uri)

    puts "--goto_page took [#{Time.now-t1}]s"
    #return the page
    page
  end #goto_page

end #class

Wrappers for Objects

I also created wrappers for Link, ResourceIndex, Person, and Activities. These will read the JSON object to retrieve the value of few attributes. These objects are far from being complete, but they work fine for this simple example.
=begin
  object representation for Link & ResourceIndex
=end
  
#resource types -- not complete list 
RT_MSG_BOARD = 'urn:oracle:webcenter:messageBoard'.to_sym
RT_CMIS = 'urn:oracle:webcenter:cmis'.to_sym
RT_FORUMS = 'urn:oracle:webcenter:discussions:forums'.to_sym
RT_RC_IDX = 'urn:oracle:webcenter:resourceindex'.to_sym
RT_ACTIVITIES = 'urn:oracle:webcenter:activities:stream'.to_sym
RT_PERSON_ACTIVITIES =  'urn:oracle:webcenter:activities:stream:person'.to_sym
RT_FEEDBACK = 'urn:oracle:webcenter:feedback'.to_sym
RT_SPACES = 'urn:oracle:webcenter:spaces'.to_sym
RT_PEOPLE = 'urn:oracle:webcenter:people'.to_sym


class Link
  attr_accessor :resourceType, :href, :template, :capabilities, :rel, :type
  
  def initialize(options)
    @resourceType = options['resourceType'] ? options['resourceType'].to_sym : :EMPTY
    @href = options['href']
    @template = options['template']
    @capabilities = options['capabilities'] ? options['capabilities'].to_sym : :EMPTY
    @rel = options['rel'] ? options['rel'].to_sym : :EMPTY
    @type = options['type'] ? options['type'].to_sym : :EMPTY
  end
  
  # to String ---
  def inspect
    "LINK rt[#{@resourceType}] hr[#{@href}] tm[#{@template}] cp[#{@capabilities}] rel[#{@rel}] tp[#{@type}]"
  end
  
  def to_s
    inspect
  end
end #class_Link


class ResourceIndex
  #construct
  def initialize
    @links = []
  end
  
  def add_link(options)
    @links << Link.new(options)
  end
  
  def get_link_by_rel(rel)
    @links.find { |l| l.rel == rel }
  end

  def get_link_by_resource_type(resource_type)
    @links.find { |l| l.resourceType == resource_type }
  end

  # to String ---
  def inspect
    "RESOURCE_INDEX #{@links}"
  end
  
  def to_s
    inspect
  end
end #class_resourceIndex
=begin
  object representation for Person
=end

class Person
  attr_accessor :guid, :id, :display_name, :links   
  
  def initialize(options)
    @guid = options['guid']
    @id = options['id']
    @display_name = options['displayName']
    @links = []
    options["links"].each { |l| @links << Link.new(l) }
  end

  # to String ---
  def inspect
    "PERSON guid[#{@guid}] id[#{@id}] name[#{@display_name}]" # links[#{@links}]"
  end
  
  def to_s
    inspect
  end
end
=begin
  object representation for Activities 
=end

class Activities
  attr_accessor :messages   
  
  def initialize(array_items)
    @messages = []
    array_items.each do |i|
      msg_template = i['message']

      tp_items = i['templateParams']['items'] #arrays of items

      msg_items = {}
      tp_items.each do |tpi|
        tp_item_k = tpi['key']
        tp_item_v = tpi['displayName']
        msg_items[tp_item_k] = tp_item_v
      end
      d = DateTime.parse(i['createdDate'])
      #run the substitutions
      msg_items.each { |k,v| msg_template.sub!(/#{Regexp.escape(k)}/,"'#{v}'") }
      m = "#{d}, #{msg_template}"
      #puts m
      @messages << m
    end
  end
  
  # to String ---
  def inspect
    "Activities messages[#{@messages}]"
  end
  
  def to_s
    inspect
  end
end

Main class

The main class -- WcRest -- is responsible for the actual retrieval of the activities. It will login to WebCenter REST server using Basic authentication, retrieve the Resource Index. After parsing it, it will retrieve the Person object, and we need the guid. It will then follow the template for urn:oracle:webcenter:activities:stream to retrieve the 10 latest activities for that user, and display it as output.

Notes:
  • as this is main file, you should make sure if can be executed, or run it as ruby wc_rest.rb
  • the response from WcConn#wc_login and WcConn#goto_page is the Mechanize#Page, and thus we need to JSON parse the body of the page
  • #follow_template method will first substitute variables on the link using the options Map
  • #run method actually contains the sequence of the REST commands
#!/usr/bin/env ruby

require 'rubygems'
require 'json/ext'
load 'conn.rb'
load 'resource_index.rb'
load 'person.rb'
load 'activities.rb'

class WcRest
  # Create the object
  def initialize(wc_user, wc_pass, wc_uri)
    @resIdx = ResourceIndex.new 
    #initialize connection
    @wc_conn = WcConn.new(wc_user, wc_pass)
    @wc_uri = wc_uri
  end #initialize 
  
  ## gets /rest/api/resourceIndex
  def get_resourceIndex
    wc_resourceIdx =  @wc_conn.wc_login @wc_uri
    doc = JSON.parse(wc_resourceIdx.body)
    #  
    doc["links"].each do |element|
      @resIdx.add_link(element)  #new Link object from JSON
    end
    
    #pp @resIdx #uncomment to see ResourceIndex object
  end #get_resourceIndex
  
  ## 
  def follow_link(link)
    puts "FOLLOWING LINK... [#{link}]"
    return unless link.href 

    wc_rest_page =  @wc_conn.goto_page link.href
    # to JSON
    JSON.parse(wc_rest_page.body)
  end #follow_link
  
  ## templates have values that must be substituted prior to GET
  def follow_template(link, options)
    puts "FOLLOWING TEMPLATE... [#{link}]"
    return unless link.template 

    #prepare template
    template = link.template
    options.each { |k,v| template.sub!(/#{k}/,"#{v}") }

    wc_rest_page =  @wc_conn.goto_page template
    # to JSON
    JSON.parse(wc_rest_page.body)
  end #follow_template
 
  ## 
  def run
    #resourceindex
    puts "\n================================================================"
    puts "== #{RT_RC_IDX} =="
    puts "================================================================\n"
    get_resourceIndex
    
    #get person
    puts "\n================================================================"
    puts "== #{RT_PEOPLE} =="
    puts "================================================================\n"
    person = follow_link @resIdx.get_link_by_resource_type RT_PEOPLE
    p = Person.new person
    pp p
    
    #get ACTIVITIES
    puts "\n================================================================"
    puts "== #{RT_ACTIVITIES} =="
    puts "================================================================\n"
    template_values = {'{startIndex}'=>'0','{serviceIds}'=>'','{personal}'=>'true',
                       '{connections}'=>'true','{personGuid}'=>p.guid,
                       '{itemsPerPage}'=>'10','{groupSpaces}'=>'true',}
    activities = follow_template(@resIdx.get_link_by_rel(RT_ACTIVITIES), template_values)
    a = Activities.new activities['items']
    #pp a

    puts "\n================================================================"
    puts "== Activities Stream == "
    puts "================================================================\n"
    puts a.messages.join("\n")

    puts "\n--END--\n\n"
  end #run
end

# ===================================
# MAIN Execution
# ===================================
if __FILE__ == $0
  # 
  wcrest = nil

  if (ARGV.length == 3) #connect to given URL
    puts "WCR> connecting to #{ARGV[2]} with BASIC-AUTH..."
    wcrest = WcRest.new(ARGV[0],ARGV[1],ARGV[2])
  else 
      puts <<END_HELP
= Synopsis 
  Simple WebCenter Spaces REST client that displays user's activity stream

= Usage 
   Connect to given server, and uses BASIC-AUTH
   wcr.rb USER PASS SPACES_SERVER_URI
   
END_HELP
  end

  #run
  wcrest.run if wcrest
end

Sample Run

Below is an example of running the above against my test server, to display the activities of user weblogic
oracle@rolima-home:~/Documents/work/ruby/wcr-post$ ./wc_rest.rb weblogic welcome1 http://localhost:8888/rest/api/resourceIndex
WCR> connecting to http://localhost:8888/rest/api/resourceIndex with BASIC-AUTH...

================================================================
== urn:oracle:webcenter:resourceindex ==
================================================================
--wc_login [http://localhost:8888/rest/api/resourceIndex]
--wc_login took [0.212828757]s

================================================================
== urn:oracle:webcenter:people ==
================================================================
FOLLOWING LINK... [LINK rt[urn:oracle:webcenter:people] hr[http://localhost:8888/rest/api/people/@me/@self?stoken=FHMh49RIbgzsSGAO5IMed5xWr9ah4Oo*] tm[http://localhost:8888/rest/api/people/@me/@self?startIndex={startIndex}&projection={projection}&itemsPerPage={itemsPerPage}&stoken=FHMh49RIbgzsSGAO5IMed5xWr9ah4Oo*] cp[urn:oracle:webcenter:read] rel[EMPTY] tp[EMPTY]]
--goto_page [http://localhost:8888/rest/api/people/@me/@self?stoken=FHMh49RIbgzsSGAO5IMed5xWr9ah4Oo*]
--goto_page took [0.016172011]s
PERSON guid[599A52A05D3511DF8F701DCDD2E623D6] id[weblogic] name[weblogic]

================================================================
== urn:oracle:webcenter:activities:stream ==
================================================================
FOLLOWING TEMPLATE... [LINK rt[urn:oracle:webcenter:activities:stream] hr[] tm[http://localhost:8888/rest/api/activities?startIndex={startIndex}&serviceIds={serviceIds}&personal={personal}&connections={connections}&personGuid={personGuid}&itemsPerPage={itemsPerPage}&groupSpaces={groupSpaces}&stoken=FHMh49RIbgzsSGAO5IMed5xWr9ah4Oo*] cp[urn:oracle:webcenter:read] rel[urn:oracle:webcenter:activities:stream] tp[EMPTY]]
--goto_page [http://localhost:8888/rest/api/activities?astartIndex=0&serviceIds=&personal=true&connections=true&personGuid=599A52A05D3511DF8F701DCDD2E623D6&itemsPerPage=10&groupSpaces=true&stoken=FHMh49RIbgzsSGAO5IMed5xWr9ah4Oo*]
--goto_page took [0.05946188]s

================================================================
== Activities Stream == 
================================================================
2010-07-28T15:24:52-04:00, 'weblogic' created the page 'public docs'
2010-07-28T15:21:59-04:00, 'weblogic' created the document 'test.htm'
2010-07-22T17:52:18-04:00, 'weblogic' 'wasssuuuuuuuuuup!'

--END--

oracle@rolima-home:~/Documents/work/ruby/wcr-post$ 

Let me know if you have any questions, and have fun with REST & Ruby!