use curl for api documentation

I’ve been working quite a bit with the rest plugin for Struts2. The really nice thing about this plugin is the way it cleans up Struts URLs. Makes them more rails-like. I chuckled when depressed programmer suggested that struts2 is “WebWork on drugs.” I hate struts2. I really do.

Anyway, I have stripped down an AccountController to show just the POST service. In reality, the create() method is wired to a middle tier service that authenticates username, password pairs then updates session attributes with member id and other bits of persistent session data I need.

// imports omitted

@Results({
  @Result(
    name = "success",
    type = ServletActionRedirectResult.class,
    value = "account")
})
public class AccountController extends ActionSupport
{
  private String username;
  private String password;
  // getters/setters omitted

  public AccountController() { }

  public HttpHeaders index()   { return notImplemented(); }
  public HttpHeaders show()    { return notImplemented(); }
  public HttpHeaders edit()    { return notImplemented(); }
  public HttpHeaders editNew() { return notImplemented(); }
  public HttpHeaders update()  { return notImplemented(); }
  public HttpHeaders destroy() { return notImplemented(); }
  public HttpHeaders create()
  {
    int status = (username.equals("alice")
           && password.equals("restaurant"))
      ? HttpServletResponse.SC_ACCEPTED
      : HttpServletResponse.SC_UNAUTHORIZED;

    return new DefaultHttpHeaders().withStatus(status);
  }

  private DefaultHttpHeaders notImplemented()
  {
    return new DefaultHttpHeaders()
      .withStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
  }

}

Note that I only return HTTP headers; the body content will always be empty.

I have found curl invaluable for documenting the API. This is a simple case but consider a much more complicated system with dozens of URLs and each URL implements many of the HTTP methods (including PUT and DELETE).

Third party developers are the bane of the support engineer. First, few people read documentation. They skim the material and furiously code. When their software fails, they file a bug that the API is broken. Usually, the API isn’t broken; the developer simply did not understand the API.

I subscribe to the agile manifesto value of “working software over comprehensive documentation.” In my work, I have found that a few curl examples clears up most of these issues. For example, to exercise the create() method in the AccountController, simply post a form.

curl                                    \
  --request POST                        \
  --include                             \
  --url "http://ws.example.com/account" \
  --form "username=alice"               \
  --form "password=restaurant"          \
  --cookie-jar "cookies"                \
  --cookie "cookies"

I like to add the “–include” flag as it displays some extra header information. When I get a support call, I have the developer trot out the “documentation” curl examples and open a bash shell. This, of course, drives the Windows guys nuts–to which I reply, “buck up.” We work through the exercise of getting the http request working with the curl example. Then a miracle occurs. The developer now has a working example on their machine from which to re-examine their code.

A final note. The “–cookie-jar” and “–cookie” parameters will handle cookies between the web server and your curl commands. In otherwords, you can login to a website and these parameters will store your authenticated session id in a file. The file in this example is named “cookies” but it can be legal filename. You can then make subsequent calls to URLs, passing the cookies (and, therefore, the session id) back up to the server.

For example, to upload your avatar picture to your new social network, first login using the curl command above. This establishes an authenticated session. Then post your picture using the curl command below, making sure you pass the cookies back up.

curl                                   \
  --request POST                       \
  --include                            \
  --url "http://ws.example.com/avatar" \
  --form "avatar=@somepix.jpg"         \
  --cookie-jar "cookies"               \
  --cookie "cookies"

Finally, if you need to add a description, publish the curl command as part of a bash script. For example,

#!/bin/bash

# 1. you must login before you can upload the avatar
# 2. the web server will reject any avatar exceeding 2MB
# 3. do not forget the '@' symbol, a common mistake
# 4. do not forget to include --cookie and --cookie-jar

curl                                   \
  --request POST                       \
  --include                            \
  --url "http://ws.example.com/avatar" \
  --form "avatar=@somepix.jpg"         \
  --cookie-jar "cookies"               \
  --cookie "cookies"

Good luck!

Software RAID 10

I’ve been putting off building the software RAID10 on marmaduke. Today, I put it off no longer.

The server marmaduke has six storage devices (2 IDE and 4 SATA)

$ ls -1 /dev/hd?
/dev/hde
/dev/hdf

