MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Applying "Digital Hub" Concepts to Enterprise Software Design, Part 4

by Adam Behringer
07/30/2004

The "hub" of most enterprise software systems is a database. However, getting the information from the database to all of the applications that need it and then back again can be a challenge. It becomes particularly challenging if those applications are written in different computer languages or run on different operating systems.

XML is a fantastic technology for transferring data in a heterogeneous environment. Apple's WebObjects technology can be used to handle the conversions from database to XML to database so that any applications or scripts that can parse XML can participate in the system. This article demonstrates a WebObjects application that can handle this data conversion. It builds on a scenario described previously where scientists and students from all over the globe are collaborating on a huge set of weather data.

Prerequisites

If you're going to build the project in this article, you'll first need to complete the steps in part two of this series. You will also need to find the Weather.eomodeld file that you created in that tutorial. Of course, I recommend reading every article in the series, as each one builds on the concepts presented in previous articles.

Connect the Dots

We are going to take the database that we built in part two, and the XML that we designed in part three, and connect them. We want to be able to extract data from the database in XML format and to be able to get data from an XML file to add it to a database. We are going to use WebObjects to do this. WebObjects has some great technologies that handle a lot of the detail for us. Let's dive right in and I will show you.

Here is the XML template from part three that we are going to use for our design:


<?xml version="1.0" encoding="UTF-8"?>
<weather>
  <measureTypes>
    <type id="0" name="Temperature" dataType="number" />
    <type id="1" name="Wind Speed" dataType="number" />
  </measureTypes>
  <data>
    <item id="0" time="2004-04-17 00:00:00 -0700"
            typeId="0" data="60" />
    <item id="1" time="2004-04-17 00:00:00 -0700"
            typeId="1" data="3.2" />
  </data>
</weather>

Getting Started

  1. Launch Xcode and choose New Project... from the File menu.

  2. Choose WebObjects Application as the project type and click Next.

    Choose "WebObjects Application" as the project type and click Next.

  3. Name the project "WeatherHub" and click Next.

  4. When prompted with the J2EE options, click Next.

  5. On the Web Service Support panel, check the box labeled "Add Web service support" and click Next.

  6. On the EOAdaptor panel, JavaJDBCAdaptor.framework should be checked. Click Next.

  7. Leave the Frameworks set to their defaults and click Next.

  8. On the Choose EOModels page, add the model that we created in part two. It is named Weather.eomodeld. Click Finish.

  9. To make sure that WebObjects and your database are working, press Apple-R to build and run your project.

  10. The application should initialize and launch a blank web page in your browser. If it does, go ahead and stop the application by pressing the stop sign icon in the Xcode toolbar. If there is an error, please fix it before continuing. Make certain that your license keys are up to date for WebObjects and Openbase, and make sure that you properly set up the database according to the instructions in part two.

Now you have the basic skeleton of your application. Next we are going to create a class that will be the heart of our application. It will contain the code for moving information from XML to database and back.

  1. Right click (or Ctrl-click) the Classes folder in the left panel of your project (you may need to expand the WeatherHub group) and choose Add -> New File...

  2. Under the WebObjects section, choose the "Java Class" type and click Next.

    Under the WebObjects section, choose the "Java Class" type and click Next.

  3. Name the file XML.java and check the box next to Application Server in the Targets selector. Make sure that none of the other targets are selected and click Finish.

    Name the file XML.java

Now that you have created a new class, add a method to it called buildCompleteXml by copying the following code into your class:


import com.webobjects.foundation.*;
import com.webobjects.eocontrol.*;
import com.webobjects.eoaccess.EOUtilities;

public class XML {

  public static String buildCompleteXml() {
    StringBuffer xmlString = new StringBuffer();
    EOEditingContext editingContext= new EOEditingContext();
    xmlString.append("<?xml version=\"1.0\"
    xmlString.append("encoding=\"UTF-8\"?>\n");
    xmlString.append("<weather><measureTypes>");

    // get measureTypes from database
    EOFetchSpecification measureTypesFS = new
    EOFetchSpecification("MeasurementType",null, null);
    NSArray measureTypes =
    new NSArray(editingContext.objectsWithFetchSpecification(measureTypesFS));
    // for each one found, add it to the XML
    for (int i=0; i < measureTypes.count(); i++) {
      EOGenericRecord currentType = 
      (EOGenericRecord)measureTypes.objectAtIndex(i);
      xmlString.append( XML.buildTypeNode(currentType));
    }
        
    xmlString.append("</measureTypes><data>");
    // get weatherData from database
    EOFetchSpecification measuredDataFS =
    new EOFetchSpecification("MeasuredData", null, null);
    NSArray weatherData =
    new NSArray(editingContext.objectsWithFetchSpecification(measuredDataFS));
    //for each one found, add it to the XML
    for (int i=0; i < weatherData.count(); i++) {
      EOGenericRecord currentData = 
      (EOGenericRecord)weatherData.objectAtIndex(i);
      xmlString.append(XML.buildDataNode(currentData));
    }        
    xmlString.append("</data></weather>");
    return xmlString.toString();
  }
}

Before we move on, I would like to explain a few things about the code. Notice that we are creating an XML file by appending tags and data to a StringBuffer. We are not doing anything fancy to create the XML (like using DOM objects) because we don't need to. In fact the tags that are always the same, such as the root tags, are just added to the string using the append method. Simple is almost always the better option.

When we need data from the database, we build a fetch specification and grab an entire table of data out of the database at a time. We could specify a more complex query, but we want to include all of the data in the XML for now. There's a lot of good documentation from Apple regarding fetch specifications and data access, so I am not going to focus on it in this article.

In the above code, I left out some of the work of node building for separate methods, so let us add those other methods now. Add the following code after the buildCompleteXml method closing bracket.


private static String buildTypeNode(EOGenericRecord theType)
{
  EOGenericRecord dataType = 
  (EOGenericRecord)theType.valueForKey("dataType");
        
  NSDictionary primaryKey =
  EOUtilities.primaryKeyForObject(theType.editingContext(),
                                  theType);
  return "<type id=\"" +
        (Integer)primaryKey.valueForKey("measurementTypeId")
         + "\" name=\""
         + (String)theType.valueForKey("name")
         + "\" dataType=\""
         + (String)dataType.valueForKey("name")
         + "\" />";
}

private static String buildDataNode(EOGenericRecord theData)
{
  NSDictionary theDataKey = 
  EOUtilities.primaryKeyForObject(theData.editingContext(),
                                  theData);

  NSTimestampFormatter formatter = new
    NSTimestampFormatter("%Y-%m-%d %H:%M:%S %z");                                    
  
  return "<item id=\"" +
         (Integer)theDataKey.valueForKey("measuredDataId")
         + "\" time=\"" +
         formatter.format((NSTimestamp)theData.valueForKey(
                                              "timeTaken"))
         + "\" typeId=\""
         + (Number)theData.valueForKey("measurementTypeId")
         + "\" data=\""
         + (String)theData.valueForKey("data")
         + "\" />";
}

For those who would prefer to download the code, here is a link (XML.java).

If you have not used WebObjects before, you may be wondering when we will be writing SQL. Well, we do not have to! WebObjects really makes this kind of thing very simple, so that we can focus on building cool applications.

Notice, in the functions above, that we had to pull the primary key out of the database, which would not normally be an attribute displayed to users. We are doing this because we want to be able to round trip the data, and the primary key will help us to match up data when we import it back into the database.

The measurementTypeId is neither the primary key of the MeasuredData table nor an attribute that we have specified as a client attribute in our EOModel. We are going to need to change this so that we can easily access it in our Java code. Here is how to modify the EOModel:

  1. Open the "Resources" group in the left panel of your Xcode project.

  2. Double click the "Weather.eomodeld" group to open it in EOModeler application.

  3. In EOModeler, click the MeasuredData object in the left panel to display its attributes.

  4. Toggle the diamond icon next to the measurementTypeId attribute so that the diamond is showing. This will allow our Java code to access this attribute. Save the model and return to Xcode.

    Toggle the diamond icon next to the measurementTypeId attribute so that the diamond is showing. This will allow our java code to access this attribute. Save the model and return to Xcode.

Getting to the XML

Now we have a Java class with some methods that will suck information out of our database and transform it to an XML string. Great! Now we need to make the XML easy to view and share. Let's build a web interface that will allow us to download the XML from our web browser. At the end of the article, I will show you how to make it available through a web service, too.

First, we need to create a component that will send the XML to a web browser. Since we already have the code to generate the XML, this part will be short and sweet.

  1. Right click (or Ctrl-click) the Web Components folder in the left panel of your project and choose Add -> New File...

    Right click (or control-click) the Web Components folder in the left panel of your project and choose Add -> New File...

  2. Under the WebObjects section, choose the "Component" type and click Next.

  3. Name the file DisplayXml and check the box next to Application Server in the Targets selector. Make sure that none of the other targets are selected and click Finish.

  4. Open DisplayXml.java in the DisplayXml folder that was just created. Edit the file to match this:


import com.webobjects.foundation.*;
import com.webobjects.appserver.*;
import com.webobjects.eocontrol.*;
import com.webobjects.eoaccess.*;

public class DisplayXml extends WOComponent {
  
  public DisplayXml(WOContext context) {
    super(context);
  }

  public String theXml()
  {
    return XML.buildCompleteXml();
  }
    

  //Allows Internet Explorer to
  //parse as XML (for nice display)
  public void appendToResponse(WOResponse response,
                               WOContext context)
  {
    super.appendToResponse(response, context);
    response.setHeader("text/xml", "Content-type");
  }	
}

We added two methods to DisplayXml.java. The first is called theXml, which grabs the XML from the XML.java class that we built earlier and returns it as a string. The second method is appendToResponse, which will add some metadata to the XML as it passes to a web browser so that the browser knows it is receiving XML instead of HTML. This will allow Internet Explorer to display the data using its built-in XML parser.

Now find and edit the DisplayXml.html file. You should be able to find it in the DisplayXml.wo subfolder. We want the contents of this file to be:

<WEBOBJECT NAME=String1></WEBOBJECT>

If you are having trouble finding DisplayXml.html, check out the left side of this screenshot:
<WEBOBJECT NAME=String1></WEBOBJECT>

That is all! We are not creating an HTML page, so there is no html tag or body tag. Only a WEBOBJECT tag that tells WebObjects to insert some dynamic content when this page is requested. The dynamic content that we want to insert is our XML. The next thing we need to do is map this WEBOBJECT tag with a local Java method that returns a string. Do this now by changing the content of the Display.wod file to the following:


String1: WOString {
    value = theXml;
    escapeHTML = false;
}

The .wod files in WebObjects connect the HTML files to the Java files. In this one, we are telling WebObjects to substitute the value returned by the theXml method for the WEBOBJECT tag named String1. We also set escapeHTML to false because we do not want WebObjects to substitute escape characters for the special characters in our XML.

Create a User Interface with HTML

Next, we are going to create a web page interface that will contain a link to download the XML and will later contain an interface for uploading XML. WebObjects has already created a page called "Main" for our default web page. We are going to edit the HTML as follows:

  1. Open Main.html in Xcode. You can find it in the following location in the left panel of Xcode: WeatherHub -> Web Components -> Main -> Main.wo -> Main.html.

  2. Replace the existing contents of Main.html with the following:


<!DOCTYPE html PUBLIC "-//W3C//DTD html 3.2//EN">
<html>
    <head>
        <title>WeatherHub</title>
    </head>
    <body bgcolor="#FFFFFF">
        <h1>Weather Hub</h1>
        <h3>Download Weather Data</h3>
        <ul>
            <li>Right click to save XML to a local disk</li>
        </ul>
    </body>
</html>
  1. Save Main.html.

This is just standard HTML. We will need to add a dynamic element in order to create a link to our displayXml component. Notice that we did not add a WEBOBJECT tag yet. Unlike the displayXml component, we are going to use the WebObjects Builder application to create the dynamic component this time. As a developer, it is up to you to either use the GUI tools supplied with WebObjects or edit the code by hand. I use both methods, depending on the specific work that I am doing.

Here are the steps for adding the dynamic link:

  1. From Xcode, launch WebObjects Builder by double clicking the Main.wo folder icon in the left panel.

  2. Highlight the text "Right click to save XML to a local disk" in WebObjects Builder. Make sure to just get the text and not the bullet point.

  3. Choose WOHyperlink from the WebObjects menu. The highlighted text will turn blue and will get a link icon at either side.

  4. Now we need to create a method that will be triggered by the link. Do this by choosing Add Action... in the Edit Source menu (it is at the lower left corner of the WebObjects Builder window).

    Now we need to create a method that will be triggered by the link. Do this by choosing Add Action... in the Edit Source menu (it is at the lower left corner of the WebObjects Builder window).
  5. In the pop-up window that's displayed, enter displayXml as the name and choose DisplayXml as the page returned. Click Add. We've just added a method to Main.java that will load the DisplayXml component in the user's browser when the method is called.

  6. To connect this new method with our WOHyperlink, drag a line from the displayXml method (in the lower left panel of WebObjects Builder) to the link icon next to the WOHyperlink that we created. A pop-up menu will appear when you release the mouse. Choose "action" to indicate that the displayXml method should be called when the hyperlink is clicked.

    To connect this new method with our WOHyperlink, drag a line from the <code>displayXml</code> method (in the lower left panel of WebObjects Builder) to the link icon next to the WOHyperlink that we created. A pop-up menu will appear when you release the mouse. Choose "action" to indicate that the <code>displayXml</code> method should be called when the hyperlink is clicked.
  7. Save your changes and go back to Xcode. Examine the Main.java, Main.wod, and Main.html files to see the changes that WebObjects Builder made to these files.

Save, build, and run your project (Apple-R). After the application compiles, WebObjects will launch a window in your browser with Main.html. You may click the link to see the XML. Please note that Safari does not display XML, so you will have to "view source" to see it, or you may right-click the link and save the file to disk. If you run your application in Internet Explorer or Firefox, it will nicely display your XML in the browser window. If there are any errors in your code, you can determine what line cause the problem by reading the stack trace.

Screenshots of Internet Explorer displaying our user interface and XML:
Screenshots of Internet Explorer displaying our user interface and XML:

Add XML Parsing

Getting data out of the database is great, but we also need a way to get data back in! Let's add XML parsing to our application. First, add the following import statements to the beginning of the XML.java file:

import javax.xml.parsers.*;
import org.xml.sax.*;
import java.io.*;

Then add the following methods to the XML class:

public static void addNewData(String theXml) {
  XML.addNewData(new InputSource(new StringReader(theXml)));
}

public static void addNewData(InputSource inSource) {
  SAXParserFactory factory =
  SAXParserFactory.newInstance();
        
  try {            
    SAXParser saxParser = factory.newSAXParser();
    XMLReader parser = saxParser.getXMLReader();
            
    parsingClass ch = new parsingClass();
            
    parser.setContentHandler(ch);
            
    parser.parse(inSource);
            
  } catch (Throwable t) {
     t.printStackTrace();  
    }
}

There are two methods here. One takes an XML string and the other takes the XML as a Java InputSource. As you can see, the second really does all the work. The reason we have the method that takes a string is that this method will be easier to call from a web service.

The parser code is fairly straightforward. We create a parser using the Java class parsingClass (which we will create in a moment). Then we send the XML to the parser. The exciting work of extracting data from the XML and adding it to the database will take place in the parsingClass file, so let's add it now.

  1. Right click (or Ctrl-click) the Classes folder in the left panel of your project (you may need to expand the WeatherHub group) and choose Add -> New File...

  2. Under the WebObjects section, choose the "Java Class" type and click Next.

  3. Name the file parsingClass.java and check the box next to Application Server in the Targets selector. Make sure that none of the other targets are selected, and click Finish.

  4. Set the contents of parsingClass.java to the following:

import com.webobjects.foundation.*;
import com.webobjects.eocontrol.*;

import java.io.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;

public class parsingClass extends DefaultHandler {    

  public void startElement(String namespace,
                           String localname,
                           String qname,
                           Attributes atts){
    if ( qname.equals("item") ) {
      String id = "";
      String data = "";
      String measurementTypeId = "";
      String timeTaken = "";
      //slurp all the attributes to java variables
      for (int i=0; i < atts.getLength(); i++) {                    
        if (atts.getQName(i).equals("id")) {
          id = atts.getValue(i);  
        }else if(atts.getQName(i).equals("data")){
          data = atts.getValue(i);  
        }else if (atts.getQName(i).equals("typeId")){
          measurementTypeId = atts.getValue(i);  
        }else if(atts.getQName(i).equals("time")){
          timeTaken = atts.getValue(i);  
        }  
      }
      //if an item has a blank ID 
      //we should add it to the database
      if ( id.equals("") ) {
        //create a data item and add it to the database
     System.out.println("Adding data item to the database");
        EOClassDescription dataDesc = 
        EOClassDescription.classDescriptionForEntityName(
                                            "MeasuredData");
        EOEditingContext editingContext = 
        new EOEditingContext();
        EOGenericRecord newDataItem =
        new EOGenericRecord(dataDesc);
                
        newDataItem.takeValueForKey(data, "data");
        newDataItem.takeValueForKey(measurementTypeId,
                                    "measurementTypeId");
                
        NSTimestampFormatter formatter = 
        new NSTimestampFormatter("%Y-%m-%d %H:%M:%S %z");

        try{
          newDataItem.takeValueForKey(
              (NSTimestamp)formatter.parseObject(timeTaken),
              "timeTaken");
        } catch (java.text.ParseException e) {
          System.out.println("error parsing timeTaken " + 
                             "date");  
        }

        editingContext.insertObject(newDataItem);
                
        editingContext.saveChanges(); 
      }  
    }
  }
}

For those who would prefer to download the code, here is a link (parsingClass.java).

To parse the XML file, we are overloading the DefaultHandler class of org.xml.sax. For this particular example, we only need to overload one method, startElement. The parser will start at the top of the file and will call this method each time a starting tag is encountered in the XML. Because we are storing all of our data in XML attributes as opposed to storing it between tags, this is sufficient. Check out the Java documentation for DefaultHandler for other methods you can use to parse more complex XML formats.

I assumed that we would not be adding new measureTypes by parsing XML. Remember that we built an administration tool in part two that could handle this task. Instead, to keep this tutorial as simple as possible, the method only adds data item information to the database. That is why most of the code is wrapped in an if statement that checks for an item XML tag.

When an item tag is found, the for loop iterates through all of the XML attributes and saves them into local variables that will be easy for us to refer to. Next, the code needs to decide whether the current node is new or not (we are only going to add new nodes, not allow modification of existing nodes). The code does this by checking for an id attribute. Since id numbers are assigned when an attribute is added to the database, we will assume that a data item without an id has not yet been added to the database.

If our data item is new, the remaining code creates a new item with WebObjects, assigns all of the attribute values that we captured from the XML, and then adds it to the database.

Upload XML User Interface

Now we have all of the code required to parse an XML file and add data items to the database. However, we still need to create a user interface that allows people to upload the XML using their web browsers. Let's make some changes to the Main component that we created earlier.

  1. First, add the following import statements to the beginning of the Main.java file:

    import java.io.*;
    import org.xml.sax.*;
  2. Second, add the following class variables to the Main class in Main.java:

    public String aFileName;
    public String aMimeType;
    public InputStream is = null;
  3. Third, add the following methods to the Main class in Main.java:

public WOComponent uploadData(){
  System.out.println("data uploading");
  return null;
}
    
public String xmlResults(){
  System.out.println(aMimeType);
  if (is != null) {
    try {
      if ((aFileName!=null) &&
          (aFileName.length()>0) &&
          aMimeType.equals("text/xml")) {
         try {
           XML.addNewData(new InputSource(is));
                        
         } catch (Exception e) {
           System.out.println("Error: failed to read " +
                              "from the input stream." +
                               e.getMessage());
         }
       } else {
         return "File selected is not XML";
       }
       return "Parsed " + aFileName;
     } catch (Exception exception) {
       return "Error parsing XML.";
     }
   } else {
     return "No file uploaded";
   }
 }
  1. Here is a link to the complete Main.java file.

The class variables that we added will store information on the file being uploaded. They will be populated with information sent from the web browser. The uploadData method returns null, which tells WebObjects to reload the existing page when this method is called. We are going to call it when a user presses a submit button on the web page. The xmlResults calls our addNewData method that we already created in the XML class. It has a bunch of error checking to handle bad input or missing input. xmlResults also returns a status string that we will display in the web browser.

Next, we will edit the Main.html file.

  1. Add the following HTML in the body tag:

<br />

<h3>Add New Weather Data</h3>
        
<WEBOBJECT NAME=FileUploadForm>
  <ol>
    <li><p><WEBOBJECT NAME=FileUpload></WEBOBJECT></p></li>
    <li><p>
      <WEBOBJECT NAME=SubmitButton></WEBOBJECT>
    </p></li>
    <li><p>Results: <br />
      <WEBOBJECT NAME=String1></WEBOBJECT>
    </p></li>
  </ol>		
</WEBOBJECT>
  1. Here is a link to the complete Main.html

This code creates several user interface elements to be displayed on our web page. It will contain a file upload control, a submit button, and the return value of xmlResults. These three elements are wrapped in form tags. Because these elements all will be dynamically generated by WebObjects, we need to update the Main.wod file to set some attributes of these tags, as well as linking them to the Java code we just wrote.

  1. Add the following code to the end of Main.wod:

    FileUpload: WOFileUpload { 	
        inputStream = is;
        filePath = aFileName;
        size = "20";
        mimeType = aMimeType;
    }
    
    FileUploadForm: WOForm {
        enctype = "multipart/form-data";
        multipleSubmit = false;
    }
    
    String1: WOString {
        value = xmlResults;
        escapeHTML = false;
    }
    
    SubmitButton: WOSubmitButton {
        action = uploadData;
    }
  2. Here is a link to the complete Main.wod file.

The code we just added tells WebObjects where to store the data collected from the web page user (as is the case for FileUpload) and tells WebObjects where to put the values returned from the Java code (as is the case in String1). We are doing a bit of a trick to call the xmlResults method. Each time the page is requested by a web browser, WebObjects adds a string to the HTML, based on the return value of xmlResults. This forces the function to run every time the page is loaded. The first time it is loaded, there will be no XML to parse, so it will return "No file uploaded." When a user chooses a file to upload and hits submit, the web page will be loaded again, but this time there will be an XML file to parse.

A Round-Trip Test

All that remains now is to give our application a spin.

  1. Build and run your application by pressing Apple-R.

  2. The application should launch a browser window with your application. I recommend using Internet Explorer (IE) for this, since IE has a nice XML viewer. You can copy and paste the URL into IE if it launched in Safari by default.

  3. Right-click (or Ctrl-click) the hyperlink and choose "Download link to disk." Save the XML in a location you will remember.

    Right-click (or control-click) the hyperlink and choose "Download link to disk." Save the XML in a location you will remember.
  4. In the freshly downloaded XML, copy, paste, and modify some of the item nodes using a text editor. This will simulate new data. Remember to delete the id value for the new nodes so that the application will know that they are new. Here is the XML that I am using if you would prefer not to create your own. Note that it is okay to add line breaks and white space, if this helps you to manage the XML.

  5. Go back to your application in the web browser. Click the Browse... button and select the XML containing the new data items. Click Submit.

    Go back to your application in the web browser. Click the Browse... button and select the XML containing the new data items. Click Submit.
  6. The Results item should indicate the successful parsing of the XML file.

  7. Click the hyperlink again to generate fresh XML from the database. You should see your new data items. Notice that, unlike the XML you uploaded, they now have id numbers assigned.

    Click the hyperlink again to generate fresh XML from the database. You should see your new data items. Notice that, unlike the XML you uploaded, they now have <code>id</code> numbers assigned.

Adding Web Service Support

Besides accessing the XML through a web page, it would be nice if the tools we plan to create could also request and submit XML via a web service. I will demonstrate this remote interaction in a future article. For now, let us add this functionality to our "hub" application. It's easy!

  1. Add the following line to the end of the Application function in the Application.java file:

WOWebServiceRegistrar.registerWebService(XML.class, true);
  1. Here is a link to the complete Application.java file.

Isn't WebObjects great? That single line of code will allow applications to connect to the XML class via web services!

Extra Credit

There are a few features that would be nice, but I left out for the sake of brevity. If you're feeling ambitious, you could add them yourself. The first is to allow imported XML to add measureTypes to the database. The second is to allow modification of existing data. Actually, if this project was a real assignment, we probably would not want to add either of these features to protect the critical data already in the database, but it would be a good programming exercise to add this functionality.

In the next article, we will build a Cocoa application to facilitate data entry. Stay Tuned!

Resources

Adam Behringer works as the CEO of Bee Documents, which strives to help legal and medical firms efficiently manage their documents.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.