Using Ruby with the Google Data APIs

Jochen Hartmann, Google Data APIs Team
April 2008


Ruby is a dynamic scripting language that has received a good amount of attention in recent years due to the popular Rails web-development framework. This article will explain how to use Ruby to interact with Google Data API services. We will not focus on Rails, instead we are more interested in explaining the underlying HTTP commands and structure of our feeds. All of the examples presented here can be followed from the command line by using irb, Ruby's interactive shell.

As you may recall from the cURL article, the Google Data APIs use the Atom Publishing Protocol to represent, create and update web resources. The beauty of this protocol is that standard HTTP verbs are used to formulate requests which are answered with standard HTTP status codes.

The verbs that we will be using in this article are GET to retrieve content, POST to upload new content and PUT to update existing content. Some of the standard codes that you may come across in using the Google Data APIs are 200 to represent success in retrieving a feed or an entry, or 201 to represent the successful creation or update of a resource. If something goes wrong, such as when a malformed request is sent, a 400 code (meaning 'Bad Request') will be sent back. A more detailed message will be provided in the response body, explaining what exactly went wrong.

Ruby provides a nice debugging option as part of the 'Net' module. For the sake of keeping these code samples reasonably short, I have not enabled it here, however.

Obtaining and installing Ruby

Ruby can be installed using most package management systems if you are using Linux. For other operating systems and to obtain the full source code, please visit http://www.ruby-lang.org/en/downloads/. Irb, the interactive shell that we are going to be using for these examples should be installed by default. To follow the code examples listed here, you will also need to install XmlSimple, a small library to parse XML into Ruby datastructures. To obtain/install XmlSimple, please visit http://xml-simple.rubyforge.org/

Once you have a copy of Ruby running on your machine, you can use the Net:HTTP package to make basic requests to Google's Data services. The snippet below shows how to do the necessary imports from Ruby's interactive shell. What we are doing is requiring the 'net/http' package, parsing the URL for the top rated video feed from YouTube and then performing an HTTP GET request.

irb(main):001:0> require 'net/http'
=> true
irb(main):002:0> youtube_top_rated_videos_feed_uri = \
=> "http://gdata.youtube.com/feeds/api/standardfeeds/top_rated"
irb(main):003:0> uri = \
=> #<URI::HTTP:0xfbf826e4 URL:http://gdata.youtube.com/feeds/api/standardfeeds/top_rated>

irb(main):004:0> uri.host
=> "gdata.youtube.com"
irb(main):005:0> Net::HTTP.start(uri.host, uri.port) do |http|
irb(main):006:1* puts http.get(uri.path)
irb(main):007:1> end

That request should have echoed quite a bit of XML to the command line. You may have noticed that all of the items are contained within a <feed> element and are referred to as <entry> elements. Let us not worry about XML formatting just yet, I just wanted to explain how to make a basic Google Data API request using HTTP. We are going to switch APIs now and focus on Spreadsheets, since the information that we can send and retrieve is more 'command-line friendly'.

Authentication | Using the Google Spreadsheets API

We will again start by retrieving a feed of entry elements. This time though we will want to work with our own spreadsheets. In order to do that, we must first authenticate with the Google Accounts service.

As you may recall from the documentation on GData Authentication, there are two ways to authenticate with Google's services. AuthSub is for web-based applications and in a nutshell involves a token-exchange process. The real benefit of AuthSub is that your application does not need to store user credentials. ClientLogin is for "installed" applications. In the ClientLogin process, username and password are sent to Google's services via https along with a string that identifies the service that you are looking to use. The Google Spreadsheets API service is identified by the string wise.

Switching back to our interactive shell, let's authenticate with Google. Note that we are using https to send our authentication request and credentials:

irb(main):008:0> require 'net/https'
=> true
irb(main):009:0> http = Net::HTTP.new('www.google.com', 443)
=> #<Net::HTTP www.google.com:443 open=false>
irb(main):010:0> http.use_ssl = true
=> true
irb(main):011:0> path = '/accounts/ClientLogin'
=> "/accounts/ClientLogin"

# Now we are passing in our actual authentication data. 
# Please visit OAuth For Installed Apps for more information 
# about the accountType parameter
irb(main):014:0> data = \
irb(main):015:0* 'accountType=HOSTED_OR_GOOGLE&Email=your email' \
irb(main):016:0* '&Passwd=your password' \
irb(main):017:0* '&service=wise'

=> accountType=HOSTED_OR_GOOGLE&Email=your email&Passwd=your password&service=wise"

# Set up a hash for the headers
irb(main):018:0> headers = \
irb(main):019:0* { 'Content-Type' => 'application/x-www-form-urlencoded'}
=> {"Content-Type"=>"application/x-www-form-urlencoded"}

# Post the request and print out the response to retrieve our authentication token
irb(main):020:0> resp, data = http.post(path, data, headers)
warning: peer certificate won't be verified in this SSL session
=> [#<Net::HTTPOK 200 OK readbody=true>, "SID=DQAAAIIAAADgV7j4F-QVQjnxdDRjpslHKC3M ... [ snipping out the rest of the authentication strings ]

# Strip out our actual token (Auth) and store it
irb(main):021:0> cl_string = data[/Auth=(.*)/, 1]
=> "DQAAAIUAAADzL... [ snip ]

# Build our headers hash and add the authorization token
irb(main):022:0> headers["Authorization"] = "GoogleLogin auth=#{cl_string}"
=> "GoogleLogin auth=DQAAAIUAAADzL... [ snip ]

OK. So now that we are authenticated, let us try to retrieve our own spreadsheets using a request to


Since this is an authenticated request, we also want to pass in our headers. Actually, since we will be making several requests for various feeds, we might as well wrap this functionality into a simple function which we will call get_feed.

# Store the URI to the feed since we may want to use it again
irb(main):023:0> spreadsheets_uri = \
irb(main):024:0* 'http://spreadsheets.google.com/feeds/spreadsheets/private/full'

# Create a simple method to obtain a feed
irb(main):025:0> def get_feed(uri, headers=nil)
irb(main):026:1> uri = URI.parse(uri)
irb(main):027:1> Net::HTTP.start(uri.host, uri.port) do |http|
irb(main):028:2* return http.get(uri.path, headers)
irb(main):029:2> end
irb(main):030:1> end
=> nil

# Lets make a request and store the response in 'my_spreadsheets'
irb(main):031:0> my_spreadsheets = get_feed(spreadsheets_uri, headers)
=> #<Net::HTTPOK 200 OK readbody=true>

irb(main):032:0> my_spreadsheets
=> #<Net::HTTPOK 200 OK readbody=true>

# Examine our XML (showing only an excerpt here...)
irb(main):033:0> my_spreadsheets.body
=> "<?xml version='1.0' encoding='UTF-8'?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/'>
<category scheme='http://schemas.google.com/spreadsheets/2006' term='http://schemas.google.com/spreadsheets/2006#spreadsheet'/>
<title type='text'>Available Spreadsheets - test.api.jhartmann@gmail.com</title><link rel='alternate' type='text/html' href='http://docs.google.com'/>
<link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/spreadsheets/private/full'/><link rel='self' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/spreadsheets/private/full?tfe='/>
<id>http://spreadsheets.google.com/feeds/spreadsheets/private/full/o04927555739056712307.4365563854844943790</id><updated>2008-03-19T20:44:41.055Z</updated><category scheme='http://schemas.google.com/spreadsheets/2006' term='http://schemas.google.com/spreadsheets/2006#spreadsheet'/><title type='text'>test02</title><content type='text'>test02</content><link rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/worksheets/o04927555739056712307.4365563854844943790/private/full'/><link rel='alternate' type='text/html' href='http://spreadsheets.google.com/ccc?key=o04927555739056712307.4365563854844943790'/><link rel='self' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/spreadsheets/private/full/o04927555739056712307.4365563854844943790'/><author><name>test.api.jhartmann</name><email>test.api.jhartmann@gmail.com</email></author></entry><entry> ...

Again we are seeing a lot of XML which I have de-emphasized above since you don't need to worry about deciphering it from the command line. To make things more user-friendly, let us instead parse it into a datastructure using XmlSimple:

# Perform imports
irb(main):034:0> require 'rubygems'
=> true
irb(main):035:0> require 'xmlsimple'
=> true
irb(main):036:0> doc = \
irb(main):037:0* XmlSimple.xml_in(my_spreadsheets.body, 'KeyAttr' => 'name')

# Import the 'pp' module for 'pretty printing'
irb(main):038:0> require 'pp'
=> true

# 'Pretty-print' our XML document
irb(main):039:0> pp doc
    "content"=>"Available Spreadsheets - Test-account"}],
    "title"=>[{"type"=>"text", "content"=>"blank"}],
       "email"=>["my email"]}],
    "content"=>{"type"=>"text", "content"=>"blank"},
    [ snipping out the rest of the XML ]

Obtaining worksheets

So as you can see in the output above, my feed contains 6 spreadsheets. To keep this article short, I have cut off the rest of the XML output above (as well as in most other listings). To dig deeper into this spreadsheet, we will need to perform a few more steps:

  1. Obtain the spreadsheet key
  2. Use the spreadsheet key to obtain our worksheet feed
  3. Obtain the id for the worksheet which we want to use
  4. Request either the cellsFeed or listFeed to access the actual content of the worksheet

This may sound like a lot of work but I will show you that it's all quite easy if we write a few simple methods. The cellsFeed and listFeed are two different representations for the actual cell content of a worksheet. The listFeed represents an entire row of information and is recommended for POSTing new data. The cellFeed represents individual cells and is used for either individual cell updates or batch updates to many individual cells (both using PUT). Please refer to the Google Spreadsheets API documentation for more detail.

First we need to extract the spreadsheet key (highlighted in the XML output above) to then obtain the worksheet feed:

# Extract the spreadsheet key from our datastructure
irb(main):040:0> spreadsheet_key = \ 
irb(main):041:0* doc["entry"][0]["id"][0][/full\/(.*)/, 1]
=> "o04927555739056712307.3387874275736238738"

# Using our get_feed method, let's obtain the worksheet feed
irb(main):042:0> worksheet_feed_uri = \ 
irb(main):043:0* "http://spreadsheets.google.com/feeds/worksheets/#{spreadsheet_key}/private/full"
=> "http://spreadsheets.google.com/feeds/worksheets/o04927555739056712307.3387874275736238738/private/full"

irb(main):044:0> worksheet_response = get_feed(worksheet_feed_uri, headers)
=> #<Net::HTTPOK 200 OK readbody=true>

# Parse the XML into a datastructure
irb(main):045:0> worksheet_data = \ 
irb(main):046:0* XmlSimple.xml_in(worksheet_response.body, 'KeyAttr' => 'name')
=> {"totalResults"=>["1"], "category"=>[{"term ... [ snip ]

# And pretty-print it
irb(main):047:0> pp worksheet_data
 "title"=>[{"type"=>"text", "content"=>"blank"}],
    "title"=>[{"type"=>"text", "content"=>"Sheet 1"}],
    "content"=>{"type"=>"text", "content"=>"Sheet 1"},
    [ snip: cutting off the rest of the XML ]

As you can see here, we can now find the links (highlighted above) for accessing the listFeed and cellsFeed. Before we dig into the listFeed, let me quickly explain what data currently exists in our sample spreadsheet so that you will know what we are looking for:

Our spreadsheet is very simple and just looks like this:


And here is what this data looks like in the listFeed:

irb(main):048:0> listfeed_uri = \
irb(main):049:0* worksheet_data["entry"][0]["link"][0]["href"]
=> "http://spreadsheets.google.com/feeds/list/o04927555739056712307.3387874275736238738/od6/private/full"

irb(main):050:0> response = get_feed(listfeed_uri, headers)
=> #<Net::HTTPOK 200 OK readbody=true>
irb(main):051:0> listfeed_doc = \ 
irb(main):052:0* XmlSimple.xml_in(response.body, 'KeyAttr' => 'name')
=> {"totalResults"=>["2"], "category"=>[{"term" ... [ snip ]

# Again we parse the XML and then pretty print it
irb(main):053:0> pp listfeed_doc
 "title"=>[{"type"=>"text", "content"=>"Programming language links"}],
    "title"=>[{"type"=>"text", "content"=>"ruby"}],
     {"type"=>"text", "content"=>"website: http://java.com"},
    "title"=>[{"type"=>"text", "content"=>"php"}],
    "content"=>{"type"=>"text", "content"=>"website: http://php.net"},
    [ snip ]

As you can see, the listFeed returns your worksheet's content by creating an entry for each row. It assumes that the first row of the spreadsheet contains your cell headers and then dynamically generates XML headers based on the data in that row. Looking at the actual XML will help explain this further:

<?xml version='1.0' encoding='UTF-8'?><feed [ snip namespaces ]>
<category scheme='http://schemas.google.com/spreadsheets/2006' term='http://schemas.google.com/spreadsheets/2006#list'/>

<title type='text'>Programming language links</title>
[ snip: cutting out links and author information ]
    [ snip: updated and category ]
    <title type='text'>java</title>
    <content type='text'>website: http://java.com</content>
    <link rel='self' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/list/o04927555739056712307.3387874275736238738/od6/private/full/cn6ca'/>
    <link rel='edit' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/list/o04927555739056712307.3387874275736238738/od6/private/full/cn6ca/1j81anl6096'/>
    [ snip: updated and category ]
    <title type='text'>php</title>
    <content type='text'>website: http://php.net</content>
    <link rel='self' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/list/o04927555739056712307.3387874275736238738/od6/private/full/cokwr'/>
    <link rel='edit' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/list/o04927555739056712307.3387874275736238738/od6/private/full/cokwr/41677fi0nc'/>

For a quick comparison, let us look at how the same information is represented in the cellsFeed:

# Extract the cellfeed link
irb(main):054:0> cellfeed_uri = \
irb(main):055:0* worksheet_data["entry"][0]["link"][1]["href"]
=> "http://spreadsheets.google.com/feeds/cells/o04927555739056712307.3387874275736238738/od6/private/full"
irb(main):056:0> response = \ 
irb(main):057:0* get_feed(cellfeed_uri, headers)
=> #<Net::HTTPOK 200 OK readbody=true>

# Parse into datastructure and print
irb(main):058:0> cellfeed_doc = \ 
irb(main):059:0* XmlSimple.xml_in(response.body, 'KeyAttr' => 'name')
=> {"totalResults"=>["6"], [ snip ]

irb(main):060:0> pp cellfeed_doc
 "title"=>[{"type"=>"text", "content"=>"Programming language links"}],
    "title"=>[{"type"=>"text", "content"=>"A1"}],
    "content"=>{"type"=>"text", "content"=>"language"},
    [ snip ]

As you can see here, 6 entries are returned, one for each cell. I have cut off all the other output besides the value for cell A1, which contains the word 'language'. Also notice the edit link shown above. This link contains a version string (8srvbs) at the end. The version string is important when updating cell data, as we will do at the end of this article. It makes sure that updates don't get overwritten. Whenever you are making a PUT request to update cell data, you must include the cell's latest version string in your request. A new version string will be returned after each update.

Posting content to the listFeed

The first thing we need in order to post content is the POST link for the listFeed. This link will be returned when the list feed is requested. It will contain the URL http://schemas.google.com/g/2005#post as the value for the rel attribute. You will need to parse this link element and extract its href attribute. First we will create a small method to make posting easier:

irb(main):061:0> def post(uri, data, headers)
irb(main):062:1> uri = URI.parse(uri)
irb(main):063:1> http = Net::HTTP.new(uri.host, uri.port)
irb(main):064:1> return http.post(uri.path, data, headers)
irb(main):065:1> end
=> nil
# Set up our POST url
irb(main):066:0> post_url = \ 
irb(main):067:0* "http://spreadsheets.google.com/feeds/list/o04927555739056712307.3387874275736238738/od6/private/full"
=> "http://spreadsheets.google.com/feeds/list/o04927555739056712307.3387874275736238738/od6/private/full"

# We must use 'application/atom+xml' as MIME type so let's change our headers 
# which were still set to 'application/x-www-form-urlencoded' when we sent our 
# ClientLogin information over https
irb(main):068:0> headers["Content-Type"] = "application/atom+xml"
=> "application/atom+xml"

# Setting up our data to post, using proper namespaces
irb(main):069:0> new_row = \ 
irb(main):070:0* '<atom:entry xmlns:atom="http://www.w3.org/2005/Atom">' << 
irb(main):071:0* '<gsx:language xmlns:gsx="http://schemas.google.com/spreadsheets/2006/extended">' <<
irb(main):072:0* 'ruby</gsx:language>' << 
irb(main):073:0* '<gsx:website xmlns:gsx="http://schemas.google.com/spreadsheets/2006/extended">' <<
irb(main):074:0* 'http://ruby-lang.org</gsx:website>' << 
irb(main):075:0* '</atom:entry>'
=> "<atom:entry xmlns:atom=\"http://www.w3.org/2005/Atom\"><gsx:language ... [ snip ] 

# Performing the post
irb(main):076:0> post_response = post(post_url, new_row, headers) 
=> #<Net::HTTPCreated 201 Created readbody=true>

The 201 status indicates that our post was successful.

Using the cellsFeed to update content

From the documentation we can see that the cells feed prefers PUT requests on existing content. But since the information that we retrieved from the cellsFeed earlier above was only the data that was already in our actual spreadsheet, how can we add new information? We will simply need to make a request for each empty cell into which we want to enter data. The snippet below shows how to retrieve the empty cell R5C1 (Row 5, Column 1) in which we want to insert some information about the Python programming language.

Our original variable cellfeed_uri contained just the URI for the cellfeed itself. We now want to append the cell that we are looking to edit and obtain that cells version string to make our edit:

# Set our query URI
irb(main):077:0> cellfeed_query = cellfeed_uri + '/R5C1'
=> "http://spreadsheets.google.com/feeds/cells/o04927555739056712307.3387874275736238738/od6/private/full/R5C1"

# Request the information to extract the edit link
irb(main):078:0> cellfeed_data = get_feed(cellfeed_query, headers)
=> #<Net::HTTPOK 200 OK readbody=true>
irb(main):079:0> cellfeed_data.body
=> "<?xml version='1.0' encoding='UTF-8'?>
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:gs='http://schemas.google.com/spreadsheets/2006' xmlns:batch='http://schemas.google.com/gdata/batch'>
<category scheme='http://schemas.google.com/spreadsheets/2006' term='http://schemas.google.com/spreadsheets/2006#cell'/>
<title type='text'>A5</title>
<content type='text'>
<link rel='self' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/cells/o04927555739056712307.3387874275736238738/od6/private/full/R5C1'/>
<link rel='edit' type='application/atom+xml' href='http://spreadsheets.google.com/feeds/cells/o04927555739056712307.3387874275736238738/od6/private/full/R5C1/47pc'/>
<gs:cell row='5' col='1' inputValue=''>

As you can see in the code listing above, the version string is 47pc. (You may need to scroll all the way over to the right side.) To make things easier, let us create a convenience method that gets us the version string for any cell that we are interested in:

irb(main):080:0> def get_version_string(uri, headers=nil)
irb(main):081:1> response = get_feed(uri, headers)
irb(main):082:1> require 'rexml/document'
irb(main):083:1> xml = REXML::Document.new response.body
irb(main):084:1> edit_link = REXML::XPath.first(xml, '//[@rel="edit"]')
irb(main):085:1> edit_link_href = edit_link.attribute('href').to_s
irb(main):086:1> return edit_link_href.split(/\//)[10]
irb(main):087:1> end
=> nil

# A quick test
irb(main):088:0> puts get_version_string(cellfeed_query, headers)
=> nil

While we are at it, we may as well write a method to perform the PUT request also, or better yet, let us write a method to perform the entire batch update. Our function is going to take an array of hashes containing the following variables:

  • :batch_id - A unique identifier for each piece of the batch request.
  • :cell_id - The id of the cell to be updated in R#C# format, where cell A1 would be represented as R1C1.
  • :data - The data that we want to insert.

irb(main):088:0> def batch_update(batch_data, cellfeed_uri, headers)
irb(main):089:1> batch_uri = cellfeed_uri + '/batch'
irb(main):090:1> batch_request = <<FEED
irb(main):091:1" <?xml version="1.0" encoding="utf-8"?> \
irb(main):092:1" <feed xmlns="http://www.w3.org/2005/Atom" \
irb(main):093:1" xmlns:batch="http://schemas.google.com/gdata/batch" \
irb(main):094:1" xmlns:gs="http://schemas.google.com/spreadsheets/2006" \
irb(main):095:1" xmlns:gd="http://schemas.google.com/g/2005">
irb(main):096:1" <id>#{cellfeed_uri}</id>
irb(main):097:1" FEED
irb(main):098:1> batch_data.each do |batch_request_data|
irb(main):099:2* version_string = get_version_string(cellfeed_uri + '/' + batch_request_data[:cell_id], headers)
irb(main):100:2> data = batch_request_data[:data]
irb(main):101:2> batch_id = batch_request_data[:batch_id]
irb(main):102:2> cell_id = batch_request_data[:cell_id]
irb(main):103:2> row = batch_request_data[:cell_id][1,1]
irb(main):104:2> column = batch_request_data[:cell_id][3,1]
irb(main):105:2> edit_link = cellfeed_uri + '/' + cell_id + '/' + version_string
irb(main):106:2> batch_request<< <<ENTRY
irb(main):107:2" <entry>
irb(main):108:2" <gs:cell col="#{column}" inputValue="#{data}" row="#{row}"/>
irb(main):109:2" <batch:id>#{batch_id}</batch:id>
irb(main):110:2" <batch:operation type="update" />
irb(main):111:2" <id>#{cellfeed_uri}/#{cell_id}</id>
irb(main):112:2" <link href="#{edit_link}" rel="edit" type="application/atom+xml" />
irb(main):113:2" </entry>
irb(main):114:2" ENTRY
irb(main):115:2> end
irb(main):116:1> batch_request << '</feed>'
irb(main):117:1> return post(batch_uri, batch_request, headers)
irb(main):118:1> end
=> nil

# Our sample batch data to insert information about the Python programming language into our worksheet
irb(main):119:0> batch_data = [ \
irb(main):120:0* {:batch_id => 'A', :cell_id => 'R5C1', :data => 'Python'}, \ 
irb(main):121:0* {:batch_id => 'B', :cell_id => 'R5C2', :data => 'http://python.org' } ]
=> [{:cell_id=>"R5C1", :data=>"Python", :batch_id=>"A"}=>{:cell_id=>"R5C2", :data=>"http://python.org", :batch_id=>"B"}]

# Perform the update
irb(main):122:0> response = batch_update(batch_data, cellfeed_uri, headers)
=> #<Net::HTTPOK 200 OK readbody=true>

# Parse the response.body XML and print it
irb(main):123:0> response_xml = XmlSimple.xml_in(response.body, 'KeyAttr' => 'name')
=> [ snip ]

irb(main):124:0> pp response_xml
{"title"=>[{"type"=>"text", "content"=>"Batch Feed"}],
  [{"status"=>[{"code"=>"200", "reason"=>"Success"}],
     [{"col"=>"1", "row"=>"5", "content"=>"Python", "inputValue"=>"Python"}],
    "title"=>[{"type"=>"text", "content"=>"A5"}],
    "content"=>{"type"=>"text", "content"=>"Python"},
    [ snip ]

As you can see, our batch request was successful as we have received the 200 OK response code. Parsing the response XML, we can see that a separate message is returned for each individual :batch_id that we set in our response_data array. For more information on batch processing, please refer to the Batch Processing in GData documentation.


As you have seen, it is very easy to use Ruby's interactive shell to play around with the Google Data APIs. We were able to access our spreadsheets and worksheets using both the listFeed and cellsFeed. Additionally we have inserted some new data using a POST request and then written methods to perform a batch update with only about 120 lines of code. From this point it should not be too hard to wrap some of these simple methods into classes and build yourself a re-usable framework.

Please join us in the discussion groups if you have any questions about using these tools with your favorite Google Data API.

A class file with the code samples detailed above can be found at http://code.google.com/p/google-data-samples-ruby

Discuss this article!