Software Development » Feature

Write Ruby, Be Happy!

Monday, April 15th, 2002 RubyCover45

Every single day, we're bombarded by commercials telling us that a particular product will make us happy. Just buy these jeans, drink this soft drink, or drive this car and you'll be happy and attractive. We can't promise it will increase your sex appeal, but we have found something that will make you happy about writing code again. It's a new programming language from Japan called "Ruby."

Using Ruby will make you feel good about programming. People smile during hands-on Ruby tutorials when they're working on the exercises. They even write to the mailing list just to say that Ruby makes them feel good.

Why is this? There are plenty of technical things to like about the language. Ruby is concise, with a simple syntax and grammar. It's both high-level and close to the machine, so you can get a lot done with a remarkably short program. It's totally object-oriented; everything is an object (or can be made into an object), and it was designed that way from the start. Like Smalltalk, Ruby's variables are not typed, but the language is strongly typed. It's dynamic; you can extend all classes, including the built-in ones, at runtime, and its eval() method lets you incrementally compile code into a running program. Its garbage collection makes coding less of a crapshoot, and the simple yet flexible exception scheme makes it easy to structure your error handling. And when you have to interface to other libraries, it is simple to write Ruby interfaces in C.

Ruby's object orientation is fairly unusual. Yes, it has classes and objects, but it also supports mixins (think Java interfaces that can have code in them) and singleton classes and singleton objects (so you can specialize whole classes, or just single instances of a class). After a while, you'll find yourself designing programs slightly differently, as the extra flexibility lets you express your ideas more naturally.

Ruby also has appealing social aspects. Ruby's been around Japan since the mid-90s and is wildly popular over there. However, Ruby has only recently started to make a splash outside Japan. The western Ruby community is still growing, so it's easy for newcomers to make a significant contribution. Perhaps that's why the Ruby community is widely recognized for its enthusiasm and friendly, supportive nature.

But there's something more. Like a woodworker's favorite hand tool, Ruby just fits. It's a very human-oriented language that blends well with your intuition. Things just tend to work. The Principle of Least Surprise is a major design goal of Ruby. And amazingly, considering Ruby combines features from Perl and Smalltalk (plus CLU, Python, and others), Ruby meets that goal. Ruby is small enough to be learned in a day and deep enough to provide years of fun. Programs in Ruby just seem to flow, and they have a nasty habit of working the first time.

Let's see what all the fuss is about.

Installing Ruby

Ruby is available for download from http://www.ruby-lang.org/. There you'll find a tarball of the current stable version (recommended if you're a first-time user). You'll also find instructions for downloading the latest development version using CVS.