$ ls -1 /dev/sd?
/dev/sda
/dev/sdb
/dev/sdc
/dev/sdd

The CDROM is attached as /dev/hde and a 300GB HDD as /dev/hdf on which I’ve installed CentOS 5.2. The four SATA drives will be used to build a RAID 10. I’ve read through a number of postings on how to build a software RAID. The cleanest, shortest and clearest of them is on tgharold.com.

mknod

First, create a node for the array.

# mknod /dev/md0 b 9 0

I chose md0 since it was available.

The parameter ‘b’ directs mknod to create a block (buffered) special file.

The parameter ’9′ is the Major version. (huh?) Seems the correct parameter can be found in /proc/devices. (see also, centos docs)

$ cat /proc/devices | grep -e "md$"
  9 md

The parameter '0' corresponds to the last digit in the device /dev/md0. tgharold.com points out that the digit used in the device name should be the same as the last parameter. Why? Dunno. Something to look up some day.

fdisk

Second, partition the disks, each and every one.

# fdisk /dev/sda
# fdisk /dev/sdb
# fdisk /dev/sdc
# fdisk /dev/sdd

I don't know why I set the boot flag. The important point is to set the ID to 'fd' which is 'Linux raid autodetect'.

   Device  Boot  Start    End     Blocks  Id  System
/dev/sda1  *         1  60801  488384001  fd  Linux raid autodetect
/dev/sdb1  *         1  36481  293033601  fd  Linux raid autodetect
/dev/sdc1  *         1  36481  293033601  fd  Linux raid autodetect
/dev/sdd1  *         1  36481  293033601  fd  Linux raid autodetect

One of my drives is larger than the others. Three drives (Seagate ST3300620AS) where previously used in a RAID5. It's impossible to find these drives any longer so I picked up the closest match (Seagate ST3500630AS). It has a larger capacity but otherwise the specs match. In a RAID10, the larger drive's extra space (~200GB) will go unused.

I formatted the drives but perhaps it was unnecessary. I did this as a check on each drive before I began building the array. Didn't seem to hurt anything.

# mkfs.ext3 /dev/sda1
# mkfs.ext3 /dev/sda2
# mkfs.ext3 /dev/sda3
# mkfs.ext3 /dev/sda4

mdadm

Third, time to pull the trigger. Let mdadm do the heavy lifting.

# mdadm             \
  --create /dev/md0 \
  -v                \
  --raid-devices=4  \
  --chunk=32        \
  --level=raid10    \
  /dev/sda1 /dev/sdb1 /dev/sdc1 /dev/sdd1

mkfs

Fourth, whether you format the drive individually or not you must format the RAID.

# mkfs.ext3 /dev/md0

mount

Finally, mount the RAID.

# mount /dev/md0 /mnt/xen

Sweet.

$ df -h
Filesystem Size Used Avail Use% Mounted on /dev/mapper/VolGroup00-LogVol00
           287G  36G  237G  14% /
/dev/hdf1   99M  19M   76M  20% /boot
tmpfs      3.9G    0  3.9G   0% /dev/shm
/dev/md0   551G 198M  523G   1% /mnt/xen

Now, where is that Xen tutorial?

RAID 01 vs. RAID 10

I just finished building a four-drive software RAID10 on marmaduke and wanted to jot down my thoughts on RAID failure. In particular, I read a number of postings on the difference between RAID 01 and RAID 10. None of them satisfactory described the differences and how those differences changed when adding more drives.

Marmaduke only has four drives in its array. Most of the web postings dealt with four drives but I also wanted to see the impact on six drives. Here is a hypothetical set of six drives.

Six Drives:
  /dev/sda  a
  /dev/sdb  b
  /dev/sdc  c
  /dev/sdd  d
  /dev/sde  e
  /dev/sdf  f

For clarity, I will refer to /dev/sda simply as ‘a’, and so on.

Recall that RAID 0 ‘stripes’ two or more drives and RAID 1 ‘mirrors’ two drives.

RAID 01 Composition

Four Drive RAID 01
  STRIPE:   a  b     as  0
  STRIPE:   c  d     as  1
  MIRROR:   0  1     as  R01 (RAID 0+1)

Six Drive RAID 01
  STRIPE:   a  b  c  as  0
  STRIPE:   d  e  f  as  1
  MIRROR:   0  1     as  R01 (RAID 0+1)

In both the four and six disk arrays, RAID 01 mirrors two striped arrays. Each striped array can contain two or more drives but there are always only two striped arrays. (A mirror has only two subarrays.)

RAID 10 Composition

Four Drive RAID 10
  MIRROR:   a  b     as  0
  MIRROR:   c  d     as  1
  STRIPE:   0  1     as  R10 (RAID 1+0)

Six Drive RAID 10
  MIRROR:   a  b     as  0
  MIRROR:   c  d     as  1
  MIRROR:   e  f     as  2
  STRIPE:   0  1  2  as  R10 (RAID 1+0)

In both the four and six disk arrays, RAID 10 stripes two or more mirrored raids. Each mirror has exactly two disks.

01 vs. 10

Which is better? Both cost the same in terms of disk drives. Both yield the same final RAID capacity. Performance is (for my purposes) the same.

I conclude that the difference is primarily in the failure rates between the two drives. There are two types of failures.

First is a failure that takes out a drive but not the array. Replace the drive and you can rebuild the array.

Second is a failure that takes out a drive and the array. Nothing you can do. The array is lost. Game over.

Which drive or drive set cause a catastrophic array loss? I’ve created two tables (ah, the beauty of pure 7-bit ASCII) to detail every scenario for both a 4-drive and a 6-drive array of both RAID 01 and RAID 10.

An asterisk denotes a catastrophic failure.

Column 1 : “F”, number of drives that failed in the array.

Column 2 : “DRIVES”, each drive in the array.

Column 3 : “RO1″, subarrays for RAID 01.

Column 4 : “R10″, subarrays for RAID 10.

Column 5 : “RAIDS”, the two final arrays.

            Four Drive Arrays

       DRIVES     R01    R10     RAIDS
    ...........   ...   .....   ...  ...
F   a b c d       0 1   0 1     R01  R10
----------------------------------------
0 |             |     |       |
----------------------------------------
  |       *     |   * |       |
1 |     *       |   * |       |
  |   *         | *   |       |
  | *           | *   |       |
----------------------------------------
  |     * *     |   * |   *   |       *
  |   *   *     | * * |       |  *
2 |   * *       | * * |       |  *
  | *     *     | * * |       |  *
  | *   *       | * * |       |  *
  | * *         | *   | *     |       *
----------------------------------------
  |   * * *     | * * |   *   |  *    *
3 | *   * *     | * * |   *   |  *    *
  | * *   *     | * * | *     |  *    *
  | * * *       | * * | *     |  *    *
----------------------------------------
4 | * * * *     | * * | * *   |  *    *
----------------------------------------

Neither RAID configurations can survive a 3 or 4 drive failure.

Both configurations can survive a 1 drive failure. One of the subarrays in RAID 01 always fail with a single drive failure but it doesn’t bring down the array. In RAID 10, the subarray doesn’t fail because the subarray is a mirror.

With four drives, there are six possible combinations of two drive failures. In this case, RAID 10 has twice the survival rate (two failure points) as does RAID 01 (four failure points).

            Six Drive Arrays

       DRIVES     R01    R10     RAIDS
    ...........   ...   .....   ...  ...
    a b c d e f   0 1   0 1 2   R01  R10
----------------------------------------
0 |             |     |       |
----------------------------------------
  |           * |   * |       |
  |         *   |   * |       |
1 |       *     |   * |       |
  |     *       | *   |       |
  |   *         | *   |       |
  | *           | *   |       |
----------------------------------------
  |         * * |   * |     * |       *
  |       *   * |   * |       |
  |       * *   |   * |       |
  |     *     * | * * |       |  *
  |     *   *   | * * |       |  *
  |     * *     | * * |   *   |  *    *
2 |   *       * | * * |       |  *
  |   *     *   | * * |       |  *
  |   *   *     | * * |       |  *
  |   * *       | *   |       |
  | *         * | * * |       |  *
  | *       *   | * * |       |  *
  | *     *     | * * |       |  *
  | *   *       | *   |       |
  | * *         | *   | *     |       *
----------------------------------------
  |       * * * |   * |     * |       *
  |     *   * * | * * |     * |  *    *
  |     * *   * | * * |   *   |  *    *
  |     * * *   | * * |   *   |  *    *
  |   *     * * | * * |     * |  *    *
  |   *   *   * | * * |       |  *
  |   *   * *   | * * |       |  *
  |   * *     * | * * |       |  *
  |   * *   *   | * * |       |  *
  |   * * *     | * * |   *   |  *    *
3 | *       * * | * * |     * |  *    *
  | *     *   * | * * |       |  *
  | *     * *   | * * |       |  *
  | *   *     * | * * |       |  *
  | *   *   *   | * * |       |  *
  | *   * *     | * * |   *   |  *    *
  | * *       * | * * | *     |  *    *
  | * *     *   | * * | *     |  *    *
  | * *   *     | * * | *     |  *    *
  | * * *       | *   | *     |       *
----------------------------------------
  |     * * * * | * * |   * * |  *    *
  |   *   * * * | * * |     * |  *    *
  |   * *   * * | * * |     * |  *    *
  |   * * *   * | * * |   *   |  *    *
  |   * * * *   | * * |   *   |  *    *
  | *     * * * | * * |     * |  *    *
4 | *   *   * * | * * |     * |  *    *
  | *   * *   * | * * |   *   |  *    *
  | *   * * *   | * * |   *   |  *    *
  | * *     * * | * * | *   * |  *    *
  | * *   *   * | * * | *     |  *    *
  | * *   * *   | * * | *     |  *    *
  | * * *     * | * * | *     |  *    *
  | * * *   *   | * * | *     |  *    *
  | * * * *     | * * | *     |  *    *
----------------------------------------
  |   * * * * * | * * |   * * |  *    *
  | *   * * * * | * * |   * * |  *    *
5 | * *   * * * | * * | *   * |  *    *
  | * * *   * * | * * | *   * |  *    *
  | * * * *   * | * * | * *   |  *    *
  | * * * * *   | * * | * *   |  *    *
----------------------------------------
6 | * * * * * * | * * | * * * |  *    *
----------------------------------------

With a six drive array, RAID 10 has three failure points if two drives fail. However, RAID 01 has nine failure points.

Finally if three drives fail, RAID 10 has 12 failure points compared to RAID 01 which has 18 failure points. In the following table, ‘prm’ is the number of permutations for that number of drive failures.

          RAID Failure Points

        4-drive           6-drive
    ...............   ...............
F   R01   R10   prm   R01   R10   prm
----------------------------------------
0                1                 1
1                4                 6
2    4     2     6     9     3    15
3    4     4     4    18    12    20
4    1     1     1    15    15    15
5    -     -     -     6     6     6
6    -     -     -     1     1     1

It is my conclusion that the likelyhood of a catastrophic array failure is substantially greater for RAID 01 and prudence suggests a preference for RAID 10.

bash progress monitor

I have a remote machine that is used to store and process XML files. Recently, I had need to duplicate a directory of XML files (e.g., cp -r a b). It’s not really germane to the subject here, but this particular server has a whack configuration and I gotta rant before I continue.

The office server (scrappy) has pretty good specs.

[scrappy ~]$ cat /proc/meminfo

MemTotal:      3980800 kB

[scrappy ~]$ cat /proc/cpuinfo

processor   : 0
model name  : Intel(R) Core(TM)2 CPU   6600  @ 2.40GHz
cpu MHz     : 2394.000
cache size  : 4096 KB

processor   : 1
model name  : Intel(R) Core(TM)2 CPU   6600  @ 2.40GHz
cpu MHz     : 2394.000
cache size  : 4096 KB

[scrappy ~]$ cat /proc/scsi/scsi

Attached devices:
Host: scsi0 Channel: 00 Id: 00 Lun: 00
  Vendor: SONY   Model: DVD RW AW-Q170A    Rev: 1.72
  Type:   CD-ROM                           ANSI SCSI revision: 05

[scrappy ~]$ cat /proc/ide/hd?/model

ST3320620AS

Whoa! What’s my SATA drive doing attached to the IDE driver? When I compare to my home CentOS box (marmaduke), I see that its drives are connected differently. Yes, marmaduke has one HDD connected via the IDE driver (ST3320620A) but that drive is a PATA drive. The four SATA drives are connected via SATA drivers. (The SATA drives will be configured as a software RAID 10, stay tuned. There’s a xen project in the making.)

[marmaduke ~]$ cat /proc/scsi/scsi

Attached devices:
Host: scsi2 Channel: 00 Id: 00 Lun: 00
  Vendor: ATA      Model: ST3500630AS      Rev: 3.AA
  Type:   Direct-Access                    ANSI SCSI revision: 05
Host: scsi3 Channel: 00 Id: 00 Lun: 00
  Vendor: ATA      Model: ST3300620AS      Rev: 3.AA
  Type:   Direct-Access                    ANSI SCSI revision: 05
Host: scsi4 Channel: 00 Id: 00 Lun: 00
  Vendor: ATA      Model: ST3300620AS      Rev: 3.AA
  Type:   Direct-Access                    ANSI SCSI revision: 05
Host: scsi5 Channel: 00 Id: 00 Lun: 00
  Vendor: ATA      Model: ST3300620AS      Rev: 3.AA
  Type:   Direct-Access                    ANSI SCSI revision: 05

[marmaduke ~]$ cat /proc/ide/hd?/model

PIONEER DVD-RW DVR-111D
ST3320620A

scrappy was configured before arriving at the office by a friend of a friend who runs a PC shop. “But it was such a deal!” Yeah, right. Bunch of monkeys. How hard is it to configure the BIOS to use the SATA interface rather than the IDE interface?

Anyway, I don’t have time to rebuild scrappy right now so I live with the dismal disk performance. Here’s the problem at hand. I have numerous XML files—some largish and some smallish. I have several sets and each set has about 4000 files.

[scrappy ~]$ ls src/*xml | wc -w

4323

[scrappy ~]$ ls -l src/*xml | sort -n -r -k5

-rw-r--r-- 1 kelly kelly 315804120 Dec 19 15:46 0001.xml
-rw-r--r-- 1 kelly kelly 275651475 Dec 19 17:34 0002.xml
-rw-r--r-- 1 kelly kelly 260250994 Dec 19 16:15 0003.xml
-rw-r--r-- 1 kelly kelly 222402294 Dec 19 16:25 0004.xml
-rw-r--r-- 1 kelly kelly 204642813 Dec 19 15:52 0005.xml
     .
     .
     .
-rw-r--r-- 1 kelly kelly      1467 Dec 19 19:15 4321.xml
-rw-r--r-- 1 kelly kelly      1467 Dec 19 16:01 4322.xml
-rw-r--r-- 1 kelly kelly      1098 Dec 19 19:19 4323.xml

I wanted to duplicate the set of files as I needed to run some prototype code that I didn’t trust to be non-destructive. Simple.

[scrappy ~]$ cp -r src tgt

However, the disk performance is agonizing. So bad that I leave it while I work on another machine. But I want to know the progress and see it as it changes. With six to ten shells open, I want something that can be resized to use minimal screen real estate. I want a quick command line progress monitor.

bash to the rescue. I didn’t want to create a script file so I just jack it right into the terminal’s command line. When you open the while loop, bash will continue on the next line until you close it with the done keyword.

[scrappy ~]$ while 'true'; do
>   ts=`date`
>   src=`ls src/*xml 2>/dev/null | wc -w`
>   tgt=`ls tgt/*xml 2>/dev/null | wc -w`
>   echo -ne "  ${ts}  ${src}  ${tgt}        \r"
>   sleep 1
> done

  Fri Jan  9 15:20:17 PST 2009  4323  2304

Recall we’ve previously covered that 2>/dev/null hides the error message generated by ls if no file is found.

The components are stored in local variables as a matter of convenience and displayed using echo.

echo is passed two switches. The -n switch supresses the trailing newline so that the cursor remains on the same line as the displayed text. The -e switch causes backslashes in the text to be interpreted as the escape character. This is useful since I want to add a trailing carriage return character. This will push the cursor to the beginning of the line while remaining on the same line as the text.

After sleeping for one second, the script generates a new echo output which overwrites the old text. I suppose I could add a test to the script to break when ${src} equals ${tgt}.

I don’t know why disk I/O is so slow on scrappy. Perhaps the mode is set to use programmed I/O rather than DMA. Who knows? Who cares? Both scrappy and marmaduke have Intel ICH8 SATA controllers. scrappy has a faster processor with more cache. Yet, marmaduke smokes on disk throughput on either the SATA or IDE drives. Something is wonky.

I’d like to say that I can ignore the issue. I have way too much going on right now. But it bugs me.

Javascript DOM Listeners

I’ve wanted to build a simple gallery viewer for some time. “But there are thousands of already built viewers,” you may be thinking. Yes there are. But I wanted a simple viewer that was at once trés snappy and minimalist.

I dislike the gallery systems that open another window or create a floating panel which must be dismissed. I wanted a simple picture portal with simple thumbnails. I wanted something that didn’t require jacking javascript or event listeners directly into the html. It had to be easy to use with wordpress.

I wanted to add a title and caption to each picture. Clicking the thumbnail would change out the picture, the title and the caption.

Since I would add galleries to a blog, the code had to handle multiple galleries per page. Therefore, the galleries could use class attributes but not id attributes in the event listeners.

For this project, I wrap each gallery in a <div /> element and give the element a class type of gallery.

<div class="gallery">
  <h3>Photo 1 Title</h3>
  <p><img width="592" height="330" alt="" src="/img/p1.jpg" /></p>
  <div class="gallery-thumbs">
    <img
      width="50" height="50"
      title="Photo 1 Title"
      alt="Photo 1 Caption."
      longdesc="/img/p1.jpg"
      src="/img/t1.jpg"
    /><img
      width="50" height="50"
      title="Photo 2 Title"
      alt="Photo 2 Caption."
      longdesc="/img/p2.jpg"
      src="/img/t2.jpg" />
  </div>
  <p>Photo 1 Caption</p>
</div>

The main parts of the viewer are:

  • <h3 /> element which displays the photo title
  • <p /> element wrapping the <img /> element which is the main photo portal
  • <div /> element with a class of <gallery-thumbs /> which is a container for all the thumbnail images; all thumbnail <img /> elements are placed inside this container
  • <p /> element which displays the photo caption

I chose to make my photo portal 592 by 330 pixels as this fits well with several of my templates. I chose this aspect ratio—which is close to “wide-screen”—as it gives me more room to display a lengthy caption without scrolling. However, any aspect ratio may be used. One limitation of this system is that all photos in a gallery set must have the same width and height dimensions.

The thumbnail <img /> elements use the alt and title attributes as they were intended. I still needed to supply the main photo URL associated with each thumbnail. Since I want my gallery code to validate, I couldn’t simply add a non-html attribute. I pragmatically chose to use the longdesc attribute in a unorthodox manner to declare the main image URL.

I like small thumbnails. Mine are 50 by 50 pixels. Some people are very, very, very adament that thumbnail images must be an exact replica of the original image. Not me. The thumbnail ratio is square regardless of the original photo ratio. I may or may not resize the image before taking a thumbnail. My thumbnails do not even have to be taken from the photo. They could be icons, for all I care. As long as they communicate effectively and are visually appealing.

I add a bit of CSS seasoning to help with presentation. Season your gallery to taste.

div.gallery {
    margin:  0 0 2em 0;
    padding: 0;
    border:  0;
}

div.gallery img {
  margin:  0;
  padding: 0;
  border:  0;
}

div.gallery-thumbs {
  margin:  0;
  padding: 0;
}

div.gallery-thumbs img {
  margin:  4px 8px 0 0;
  padding: 0;
  border:  1px solid black;
}

div.gallery p {
  clear: both;
  margin:  0;
  padding: 0;
}

Before I get into the core javascript functions, let’s look at how we register the event listeners. This is the code that must be added <head /> element of every page that contains a gallery.

<script type="text/javascript">
/*<![CDATA[*/

  function init() {
    registerGalleries();
  }
  window.onload = init;

/*]]>*/
</script>

I initialize the event listeners after the page loads using the window.onload event. While I could have included the init function in an external file, I may want to extend it in the future to register events for other systems. The registerGalleries function is defined in the external gallery javascript file.

<script type="text/javascript" src="js/gallery.js"></script>

Once the window loads, init is called which in turn calls registerGalleries which registers event listeners for any and all galleries on the page.

function registerGalleries() {

  // get every element in the document
  var allTags = document.getElementsByTagName("*");

  // check each element to see if it's a
  // div element and gallery class
  for (var i = 0; i < allTags.length; i++) {
    if (allTags[i].tagName == "DIV") {
      if (allTags[i].className == "gallery") {

        // it is a gallery, so get all the subelements
        var galleryTags = allTags[i].childNodes;

        // check each subelement for the gallery-thumbs container
        for (var j = 0; j < galleryTags.length; j++) {
          if (galleryTags[j].className == "gallery-thumbs") {

            // it is a gallery-thumbs, so get all the subelements
            var thumbTags = galleryTags[j].childNodes;

            // and register event listeners on all thumbnails
            registerThumbs(thumbTags);
          }
        }
      }
    }
  }
}

The registerThumbs function looks for the image tags within gallery-thumbs container.

function registerThumbs(thumbs) {
  for (var i = 0; i < thumbs.length; i++) {

    // the gallery-thumbs container has more than just <img />
    // elements. The whitespace between each <img /> element
    // is also a node. So, we must check for the actual thumbnail
    // code and add the event listener to that.
    if (thumbs[i].tagName == "IMG") {
      addListener(thumbs[i], "click", updateGallery);
    }
  }
}

Attaching events using javascript and DOM differs between Microsoft and the rest of the world. {{sign}} The following code has worked well for me on numerous projects.

function addListener(element, event, process) {
  if ( element.addEventListener ) {
    element.addEventListener(event, process, false);
  }
  else if ( element.attachEvent ) {
    element.attachEvent("on"+event, process);
  }
}

From this point on, the gallery system gets brittle. The javascript depends on a specific node order for node traversal. If the node order changes, you must modify the javascript accordingly. Perhaps one day I will make the code node-order independent. The gist of updateGallery is to get the thumbnail <img /> element from the event, extract the contextually relevant information from that element, traverse the nodes in the gallery container to find the title, picture portal and caption elements and update those elements with the new information.

function updateGallery(e) {
  thumb = e.target ? e.target : e.srcElement;

  var thumbTtl = thumb.getAttribute("TITLE");
  var thumbAlt = thumb.getAttribute("ALT");
  var thumbLnk = thumb.getAttribute("LONGDESC");

  var thumbTxt = thumb.parentNode
                      .nextSibling
                      .nextSibling;
  var thumbPix = thumb.parentNode
                      .previousSibling
                      .previousSibling
                      .firstChild;
  var thumbCap = thumb.parentNode
                      .previousSibling
                      .previousSibling
                      .previousSibling
                      .previousSibling;

  thumbPix.src = thumbLnk;
  thumbTxt.firstChild.nodeValue = thumbAlt;
  thumbCap.firstChild.nodeValue = thumbTtl;
}

That’s it. Below is an example that I created for this project. It uses ten Scandinavian photos from a stock image/clipart DVD I got years ago.

I found that when I entered the gallery code into wordpress, wordpress added <br /> elements where I didn’t want them. This would break the layout (duh!). Originally, I didn’t have a paragraph element wrapping the main image. WordPress insisted on adding the tag. Likewise, I originally had each thumbnail begin on a new line. WordPress insisted on adding a break element so I ended up butting the end of one thumbnail image element against the next (no white space).

Just for grins, I added another gallery from the same clipart collection. This time from India.

I’m pleased with the results. On to the next project.

Peace on Earth

This Christmas is good. So much of the year was absolutely lousy. I’m glad it’s ending on the upbeat. There are more than a few painful memories in 2008.

2009 Harley-Davidson FXDL Dyna Low Rider

A new Harley goes a long way towards easing a painful memory. And my in-laws Julie and Bob have come through with the goods: raffle tickets for a 2009 Harley Dyna Low Rider.

2009 Harley-Davidson FXDL Dyna Low Rider

The kids sent me two raffle tickets last year for a 2008 Harley Fat Bob. Somehow, I didn’t win. There’s a happy Harley mom out there somewhere. Good for her.

This year, my chances of winning have increased as I have THREE tickets, not just two. That’s half again as many tickets. You don’t have to be a statistician to understand that three tickets improve your odds over just two. Just that much closer to the open road.

I really haven’t been in the holiday spirit. My spirits are now a little sunnier. Not simply because I anticipate taking delivery of my new Harley in March. But that two good people, solid people, honest people from the heartland of America remembered me on this day. I’m touched. I miss them.

My spirits are sunnier today. I join with millions around the world with the sentiment, “May there be peace on earth and goodwill towards men.”