Assuming you downloaded the tarball (it's less than a megabyte), installation is pretty standard. Unpack the tar file, then build Ruby by using the usual open source incantation: ./configure; make; make test. If the tests pass, you then run make install as root.

Let's Code!

The first rule of writing about languages is that all examples must start with a "Hello, World!" program, so:

puts "Hello, World. Time: #{Time.now}"

Ruby has a built-in method called puts(). It writes its argument to standard output and appends a newline if there isn't one there already. The surprising thing here might be the #{Time.now} stuff. If Ruby finds the construct #{expr} in double quoted strings, it evaluates the enclosed expression, converts the result to a string, and substitutes that back into the original.

The expression can be anything, from a simple variable reference up to a sizable chunk of code. In this case, the expression is a call to the now() method of the class Time, which returns a new Time object initialized to the current time. When converted to a string, time objects look comfortingly familiar, so if we ran this code, the puts() method would send the string to standard output and we'd see output like the following:

Hello, World. Time: Thu Nov 15 14:02:51 CST 2001

So how do you run this code? Ruby normally runs programs from files, so you could type the code into a file, say hello.rb, and run it with ruby hello.rb. Ruby also works with shebang lines, so you could make your script executable using chmod +x hello.rb, add the #! line, and run it by just giving its name:

#!/usr/local/bin/ruby
puts "Hello, World. Time: #{Time.now}"

You can pass Ruby the code using the -e command-line option, but quoting can get tricky:

ruby -e  'puts "Hello, World. Time: #{Time.now}"'

Ruby also comes with irb, a tool that lets you enter and run code interactively. With irb you don't need the puts to see the result, as irb automatically displays the values of expressions as it evaluates them. irb is a great tool for experimenting with Ruby.

$ irb
irb(main):001:0> "Hello, World. Time: #{Time.now}"
Hello, World. Time: Thu Nov 15 14:18:55 CST 2001
irb(main):002:0>

Now let's wrap our greeting in a method, starting with the def keyword and ending, appropriately enough, with an end. (Ruby calls them methods, while other languages call them functions, procedures, or subroutines). At the same time, let's parameterize it, allowing us to change the name we output. We'll test this by calling our method twice using different names as parameters. As we already know how to substitute values in to strings using the #{...} notation, this is a breeze:

def say_hello(name)
   puts "Hello, #{name}!"
end

say_hello "Larry"
say_hello("Moe")

Running this program gives us the output:

Hello, Larry!
Hello, Moe!

As the code shows, Ruby doesn't insist on parentheses around the parameters to methods, but it's generally a good idea in all but the simplest cases.

Collections and Control Structures

In addition to strings and numbers, Ruby has a large cast of other built-in classes. Arrays are simple collections of arbitrary objects (for example, you can have an array containing a string, a number, and another array). An array literal is a list of objects surrounded by square brackets. Arrays (and other collections) are conveniently traversed using a for loop. In the code that follows, the variable i will contain, in turn, each element in the array. Like method definitions, the for loop is terminated with end.

my_array = [ 42, "Hello, world!", [1,2,3] ]

for i in my_array
   puts i
end

This program outputs:

42
Hello, world!
[ 1, 2, 3 ]

Arrays in Ruby are a surprisingly general data structure; you'll often see them used as stacks, queues, and dequeues (and sometimes even as arrays).

Like most scripting languages, Ruby comes with built-in hashes (or to give them their Latin name, associative arrays). Hashes are collections that are indexed just like arrays, but the thing you index with (often called the key) can be just about any object, not just integers. In Ruby, each hash can contain keys of different types, and the values they map to can also be different types. Hash literals are written between braces, with "=>" between the keys and values:

my_hash = {
   99 => "balloons",
   "seventy-six" => "trombones"
}

puts my_hash[99]
puts my_hash[76+23]
puts my_hash["seventy-six"]

This program produces the output:

balloons
balloons
trombones

If there isn't an entry corresponding to the key you use to index a hash, the special object nil is returned. The nil object is a bit like Perl's undef and the null value in relational databases; it represents the idea of "no value." No other value equals nil, and nil is equivalent to false in contexts requiring a truth value. This lets you write code like the following.

currency_of = {
   "us" => "dollar",
   "uk" => "pound",
   "mx" => "peso"
}

while line = gets
   line.chomp!
   currency = currency_of[line]
   if currency
      puts "Currency of the #{line} is #{currency}"
   else
      puts "Don't know the currency of #{line}"
   end
end

The while loop reads successive lines from standard input using the gets() method. When gets() reaches end of file it returns nil, so the condition of the while loop becomes false and the loop terminates.

Inside the body of the loop, we first strip the trailing newline from the input using chomp!. We then use the result to index the hash, mapping a country to a currency. If we find a match, we report it. However, if we don't find a match, nil will be returned, and we'll output the "Don't know..." message instead.

As you can see from the previous example, Ruby has the normal control structures if and while, along with their negated cousins unless and until, all terminated with end.

Ruby also has a marvelous case statement that works on just about any data type there is (including those that you define yourself). A case statement can check a value against a range, a string, a regular expression, the value's type, and so on.

print "Test score: "
score = gets.to_i

case score
when 0...40
   puts "Back to flipping burgers"
when 40...60
   puts "Squeaked in"
when 60...80
   puts "Solid performance (yawn)"
when 80...95
   puts "Wow!"
when 95..100
   puts "We are not worthy"
else
   puts "Huh?"
end

This example also shows Ruby's ranges. The input value is converted to an integer via the to_i() method, then compared against the various ranges in the case statement. The three dot form, a...b, denotes the values starting from a up to but not including b. The two-dot form is inclusive (a up to and including b). Ranges work on any types where it makes sense, so "a".."z" is equivalent to the lower case ASCII letters. It's easy to write your classes so that they can be used in ranges too.

Top of the Class

Way back when we started, we claimed that Ruby is a soup-to-nuts, object-oriented language. But we've seen all this code, and not an object in sight. What's up?

This is one of the clever things about Ruby. Everything is an object, but if you don't want to do object-oriented programming, you don't have to. Behind the scenes, Ruby handles all this for you.

Object-Oriented Programming

In procedural languages such as C, you have data (ints, chars, and so on) and write code to operate on that data.

In object-oriented programming, data and code are unified. Typically, you write something called a class, which encapsulates some behavior and the data needed to support that behavior. Sounds scary, but it's actually pretty natural. For example, you might be writing an application that draws shapes on the screen. For each shape, you'd need to record things like its color and its present position. These are attributes, the data on which a shape works. You also want behaviors, things like drawOn(screen) and moveTo(x,y). In object-oriented programming, you'd wrap the data and the behavior together into a class definition. When you need a particular shape, you'd create an instance of that class; if you needed ten shapes, you'd have ten separate instances in your running program. These instances are also called "objects."

Languages such as Java and C++ are hybrids. You can create classes in them and then manipulate objects instantiated from these classes. However, the basic built-in types like numbers are not objects; they aren't derived from any particular class. This can be awkward; in Java, every time you want to put a number into a collection you have to wrap it up inside an object (because collections only work with objects). It also means the programmer has to know two different styles of coding, one based on asking objects to execute their behaviors, the other based on conventional non-object semantics.

Ruby is different because everything you manipulate in Ruby is an object. As with Java and C++, you can create your own classes and objects, and you can use the classes supplied with the language. In addition, the number '1' is also a full-blown object (it's an instance of class Fixnum). This is very convenient, because it means that there's no programming divide between objects and non-objects; everything can be manipulated in the same way. If you want to populate a collection with two numbers and a String, you can do it. And when you write 1 + 2 in Ruby, you're not using some magic in the compiler that knows how to add numbers. Instead you're invoking behaviors in objects. In this case you're asking the object '1' to perform addition, passing it the object '2'. The result is a new object (hopefully the Fixnum '3').

Object-orientation is a powerful way of thinking about problems. You design by breaking the world into classes, each with its own responsibilities, and then let objects of those classes interact. The resulting code can be a lot easier to understand and maintain, as behaviors are all neatly encapsulated within classes. However, object-orientation is not a universal solution, and sometimes other paradigms work better. This is where Java can be a pain; you must write in terms of classes, even if they aren't appropriate to your solution. Ruby is flexible; although it is a truly object-oriented language, it doesn't force you to write you code using classes.

When you write line.chomp!, you're actually telling Ruby to execute the chomp! method in the object referenced by the variable line. When you write my_array[1], you're invoking a method called "[]" on the array object that has been referenced by my_array.

Not even arithmetic escapes. When you write 1 + 2 * 3, you've actually created three objects, 1, 2, and 3, of type Fixnum. (Fixnum is used to represent integers less than a machine-dependent limit, normally 30 bits. When integers get bigger than this, Ruby automatically converts them to Bignums, whose size is limited only by the amount of memory in your box.) Ruby performs the multiplication by calling 2's multiply method (conveniently called "*"), passing 3 as a parameter. This returns a new object, the Fixnum 6, which is passed as a parameter to the plus method of the 1 object. In fact, you can make this explicit in Ruby: type the following into irb and you'll get seven as a result:

1.+(2.*(3))

This style of calling a method in an object by writing object.method will be familiar to Java and C# programmers. What isn't so familiar is that everything in Ruby is an object; there are no special cases for numbers as in Java. That's why you can say 1.plus(2):

puts "cat".length
a = [ "c", "a", "b" ]
a.sort!
puts a
puts a.include? "f"

As the above code shows, method names can end with exclamation marks and question marks. The built-in classes typically reserve names that end in "!" for methods that have a potentially surprising side-effect (such as modifying the array), while those ending "?" are typically used when querying an object's state.

This would output:

3
[ "a", "b", "c" ]
false

Creating classes in Ruby is as simple as creating methods. The code in Listing One creates a class called Person, containing two methods, initialize() and to_s(). The initialize() method is special; Ruby calls it to initialize newly created instances of a class. In this example, the initialize method copies the values of its two parameters into the instance variables @name and @age. (Instance variables, sometimes called member variables, are values associated with particular instances of a class, and are prefixed with the "@" sign.) On line 11 we create a new Person object, passing in the string "Dora" and the number 31. The resulting object has its @name and @age instance variables set to "Dora" and 31. We assign it to the variable p1. The to_s() method lets us verify this, returning a string representation of the object. On line 14 we use this to output Dora's information:

Dora, age: 31

Listing One: The Person Class

1   class Person
2      def initialize(name, age)
3         @name = name
4         @age  = age
5      end
6      def to_s
7         "#{@name}, age: #{@age}"
8      end
9   end
10
11  p1 = Person.new("Dora",  31)
12  p2 = Person.new("Flora", 42)
13
14  puts p1.to_s
15  puts p2
16  puts "#{p1} and #{p2}"

We're using a little built-in magic on lines 15 and 16. The puts() method needs a String to send to standard output. Since p2 is not a String, it calls p2's to_s() method to convert it into one (just like toString() in Java). Similarly, the expressions that are inside the #{expr} constructs in the string literal are automatically converted to strings as they are interpolated, calling the to_s() method in class Person each time.

So, these two lines will end up producing:

Flora, age: 42
Dora, age: 31 and Flora, age: 42

Iterators and Blocks

Say you have a chunk of code you want to execute three times. In C, you might write something like:

for (int i = 0; i < 3; i++) {
   printf("Ho! ");
}
printf("Merry Christmas\n");

In Ruby, you'd probably use the method times(), which is an iterator:

3.times { print "Ho! " }
puts "Merry Christmas"

The code between the braces is called a block. (This is confusing terminology, because it looks just like a C or Java block, but it's behavior is totally different.) A block is simply a chunk of code between the keywords do and end, or between curly braces as above. In many ways, blocks are like anonymous methods; the code they contain is called by the iterator method. In this example, the code in the block (print "Ho! ") is executed three times by the iterator times(), which is a method defined for all integers.

As with regular methods, blocks can take parameters. Unlike methods, parameters to a block appear between vertical bars. In the following example, the iterator method each() calls the block four different times, passing in each element of the array in turn. The block parameter item now receives the element, which is then printed to standard out:

array = [ 1, "cat", 3.14159, 99 ]
array.each do |item|
   puts item
end

The previous code shows an important use of iterators. We could have written it as:

array = [ 1, "cat", 3.14159, 99 ]
for i in 0...array.length
   puts array[i]
end

This would seem completely natural to an experienced C or Java programmer, but using the collection's built-in iterator is better style; that way, you are making fewer assumptions about the internal representation of the object.

For example, file objects also have iterators. A file object's each() iterator returns the file's contents line by line. The code in the following example, therefore, will print out the contents of the file names.lst to standard out.

people = File.open("names.lst")
people.each do |item|
   puts item
end

See how the loop looks nearly identical to the array example. We have a generic looping construct that doesn't care if it's iterating over arrays, files, messages in an e-mail inbox, winning numbers in the lottery, or as this next example shows, the names of files in a directory.

Dir.open("/tmp").each do |file_name|
   puts file_name
end

In fact, just about all Ruby objects that can contain collections of other objects implement iterators. Powerful mojo! However, because many people like their for loops, Ruby has a little bit more internal magic. When you write a for loop that looks like the following:

for item in thing
   puts item
end

Ruby translates it into a series of calls to thing.each(). This means that if you write a class that supports an each() iterator, you can use it in a for loop.

So how do you write your own iterator method? It turns out to be pretty simple. An iterator is just a regular method that uses the yield statement to pass values out to a block. The iterator method squares() in the next example returns the squares of the numbers one to limit:

def squares(limit)
   1.upto(limit) do |i|
      yield i*i
   end
end

squares(4) do |result|
   puts result
end

So, what happens when we run this code? First, when Ruby sees squares(4), it calls the method squares(), setting the parameter limit to 4. Inside the method, we loop from 1 to the value of limit using upto, yet another iterator method available to integers.

Each time that Ruby goes around the loop, it executes the yield statement, passing it the value of the loop counter squared as a parameter. Each time the yield is executed, the block associated with the call to squares() is also executed. The parameter to yield is passed to this block, which then prints it. So what is the result of all this? The program outputs:

1
4
9
16

Automated Ego Trip

Having a book published uses up far too much time. But, it isn't just the amount of time spent writing; the real waste of time comes after the book is published; you fritter away your life going to Amazon.com and checking your book's ranking (every five minutes, all day, every day).

So, let's get a machine to waste its time instead. We'll write a simple Ruby script that goes to a set of Amazon pages, extracts the current sales rank, and tabulates the results. Since we want to minimize the delay in getting the results, we'll fetch these pages in parallel, using Ruby's multi-threading capabilities. The code is shown in Listing Two .

Listing Two: Collecting Statistics from Amazon

1   def get_rank_for(url)
2
3      data = 'lynx -dump #{url}'
4
5      return $1 if data =~ /Amazon.com Sales Rank:\s*([0-9,]+)/
6      return $1 if data =~ /Amazon.co.jp.*?:.*?\n([0-9,]+)/
7
8      raise "Couldn't find sales rank in page"
9   end
10
11  URLS = [
12     "http://www.amazon.com/exec/obidos/ASIN/020161622X/o",
13     "http://www.amazon.com/exec/obidos/ASIN/0201710897",
14     "http://www.amazon.co.jp/exec/obidos/ASIN/0201710897",
15     "http://www.amazon.co.jp/exec/obidos/ASIN/4894714531",
16     "http://www.amazon.co.jp/exec/obidos/ASIN/4894712741",
17  ]
18
19  threads = URLS.collect do |url|
20     Thread.new(url) do |a_url|
21        get_rank_for(a_url)
22     end
23  end
24
25  ranks = threads.collect {|t| t.value }
26
27  print  Time.now.strftime("%Y/%m/%d %H:%M ")
28  ranks.each  {|r| printf("%7s", r) }
29  puts

Lines 1 to 9 of the program define the method get_rank_ for() that fetches the sales rank from a Web page. Although we could have used the Ruby Web libraries to perform the search, the code would have been longer, as Amazon does a fair amount of redirecting between pages. (Those interested can look at Listing Three , which does the page fetching this way.) Instead, in this example we were pragmatic and used the lynx program to fetch the page and return its contents as text. The backticks on line 3 of our code run an external program and return its output as a string.

Listing Three: Improved Amazon Statistics Collector

#!/usr/bin/eval ruby -w
require 'net/http'
require 'uri'

URLS = [
   ["www.amazon.com",   "/exec/obidos/ASIN/020161622X/o" ],
   ["www.amazon.com",   "/exec/obidos/ASIN/0201710897"   ],
   ["www.amazon.co.jp", "/exec/obidos/ASIN/0201710897"   ],
   ["www.amazon.co.jp", "/exec/obidos/ASIN/4894714531"   ],
   ["www.amazon.co.jp", "/exec/obidos/ASIN/4894712741"   ]
]

# Use HTTP to fetch the page. Most of the code is redirect handling

def get_rank_for(host, path, port=80)

   loop do
      http = Net::HTTP.new(host, port)
      resp = http.get2(path)

      case resp.code

      when "200"
         data  = resp.body

         return $1 if data =~ /Amazon.com Sales 
            Rank:.*?\n?([0-9,]+)/
         return $1 if data =~ /Amazon.co.jp.*?:.*?\n([0-9,]+)/

         raise "Couldn't find sales rank in page"

      when "301", "302", "303"    # retry

         uri = URI.parse(resp['location'])

         host     = uri.host if uri.host
         port     = uri.port if uri.port
         new_path = uri.path if uri.path

         if new_path[0,1] == '/'
            path = new_path
         else
            path = File.join(File.dirname(path), path)
         end

         http.finish if http.active?

      else
         raise "Error #{resp.code} from #{host}#{path}"
      end
   end
end

# Fire off a thread for each request
threads = URLS.collect do |url|
   Thread.new(*url) do |*a_url|
      get_rank_for(*url)
   end
end

# Collect the results as the threads finish
ranks = threads.collect {|t| t.value }

print  Time.now.strftime("%Y/%m/%d %H:%M ")
ranks.each  {|r| printf("%7s", r) }
puts

Lines 5 and 6 then search the page for the sales rank. The first test is for the U.S. pages. It uses a regular expression to look for the text "Amazon.com Sales Rank:" followed by zero or more spaces and then one or more digits and commas. Because the "digits and commas" part of the regular expression is in parentheses, the text it matches is extracted and stored in the variable $1. The method returns this value if the regular expression matches. Line 6 does the same thing with the Japanese pages (which have a different format). If neither match, we use raise to raise an exception, causing the program to exit.

Lines 11 through 17 initialize an array with the list of URLs to search. We could simply search these sequentially to return a sales rank from each, but that means that we'd only start fetching the fifth page after we'd finished processing the first four. When you're hungry for sales ranks, that delay seems to go on forever. Instead, we spiced up this example by using Ruby's threads. These allow us to run the same chunk of Ruby code many times in parallel. We do this in lines 19 through 23, kicking off a separate thread for each URL in the list. The way we do this is slightly tricky, and we'll look at it in a second.

Line 25 waits for each of the threads to finish executing and collects the return value of each, a sales rank (again, we'll explain how this works shortly). Finally, lines 27 through 29 format the current time and write it out, along with all the sales ranks.

So what's going on in lines 19 through 23? The problem we're trying to solve has two parts. First, we want to start a thread for each URL. However, we also need to remember the thread object that is created, because we'll want to ask it to give us back the sale rank it fetched. So, given a list of URLs, we'd like to end up with a list of threads. Fortunately, Ruby collections have a method that helps us. The collect() method (also known as map()) takes the values in a collection and passes each in turn to a block. It then collects the values returned by that block (which is the value of the last statement executed in the block) to produce a new array. For example, the following code would output 1, 4, 9, and 16.

numbers = [ 1, 2, 3, 4]
squares = numbers.collect {|n| n*n}
puts squares

Notice we've used the alternate form of defining a block, a pair of curly braces, rather than a do/end combination.

In our sales rank example, we use collect() to convert an array of URLs into an array of thread objects, because the value of the block following the collect() is the value of Thread.new(), a new thread. What does that thread do? It calls get_rank_for(), fetching the sales rank for one URL. There's a subtlety in this code; we have to pass the URL in to the thread as a parameter, otherwise there's a potential race condition.

This chunk of code starts all the threads running in parallel, but how do we wait for them to finish, and how do we collect the result each has returned? Well, again we have a problem that looks like "given a collection of x, we need a collection of y." In this case, x is a thread and y is that thread's results. The method value() waits for a thread to finish and then returns its value. Putting this in a collect() block (line 25) converts our list of threads into a list of sales ranks.

What's Next?

In this short article we've only just scratched the surface of what you can do with Ruby. We haven't looked at the networking classes, the Web stuff, XML and SOAP support, database access, GUI interfaces, or any of the other libraries and extensions that make Ruby a serious player as a scripting and general-purpose programming language.

Learning Ruby is simple and rewarding. Why not download and install a copy today? The Resources sidebar has details of where to find both Ruby and other online resources (including the full text of our book). Try Ruby for your next programming or scripting job. It'll make you happy.

Resources

Download:

The latest version of Ruby can be downloaded from http://www.ruby-lang.org/en/download.html. You can also get it via CVS and CVSUP. Details are on the site.

Community:

The English-language mailing list is ruby-talk. For information on subscribing, see http://www.ruby-lang.org/en/ml.html/. The newsgroup comp.lang.ruby is mirrored to this list. You can also chat with Ruby users on the #ruby-lang IRC channel on OpenProjects.net.

Books:

  • Programming Ruby: The Pragmatic Programmer's Guide by David Thomas and Andrew Hunt, ISBN 0-201-71089-7
  • Ruby Developer's Guide by Michael Neumann, Robert Feldt, and Lyle Johnson, ISBN 1928994644
  • Teach Yourself Ruby in 21 Days by Mark Slagell, ISBN 0672322528
  • Ruby in a Nutshell by Matsumoto Yukihiro, ISBN 0596002149
  • Programmieren mit Ruby by Armin Röehrl, Stefan Schmiedl, and Clemens Wyss, ISBN 3898641511

Dave Thomas and Andy Hunt are authors of Programming Ruby and The Pragmatic Programmer . They run an independent consultancy from offices in Dallas, TX and Raleigh, NC. You can reach them via www.pragmaticprogrammer.com.

No Comments on “Write Ruby, Be Happy!”

Comments are closed.

Archived Webinars

Email Newsletters

  • Linux Mag Weekly
  • Linux Mag PR Daily
  • Linux Mag Webinar Update
  • Linux Mag White Paper Update
  • Linux Mag Case Study Update
  • Linux Mag Product Update
  • Email Address: