diff --git a/lib/.pear2registry b/lib/.pear2registry
index fae52034d512c482b1b4fa280fb21f50e4afe04a..a421eaecf4abcdef93299b11da5698da5eb6f63b 100644
Binary files a/lib/.pear2registry and b/lib/.pear2registry differ
diff --git a/lib/.xmlregistry/packages/pear.unl.edu/UNL_Services_CourseApproval/0.4.0-info.xml b/lib/.xmlregistry/packages/pear.unl.edu/UNL_Services_CourseApproval/0.4.0-info.xml
new file mode 100644
index 0000000000000000000000000000000000000000..7d33401158bdc82ddbc8d718f4d12dd382a616a9
--- /dev/null
+++ b/lib/.xmlregistry/packages/pear.unl.edu/UNL_Services_CourseApproval/0.4.0-info.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://pear.php.net/dtd/package-2.1" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.1" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0     http://pear.php.net/dtd/tasks-1.0.xsd     http://pear.php.net/dtd/package-2.1     http://pear.php.net/dtd/package-2.1.xsd">
+ <name>UNL_Services_CourseApproval</name>
+ <channel>pear.unl.edu</channel>
+ <summary>Client API for the curriculum request system at creq.unl.edu
+</summary>
+ <description>
+This project provides a simple API for the course data within the creq system
+built by Tim Steiner.
+
+This project requires the UNL_Autoload package, and optionally Cache_Lite for
+caching data from the creq system.
+
+Currently data is cached on the local system in /tmp/cache_* files and stored
+for one week.
+
+See the docs/examples/ directory for examples.
+
+For information on the XML format, see the XSD -
+http://courseapproval.unl.edu/schema/courses.xsd</description>
+ <lead>
+  <name>Brett Bieber</name>
+  <user>saltybeagle</user>
+  <email>brett.bieber@gmail.com</email>
+  <active>yes</active>
+ </lead>
+ <date>2013-09-24</date>
+ <time>10:29:31</time>
+ <version>
+  <release>0.4.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.opensource.org/licenses/bsd-license.php">New BSD License</license>
+ <notes>Feature Release!
+
+New tools for filtering out courses
+
+Features:
+
+* Add support for searching by course number suffix. E.g. 41 for 141, 241, 341
+</notes>
+ <contents>
+  <dir name="/">
+   <file role="test" name="tests/test_framework.php" md5sum="e4f9bc1757951877325604c19e3ab217"/>
+   <file role="test" name="tests/subsequent_courses.phpt" md5sum="f64a2e44fbeb29244d1f513dc9345614"/>
+   <file role="test" name="tests/subjects.phpt" md5sum="2cad4c38c985bf9ad5447cd125f722e4"/>
+   <file role="test" name="tests/search.phpt" md5sum="96138fa9c28c562d58cfc34a015df468"/>
+   <file role="test" name="tests/sample.phpt" md5sum="3aa1605f14db215adbecb5aea6fb9e7e"/>
+   <file role="test" name="tests/listing.phpt" md5sum="09c4d745744c0d1868940c784c818ec1"/>
+   <file role="test" name="tests/isset.phpt" md5sum="894be932fa8e4a53fe5ab6a6a09cf108"/>
+   <file role="test" name="tests/dfremoval.phpt" md5sum="1d7731d25166be2846dca42482d0e9d8"/>
+   <file role="test" name="tests/credits.phpt" md5sum="d8a226bcd8df76eba52d2be3fcd1f232"/>
+   <file role="test" name="tests/array_details.phpt" md5sum="a594d847a39c1e4f252ab6f224a4f7f1"/>
+   <file role="test" name="tests/activities.phpt" md5sum="8fc02ef556eebeb64845686c5329a915"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/XCRIService/MockService.php" md5sum="6ce3e7960cc96859324b01d3689e483c"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/XCRIService/creq.php" md5sum="fa8f0b57588220b7b1d27d9c310ea438"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/XCRIService.php" md5sum="43510f6da18f84d84906db84e85c12ec"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/SubjectArea/Groups.php" md5sum="1366c9b03a537a89738761649f6f9192"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/SubjectArea/Courses.php" md5sum="5d8f10bdb2abd05623f2d17c4e03ead0"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/SubjectArea.php" md5sum="37a06f138ab00f42acca4ba2fa4cedbc"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/SearchInterface/XPath.php" md5sum="b3b89c725efffcc1dd0f5224a337b291"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/SearchInterface.php" md5sum="51d01d912970c154eaf804e98ca541c3"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Search/Results.php" md5sum="fec9d8c559335a6f7e7b894a85056772"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Search.php" md5sum="6c17f58d0f4a7f4e83a441a75e064716"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Listing.php" md5sum="0d2b6c93325511b5d4a06db12d709200"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Filter/ExcludeUndergraduateCourses.php" md5sum="17a5e0f582d8f8d086108df6e24a6959"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Filter/ExcludeGraduateCourses.php" md5sum="099d7af04e732e1851840901c5be3a41"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Courses.php" md5sum="867d1cf75bf8af88b4eb273c9384e417"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Course/Credits.php" md5sum="32be56ea6645167ce0a5a7cc62c4ba96"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Course/Codes.php" md5sum="fae6d972149685f84d70dd7273552661"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Course/Activities.php" md5sum="05600adcb178cb7630337b52969633a9"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/Course.php" md5sum="d3da6078336cae6b2b794ed2c40e8e47"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/College.php" md5sum="90b76d7bbac0738920d6439b37f49d97"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/CachingService/Null.php" md5sum="c830c9993bfa2e7367f52dacb2857ef5"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/CachingService/CacheLite.php" md5sum="45e0a57bdcdb743c006c9a9b754260d4"/>
+   <file role="php" name="src/UNL/Services/CourseApproval/CachingService.php" md5sum="9ff7d4e53553663a7b12f8f89da48fd5"/>
+   <file role="php" name="src/UNL/Services/CourseApproval.php" md5sum="9aa460c3b7225ab2987762fd526a36c7"/>
+   <file role="doc" name="docs/examples/Courses_by_Subject_Code.php" md5sum="7c2172b207b574d0fa0ed31013993fd3"/>
+  </dir>
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>2.0.0a1</min>
+   </pearinstaller>
+  </required>
+ </dependencies>
+ <phprelease/>
+</package>
diff --git a/lib/docs/pear.unl.edu/UNL_Services_CourseApproval/examples/Courses_by_Subject_Code.php b/lib/docs/pear.unl.edu/UNL_Services_CourseApproval/examples/Courses_by_Subject_Code.php
new file mode 100644
index 0000000000000000000000000000000000000000..76ea63035ee5487a934909317855bb2adcc29513
--- /dev/null
+++ b/lib/docs/pear.unl.edu/UNL_Services_CourseApproval/examples/Courses_by_Subject_Code.php
@@ -0,0 +1,185 @@
+<?php 
+chdir(dirname(dirname(dirname(__FILE__))).'/src');
+
+require_once 'UNL/Autoload.php';
+UNL_Templates::$options['version'] = 3;
+$page = UNL_Templates::factory('Fixed');
+
+$page->addStyleDeclaration('
+.course .subjectCode {background-color:#E7F0F9;margin-bottom:-1px;color:#818489;display:block;float:left;min-width:85px;text-align:center;}
+.course .number {font-size:2.5em;padding:7px 0px;margin:0 5px 0 0;background-color:#E7F0F9;display:block;clear:left;float:left;font-weight:bold;min-width:85px;text-align:center;}
+.course .title {font-size:1.5em; display:block; border-bottom:1px solid #C8C8C8;font-style:normal;font-weight:bold;margin-left:95px;}
+.course .crosslistings {margin:4px 0 4px 95px;display:block;}
+.course .crosslistings .crosslisting {font-size:1em;color:#C60203;background:none;}
+
+.course .prereqs,
+.course .notes,
+.course .description {margin:4px 0;float:left;clear:left;width:450px;}
+
+.course .prereqs {color:#0F900A;font-weight:bold;}
+.course .notes {font-style:italic;}
+.course .description {border-left:3px solid #C8C8C8;padding-left:5px;}
+
+.course .details {width:220px;border-collapse:collapse;right:0px;float:right;}
+.course .details tr.alt td {border:1px solid #C9E2F6;border-right:none;border-left:none;background-color:#E3F0FF;}
+.course .details td {}
+.course .details .label {font-weight:bold;}
+.course .details .value {text-align:right;}
+dd {margin:0 0 3em 0;padding-left:0 !important;position:relative;overflow:hidden;}
+dt {padding:3em 0 0 0 !important;}
+.course {clear:both;}
+');
+
+$page->titlegraphic = '<h1>Undergraduate Bulletin</h1>
+                       <h2>Your Academic Guide</h2>';
+$page->doctitle = '<title>UNL | Undergraduate Bulletin</title>';
+$page->breadcrumbs = '<ul>
+    <li><a href="http://www.unl.edu/">UNL</a></li>
+    <li>Undergraduate Bulletin</li></ul>';
+$page->navlinks = '
+<ul>
+    <li><a href="#">Academic Policies</a></li>
+    <li><a href="#">Achievement-Centered Education (ACE)</a></li>
+    <li><a href="#">Academic Colleges</a></li>
+    <li><a href="#">Areas of Study</a></li>
+    <li><a href="#">Courses</a></li>
+</ul>
+';
+$page->leftRandomPromo = '';
+$page->maincontentarea = '';
+if (!isset($_GET['subject'])) {
+    echo 'Enter a subject code';
+    exit();
+}
+
+
+$subject = new UNL_Services_CourseApproval_SubjectArea($_GET['subject']);
+
+$page->maincontentarea .= '<h1>There are '.count($subject->courses).' courses for '.htmlentities($subject).'</h1>';
+
+$page->maincontentarea .= implode(', ', $subject->groups);
+
+$page->maincontentarea .=  '<dl>';
+
+foreach ($subject->courses as $course) {
+    $listings = '';
+    $crosslistings = '';
+    $groups = '';
+    foreach ($course->codes as $listing) {
+        if ($listing->subjectArea == $subject->subject) {
+            if ($listing->hasGroups()) {
+                $groups = implode(', ', $listing->groups);
+            }
+            $listings .= $listing->courseNumber.'/';
+        } else {
+            $crosslistings .= '<span class="crosslisting">'.$listing->subjectArea.' '.$listing->courseNumber.'</span>, ';
+        }
+    }
+    $listings = trim($listings, '/');
+    $crosslistings = trim($crosslistings, ', ');
+    
+    $credits = '';
+    if (isset($course->credits['Single Value'])) {
+        $credits = $course->credits['Single Value'];
+    }
+    
+    $format = '';
+    foreach ($course->activities as $type=>$activity) {
+        switch ($type) {
+            case 'lec':
+                $format .= 'Lecture';
+                break;
+            case 'lab':
+                $format .= 'Lab';
+                break;
+            case 'quz':
+                $format .= 'Quiz';
+                break;
+            case 'rct':
+                $format .= 'Recitation';
+                break;
+            case 'stu':
+                $format .= 'Studio';
+                break;
+            case 'fld':
+                $format .= 'Field';
+                break;
+            case 'ind':
+                $format .= 'Independent Study';
+                break;
+            case 'psi':
+                $format .= 'Personalized System of Instruction';
+                break;
+            default:
+                throw new Exception('Unknown activity type! '.$type);
+                break;
+        }
+        $format .= ' '.$activity->hours.', ';
+    }
+    $format = trim($format, ', ');
+    
+    $page->maincontentarea .= "
+        <dt class='course'>
+            <span class='subjectCode'>".htmlentities($subject->subject)."</span>
+            <span class='number'>$listings</span>
+            <span class='title'>".htmlentities($course->title)."</span>";
+        if (!empty($crosslistings)) {
+            $page->maincontentarea .= '<span class="crosslistings">Crosslisted as '.$crosslistings.'</span>';
+        }
+        $page->maincontentarea .= "</dt>
+        <dd class='course'>";
+        $page->maincontentarea .= '<table class="zentable cool details">';
+        $page->maincontentarea .= '<tr class="credits">
+                                    <td class="label">Credit Hours:</td>
+                                    <td class="value">'.$credits.'</td>
+                                    </tr>';
+        if (!empty($format)) {
+            $page->maincontentarea .= '<tr class="format">
+                                        <td class="label">Course Format:</td>
+                                        <td class="value">'.$format.'</td>
+                                        </tr>';
+        }
+        if (count($course->campuses) == 1
+            && $course->campuses[0] != 'UNL') {
+            $page->maincontentarea .= '<tr class="campus">
+                                        <td class="label">Campus:</td>
+                                        <td class="value">'.implode(', ', $course->campuses).'</td>
+                                        </tr>';
+        }
+//        $page->maincontentarea .= '<tr class="termsOffered alt">
+//                                    <td class="label">Terms Offered:</td>
+//                                    <td class="value">'.implode(', ', $course->termsOffered).'</td>
+//                                    </tr>';
+        $page->maincontentarea .= '<tr class="deliveryMethods">
+                                    <td class="label">Course Delivery:</td>
+                                    <td class="value">'.implode(', ', $course->deliveryMethods).'</td>
+                                    </tr>';
+        $ace = '';
+        if (!empty($course->aceOutcomes)) {
+            $ace = implode(', ', $course->aceOutcomes);
+            $page->maincontentarea .= '<tr class="aceOutcomes">
+                                        <td class="label">ACE Outcomes:</td>
+                                        <td class="value">'.$ace.'</td>
+                                        </tr>';
+        }
+        if (!empty($groups)) {
+            $page->maincontentarea .= '<tr class="groups">
+                                        <td class="label">Groups:</td>
+                                        <td class="value">'.$groups.'</td>
+                                       </tr>';
+        }
+        $page->maincontentarea .= '</table>';
+
+        if (!empty($course->prerequisite)) {
+            $page->maincontentarea .= "<p class='prereqs'>Prereqs: ".htmlentities($course->prerequisite)."</p>";
+        }
+        if (!empty($course->notes)) {
+            $page->maincontentarea .= "<p class='notes'>".htmlentities($course->notes)."</p>";
+        }
+        $page->maincontentarea .= "<p class='description'>".htmlentities($course->description)."</p>";
+        
+    $page->maincontentarea .= "</dd>";
+}
+$page->maincontentarea .= '</dl>';
+
+echo $page;
\ No newline at end of file
diff --git a/lib/php/Savvy.php b/lib/php/Savvy.php
index 484bdc884b30d68a6897d05911eac040e2ca741f..00110a1202b09cfb4b686a5b26bd2d419f0f4700 100644
--- a/lib/php/Savvy.php
+++ b/lib/php/Savvy.php
@@ -836,7 +836,7 @@ class Savvy
         }
     }
     
-    protected function fetch($mixed, $template = null)
+    public function fetch($mixed, $template = null)
     {
         if ($template) {
             $this->template = $template;
diff --git a/lib/php/UNL/Services/CourseApproval.php b/lib/php/UNL/Services/CourseApproval.php
new file mode 100644
index 0000000000000000000000000000000000000000..37fec840fcc2968400b181d537259c9a9bfb3bb3
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval.php
@@ -0,0 +1,76 @@
+<?php
+class UNL_Services_CourseApproval
+{
+    /**
+     * The caching service used.
+     * 
+     * @var UNL_Services_CourseApproval_CachingService
+     */
+    protected static $_cache;
+    
+    /**
+     * The XCRI service used.
+     * 
+     * @var UNL_Services_CourseApproval_XCRIService
+     */
+    protected static $_xcri;
+    
+    /**
+     * Get the static caching service
+     * 
+     * @return UNL_Services_CourseApproval_CachingService
+     */
+    public static function getCachingService()
+    {
+        if (!isset(self::$_cache)) {
+            try {
+                self::setCachingService(new UNL_Services_CourseApproval_CachingService_CacheLite());
+            } catch(Exception $e) {
+                self::setCachingService(new UNL_Services_CourseApproval_CachingService_Null());
+            }
+        }
+        
+        return self::$_cache;
+    }
+    
+    /**
+     * Set the static caching service
+     * 
+     * @param UNL_Services_CourseApproval_CachingService $service The caching service to use
+     * 
+     * @return UNL_Services_CourseApproval_CachingService
+     */
+    public static function setCachingService(UNL_Services_CourseApproval_CachingService $service)
+    {
+        self::$_cache = $service;
+        
+        return self::$_cache;
+    }
+    
+    /**
+     * Gets the XCRI service we're subscribed to.
+     * 
+     * @return UNL_Services_CourseApproval_XCRIService
+     */
+    public static function getXCRIService()
+    {
+        if (!isset(self::$_xcri)) {
+            self::setXCRIService(new UNL_Services_CourseApproval_XCRIService_creq());
+        }
+        
+        return self::$_xcri;
+    }
+    
+    /**
+     * Set the static XCRI service
+     * 
+     * @param UNL_Services_CourseApproval_XCRIService $xcri The XCRI service object
+     * 
+     * @return UNL_Services_CourseApproval_XCRIService
+     */
+    public static function setXCRIService(UNL_Services_CourseApproval_XCRIService $xcri)
+    {
+        self::$_xcri = $xcri;
+        return self::$_xcri;
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/CachingService.php b/lib/php/UNL/Services/CourseApproval/CachingService.php
new file mode 100644
index 0000000000000000000000000000000000000000..e16cee45df11e5864786e955661141732d20305c
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/CachingService.php
@@ -0,0 +1,6 @@
+<?php 
+interface UNL_Services_CourseApproval_CachingService
+{
+    function save($key, $data);
+    function get($key);
+}
diff --git a/lib/php/UNL/Services/CourseApproval/CachingService/CacheLite.php b/lib/php/UNL/Services/CourseApproval/CachingService/CacheLite.php
new file mode 100644
index 0000000000000000000000000000000000000000..e5ae58adcafa4628c1b10782ddfc96e953833d27
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/CachingService/CacheLite.php
@@ -0,0 +1,28 @@
+<?php 
+class UNL_Services_CourseApproval_CachingService_CacheLite implements UNL_Services_CourseApproval_CachingService
+{
+    protected $cache;
+    
+    function __construct()
+    {
+        @include_once 'Cache/Lite.php';
+        if (!class_exists('Cache_Lite')) {
+            throw new Exception('Unable to include Cache_Lite, is it installed?');
+        }
+        $options = array('lifeTime'=>604800); //one week lifetime
+        $this->cache = new Cache_Lite();
+    }
+    
+    function save($key, $data)
+    {
+        return $this->cache->save($data, $key, 'ugbulletin');
+    }
+    
+    function get($key)
+    {
+        if ($data = $this->cache->get($key, 'ugbulletin')) {
+            return $data;
+        }
+        return false;
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/CachingService/Null.php b/lib/php/UNL/Services/CourseApproval/CachingService/Null.php
new file mode 100644
index 0000000000000000000000000000000000000000..d0339ebc1366cba46d9e966de0bb417af525328e
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/CachingService/Null.php
@@ -0,0 +1,15 @@
+<?php
+class UNL_Services_CourseApproval_CachingService_Null implements UNL_Services_CourseApproval_CachingService
+{
+    function get($key)
+    {
+        // Expired cache always.
+        return false;
+    }
+    
+    function save($key, $data)
+    {
+        // Make it appear as though it was saved.
+        return true;
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/College.php b/lib/php/UNL/Services/CourseApproval/College.php
new file mode 100644
index 0000000000000000000000000000000000000000..7e646d79694c4aaf15b226298dbd68c0eb6a0e93
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/College.php
@@ -0,0 +1,5 @@
+<?php
+class UNL_Services_CourseApproval_College
+{
+    public $areas_of_study;
+}
diff --git a/lib/php/UNL/Services/CourseApproval/Course.php b/lib/php/UNL/Services/CourseApproval/Course.php
new file mode 100644
index 0000000000000000000000000000000000000000..36d4e7a38f05ce294f6a871868f19e65baa584a5
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Course.php
@@ -0,0 +1,262 @@
+<?php 
+class UNL_Services_CourseApproval_Course
+{
+    
+    /**
+     * The internal object
+     * 
+     * @var SimpleXMLElement
+     */
+    protected $_internal;
+
+    /**
+     * Collection of course codes
+     * 
+     * @var UNL_Services_CourseApproval_Course_Codes
+     */
+    public $codes;
+    
+    protected $_getMap = array('credits'         => 'getCredits',
+                               'dfRemoval'       => 'getDFRemoval',
+                               'campuses'        => 'getCampuses',
+                               'deliveryMethods' => 'getDeliveryMethods',
+                               'termsOffered'    => 'getTermsOffered',
+                               'activities'      => 'getActivities',
+                               'aceOutcomes'     => 'getACEOutcomes',
+                               );
+
+    protected $ns_prefix = '';
+
+    function __construct(SimpleXMLElement $xml)
+    {
+        $this->_internal = $xml;
+        //Fetch all namespaces
+        $namespaces = $this->_internal->getNamespaces(true);
+        if (isset($namespaces[''])
+            && $namespaces[''] == 'http://courseapproval.unl.edu/courses') {
+            $this->_internal->registerXPathNamespace('default', $namespaces['']);
+            $this->ns_prefix = 'default:';
+
+            //Register the rest with their prefixes
+            foreach ($namespaces as $prefix => $ns) {
+                $this->_internal->registerXPathNamespace($prefix, $ns);
+            }
+        }
+        $this->codes = new UNL_Services_CourseApproval_Course_Codes($this->_internal->courseCodes->children());
+    }
+    
+    function __get($var)
+    {
+        if (array_key_exists($var, $this->_getMap)) {
+            return $this->{$this->_getMap[$var]}();
+        }
+
+        if (isset($this->_internal->$var)
+            && count($this->_internal->$var->children())) {
+            if (isset($this->_internal->$var->div)) {
+                return str_replace(' xmlns="http://www.w3.org/1999/xhtml"', 
+                                   '',
+                                   html_entity_decode($this->_internal->$var->div->asXML()));
+            }
+        }
+
+        return (string)$this->_internal->$var;
+    }
+    
+    function __isset($var)
+    {
+        $elements = $this->_internal->xpath($this->ns_prefix.$var);
+        if (count($elements)) {
+            return true;
+        }
+        return false;
+    }
+    
+    function getCampuses()
+    {
+        return $this->getArray('campuses');
+    }
+    
+    function getTermsOffered()
+    {
+        return $this->getArray('termsOffered');
+    }
+    
+    function getDeliveryMethods()
+    {
+        return $this->getArray('deliveryMethods');
+    }
+    
+    function getActivities()
+    {
+        return new UNL_Services_CourseApproval_Course_Activities($this->_internal->activities->children());
+    }
+    
+    function getACEOutcomes()
+    {
+        return $this->getArray('aceOutcomes');
+    }
+    
+    function getArray($var)
+    {
+        $results = array();
+        foreach ($this->_internal->$var->children() as $el) {
+            $results[] = (string)$el;
+        }
+        return $results;
+    }
+    
+    /**
+     * Gets the types of credits offered for this course.
+     * 
+     * @return UNL_Services_CourseApproval_Course_Credits
+     */
+    function getCredits()
+    {
+        return new UNL_Services_CourseApproval_Course_Credits($this->_internal->credits->children());
+    }
+    
+    /**
+     * Checks whether this course can remove a previous grade of D or F for the same course.
+     * 
+     * @return bool
+     */
+    function getDFRemoval()
+    {
+        if ($this->_internal->dfRemoval == 'true') {
+            return true;
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Verifies that the course number is in the correct format.
+     * 
+     * @param $number The course number eg 201H, 4004I
+     * @param $parts  Array of matched parts
+     * 
+     * @return bool
+     */
+    public static function validCourseNumber($number, &$parts = null)
+    {
+        $matches = array();
+        if (preg_match('/^([\d]?[\d]{2,3})([A-Z])?$/i', $number, $matches)) {
+            $parts['courseNumber'] = $matches[1];
+            if (isset($matches[2])) {
+                $parts['courseLetter'] = $matches[2];
+            }
+            return true;
+        }
+        
+        return false;
+    }
+    
+    public static function courseNumberFromCourseCode(SimpleXMLElement $xml)
+    {
+        $number = (string)$xml->courseNumber;
+        if (isset($xml->courseLetter)) {
+            $number .= (string)$xml->courseLetter;
+        }
+        return $number;
+    }
+
+    public static function getListingGroups(SimpleXMLElement $xml)
+    {
+        $groups = array();
+        if (isset($xml->courseGroup)) {
+            foreach ($xml->courseGroup as $group) {
+                $groups[] = $group;
+            }
+        }
+        return $groups;
+    }
+    
+    function getHomeListing()
+    {
+        $home_listing = $this->_internal->xpath($this->ns_prefix.'courseCodes/'.$this->ns_prefix.'courseCode[@type="home listing"]');
+        if ($home_listing === false
+            || count($home_listing) < 1) {
+            return false;
+        }
+        $number = UNL_Services_CourseApproval_Course::courseNumberFromCourseCode($home_listing[0]);
+        return new UNL_Services_CourseApproval_Listing($home_listing[0]->subject,
+                                                     $number,
+                                                     UNL_Services_CourseApproval_Course::getListingGroups($home_listing[0]));
+    }
+
+    /**
+     * Search for subsequent courses
+     * 
+     * (reverse prereqs)
+     *
+     * @param UNL_Services_CourseApproval_Search $search_driver
+     *
+     * @return UNL_Services_CourseApproval_Courses
+     */
+    function getSubsequentCourses($search_driver = null)
+    {
+        $searcher = new UNL_Services_CourseApproval_Search($search_driver);
+
+        $query = $this->getHomeListing()->subjectArea.' '.$this->getHomeListing()->courseNumber;
+        return $searcher->byPrerequisite($query);
+    }
+    
+    function asXML()
+    {
+        return $this->_internal->asXML();
+    }
+
+    public static function getPossibleActivities()
+    {
+        //Value=>Description
+        return array('lec' => 'Lecture',
+                     'lab' => 'Lab',
+                     'stu' => 'Studio',
+                     'fld' => 'Field',
+                     'quz' => 'Quiz',
+                     'rct' => 'Recitation',
+                     'ind' => 'Independent Study',
+                     'psi' => 'Personalized System of Instruction');
+    }
+
+    public static function getPossibleAceOutcomes()
+    {
+        //Value=>Description
+        return array(1  => 'ACE 1',
+                     2  => 'ACE 2',
+                     3  => 'ACE 3',
+                     4  => 'ACE 4',
+                     5  => 'ACE 5',
+                     6  => 'ACE 6',
+                     7  => 'ACE 7',
+                     8  => 'ACE 8',
+                     9  => 'ACE 9',
+                     10 => 'ACE 10');
+    }
+
+    public static function getPossibleCampuses()
+    {
+        //Value=>Description
+        return array('UNL'  => 'University of Nebraska Lincoln',
+                     'UNO'  => 'University of Nebraska Omaha',
+                     'UNMC' => 'University of Nebraska Medical University',
+                     'UNK'  => 'University of Nebraska Kearney');
+    }
+
+    public static function getPossibleDeliveryMethods()
+    {
+        //Value=>Description
+        return array('Classroom'      => 'Classroom',
+                     'Web'            => 'Online',
+                     'Correspondence' => 'Correspondence');
+    }
+
+    public static function getPossibleTermsOffered()
+    {
+        //Value=>Description
+        return array('Fall'   => 'Fall',
+                     'Spring' => 'Spring',
+                     'Summer' => 'Summer');
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/Course/Activities.php b/lib/php/UNL/Services/CourseApproval/Course/Activities.php
new file mode 100644
index 0000000000000000000000000000000000000000..335972cd65c58fbd77a1fe48f45bfd5c8312b60b
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Course/Activities.php
@@ -0,0 +1,55 @@
+<?php 
+class UNL_Services_CourseApproval_Course_Activities implements Countable, Iterator
+{
+    protected $_xmlActivities;
+    
+    protected $_currentActivity = 0;
+    
+    function __construct(SimpleXMLElement $xml)
+    {
+        $this->_xmlActivities = $xml;
+    }
+    
+    function current()
+    {
+        return $this->_xmlActivities[$this->_currentActivity];
+    }
+    
+    function next()
+    {
+        ++$this->_currentActivity;
+    }
+    
+    function rewind()
+    {
+        $this->_currentActivity = 0;
+    }
+    
+    function valid()
+    {
+        if ($this->_currentActivity >= $this->count()) {
+            return false;
+        }
+        return true;
+    }
+    
+    function key()
+    {
+        return (string)$this->current()->type;
+    }
+    
+    function count()
+    {
+        return count($this->_xmlActivities);
+    }
+
+    public static function getFullDescription($activity)
+    {
+        $activities = UNL_Services_CourseApproval_Course::getPossibleActivities();
+        if (!isset($activities[$activity])) {
+            throw new Exception('Unknown activity type! '.$activity);
+        }
+
+        return $activities[$activity];
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/Course/Codes.php b/lib/php/UNL/Services/CourseApproval/Course/Codes.php
new file mode 100644
index 0000000000000000000000000000000000000000..15e59e1f36d20d28cc3e67298db1af5adf0a6487
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Course/Codes.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Collection of course codes for this course
+ *
+ * @author Brett Bieber <brett.bieber@gmail.com>
+ */
+class UNL_Services_CourseApproval_Course_Codes extends ArrayIterator
+{
+
+	/**
+	 * Array of results, usually from an xpath query
+	 * 
+	 * @param array $courseCodes
+	 */
+    function __construct($courseCodes)
+    {
+        $codes = array();
+        foreach ($courseCodes as $code) {
+            $codes[] = $code;
+        }
+        parent::__construct($codes);
+    }
+
+    /**
+     * Get the listing
+     *
+     * @return UNL_Services_CourseApproval_Listing
+     */
+    function current()
+    {
+        $number = UNL_Services_CourseApproval_Course::courseNumberFromCourseCode(parent::current());
+        return new UNL_Services_CourseApproval_Listing(parent::current()->subject,
+                                                     $number,
+                                                     UNL_Services_CourseApproval_Course::getListingGroups(parent::current()));
+    }
+
+    /**
+     * Get the course number
+     * 
+     * @return string course number
+     */
+    function key()
+    {
+        return $this->current()->courseNumber;
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/Course/Credits.php b/lib/php/UNL/Services/CourseApproval/Course/Credits.php
new file mode 100644
index 0000000000000000000000000000000000000000..341f131872cccd43e994edbe57ca19868b498f9c
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Course/Credits.php
@@ -0,0 +1,76 @@
+<?php 
+
+class UNL_Services_CourseApproval_Course_Credits implements Countable, ArrayAccess
+{
+    protected $_xmlCredits;
+    
+    protected $_currentCredit = 0;
+    
+    function __construct(SimpleXMLElement $xml)
+    {
+        $this->_xmlCredits = $xml;
+    }
+    
+    function current()
+    {
+        return $this->_xmlCredits[$this->_currentCredit];
+    }
+    
+    function next()
+    {
+        ++$this->_currentCredit;
+    }
+    
+    function rewind()
+    {
+        $this->_currentCredit = 0;
+    }
+    
+    function valid()
+    {
+        if ($this->_currentCredit >= $this->count()) {
+            return false;
+        }
+        return true;
+    }
+    
+    function count()
+    {
+        return count($this->_xmlCredits);
+    }
+    
+    function key()
+    {
+        $credit = $this->current();
+        return $credit['creditType'];
+    }
+    
+    function offsetExists($type)
+    {
+        foreach ($this->_xmlCredits as $credit) {
+            if ($credit['type'] == $type) {
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    function offsetGet($type)
+    {
+        foreach ($this->_xmlCredits as $credit) {
+            if ($credit['type'] == $type) {
+                return (int)$credit;
+            }
+        }
+    }
+    
+    function offsetSet($type, $var)
+    {
+        throw new Exception('Not available.');
+    }
+    
+    function offsetUnset($type)
+    {
+        throw new Exception('Not available.');
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/Courses.php b/lib/php/UNL/Services/CourseApproval/Courses.php
new file mode 100644
index 0000000000000000000000000000000000000000..f8d8845a4b9fd2b24b14ca8e837de643c72839c5
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Courses.php
@@ -0,0 +1,20 @@
+<?php
+
+class UNL_Services_CourseApproval_Courses extends ArrayIterator
+{
+    
+    function __construct($courses)
+    {
+        parent::__construct($courses);
+    }
+    
+    /**
+     * Get the current course
+     * 
+     * @return UNL_Services_CourseApproval_Course
+     */
+    function current()
+    {
+        return new UNL_Services_CourseApproval_Course(parent::current());
+    }
+}
\ No newline at end of file
diff --git a/lib/php/UNL/Services/CourseApproval/Filter/ExcludeGraduateCourses.php b/lib/php/UNL/Services/CourseApproval/Filter/ExcludeGraduateCourses.php
new file mode 100644
index 0000000000000000000000000000000000000000..0d256a4eb5ad77c087cdee573f695f11ebe60b38
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Filter/ExcludeGraduateCourses.php
@@ -0,0 +1,15 @@
+<?php
+class UNL_Services_CourseApproval_Filter_ExcludeGraduateCourses extends FilterIterator
+{
+    function accept()
+    {
+        $course = $this->getInnerIterator()->current();
+        foreach ($course->codes as $listing) {
+            if ($listing->courseNumber < 500) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/lib/php/UNL/Services/CourseApproval/Filter/ExcludeUndergraduateCourses.php b/lib/php/UNL/Services/CourseApproval/Filter/ExcludeUndergraduateCourses.php
new file mode 100644
index 0000000000000000000000000000000000000000..dfb68724b6089fcc2f59a68382070ec9469f8f17
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Filter/ExcludeUndergraduateCourses.php
@@ -0,0 +1,15 @@
+<?php
+class UNL_Services_CourseApproval_Filter_ExcludeUndergraduateCourses extends FilterIterator
+{
+    function accept()
+    {
+        $course = $this->getInnerIterator()->current();
+        foreach ($course->codes as $listing) {
+            if ($listing->courseNumber >= 500) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
\ No newline at end of file
diff --git a/lib/php/UNL/Services/CourseApproval/Listing.php b/lib/php/UNL/Services/CourseApproval/Listing.php
new file mode 100644
index 0000000000000000000000000000000000000000..874ce3e983f38fb8a297f8bc507197ea21f2900a
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Listing.php
@@ -0,0 +1,57 @@
+<?php 
+class UNL_Services_CourseApproval_Listing
+{
+    
+    /**
+     * Internal subject area object
+     * 
+     * @var UNL_Services_CourseApproval_SubjectArea
+     */
+    protected $_subjectArea;
+    
+    /**
+     * The subject area for this listing eg ACCT
+     * 
+     * @var string
+     */
+    public $subjectArea;
+    
+    /**
+     * The course number eg 201
+     * 
+     * @var string|int
+     */
+    public $courseNumber;
+    
+    public $groups = array();
+    
+    function __construct($subject, $number, $groups = array())
+    {
+        $this->subjectArea  = $subject;
+        $this->courseNumber = $number;
+        $this->groups       = $groups;
+    }
+    
+    function __get($var)
+    {
+        if ($var == 'course') {
+            if (!isset($this->_subjectArea)) {
+                $this->_subjectArea = new UNL_Services_CourseApproval_SubjectArea($this->subjectArea);
+            }
+            return $this->_subjectArea->courses[$this->courseNumber];
+        }
+        // Delegate to the course
+        return $this->course->$var;
+    }
+    
+    function __isset($var)
+    {
+        // Delegate to the course
+        return isset($this->course->$var);
+    }
+
+    function hasGroups()
+    {
+        return count($this->groups)? true : false;
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/Search.php b/lib/php/UNL/Services/CourseApproval/Search.php
new file mode 100644
index 0000000000000000000000000000000000000000..4c0918d71a6db08eb768cdff42bd8b161a07f95a
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Search.php
@@ -0,0 +1,93 @@
+<?php
+class UNL_Services_CourseApproval_Search extends UNL_Services_CourseApproval_SearchInterface
+{
+    /**
+     * The driver that performs the searches
+     * @var UNL_Services_CourseApproval_SearchInterface
+     */
+    public $driver;
+
+    function __construct(UNL_Services_CourseApproval_SearchInterface $driver = null)
+    {
+        if (!isset($driver)) {
+            $this->driver = new UNL_Services_CourseApproval_SearchInterface_XPath();
+        } else {
+            $this->driver = $driver;
+        }
+    }
+
+    /**
+     * Combine two queries into one which will return the intersect
+     *
+     * @return string
+     */
+    function intersectQuery($query1, $query2)
+    {
+        return $this->driver->intersectQuery($query1, $query2);
+    }
+
+    function aceQuery($ace)
+    {
+        return $this->driver->aceQuery($ace);
+    }
+    function aceAndNumberPrefixQuery($number)
+    {
+        return $this->driver->aceAndNumberPrefixQuery($number);
+    }
+    function subjectAndNumberQuery($subject, $number, $letter = null)
+    {
+        return $this->driver->subjectAndNumberQuery($subject, $number, $letter);
+    }
+    function subjectAndNumberPrefixQuery($subject, $number)
+    {
+        return $this->driver->subjectAndNumberPrefixQuery($subject, $number);
+    }
+    function subjectAndNumberSuffixQuery($subject, $number)
+    {
+        return $this->driver->subjectAndNumberSuffixQuery($subject, $number);
+    }
+    function numberPrefixQuery($number)
+    {
+        return $this->driver->numberPrefixQuery($number);
+    }
+    function numberSuffixQuery($number)
+    {
+        return $this->driver->numberSuffixQuery($number);
+    }
+    function honorsQuery()
+    {
+        return $this->driver->honorsQuery();
+    }
+    function titleQuery($title)
+    {
+        return $this->driver->titleQuery($title);
+    }
+    function subjectAreaQuery($subject)
+    {
+        return $this->driver->subjectAreaQuery($subject);
+    }
+    function getQueryResult($query, $offset = 0, $limit = -1)
+    {
+        return $this->driver->getQueryResult($query, $offset, $limit);
+    }
+    function numberQuery($number, $letter = null)
+    {
+        return $this->driver->numberQuery($number, $letter);
+    }
+    function creditQuery($credits)
+    {
+        return $this->driver->creditQuery($credits);
+    }
+    function prerequisiteQuery($prereq)
+    {
+        return $this->driver->prerequisiteQuery($prereq);
+    }
+    function undergraduateQuery()
+    {
+        return $this->driver->undergraduateQuery();
+    }
+    function graduateQuery()
+    {
+        return $this->driver->graduateQuery();
+    }
+}
\ No newline at end of file
diff --git a/lib/php/UNL/Services/CourseApproval/Search/Results.php b/lib/php/UNL/Services/CourseApproval/Search/Results.php
new file mode 100644
index 0000000000000000000000000000000000000000..a08406d606c131bd035d55551fda04e78629faef
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/Search/Results.php
@@ -0,0 +1,25 @@
+<?php
+class UNL_Services_CourseApproval_Search_Results extends UNL_Services_CourseApproval_Courses implements Countable
+{
+    protected $total;
+    
+    function __construct($results, $offset = 0, $limit = -1)
+    {
+        $this->total = count($results);
+
+        if (
+                $limit > 0
+            &&
+                $this->total < $offset + $limit
+            ) {
+            $results = array_slice($results, $offset, $limit);
+        }
+
+        parent::__construct($results);
+    }
+    
+    function count()
+    {
+        return $this->total;
+    }
+}
\ No newline at end of file
diff --git a/lib/php/UNL/Services/CourseApproval/SearchInterface.php b/lib/php/UNL/Services/CourseApproval/SearchInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd1db3c001919094b6aeb21b46c83d982273cab1
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/SearchInterface.php
@@ -0,0 +1,137 @@
+<?php
+abstract class UNL_Services_CourseApproval_SearchInterface
+{
+    abstract function aceQuery($ace);
+    abstract function subjectAndNumberQuery($subject, $number, $letter = null);
+    abstract function subjectAndNumberPrefixQuery($subject, $number);
+    abstract function subjectAndNumberSuffixQuery($subject, $number);
+    abstract function numberPrefixQuery($number);
+    abstract function numberSuffixQuery($number);
+    abstract function honorsQuery();
+    abstract function titleQuery($title);
+    abstract function subjectAreaQuery($subject);
+    abstract function numberQuery($number, $letter = null);
+    abstract function creditQuery($credits);
+    abstract function prerequisiteQuery($prereq);
+    abstract function intersectQuery($query1, $query2);
+    abstract function graduateQuery();
+    abstract function undergraduateQuery();
+
+    function filterQuery($query)
+    {
+        return trim($query);
+    }
+
+    public function byTitle($query, $offset = 0, $limit = -1)
+    {
+        $query = $this->titleQuery($this->filterQuery($query));
+
+        return $this->getQueryResult($query, $offset, $limit);
+    }
+
+    public function byNumber($query, $offset = 0, $limit = -1)
+    {
+        $query = $this->numberQuery($this->filterQuery($query));
+
+        return $this->getQueryResult($query, $offset, $limit);
+    }
+
+    public function bySubject($query, $offset = 0, $limit = -1)
+    {
+        $query = $this->subjectAreaQuery($this->filterQuery($query));
+
+        return $this->getQueryResult($query, $offset, $limit);
+    }
+
+    public function byPrerequisite($query, $offset = 0, $limit = -1)
+    {
+        $query = $this->prerequisiteQuery($query);
+
+        return $this->getQueryResult($query, $offset, $limit);
+    }
+
+    public function graduateCourses($offset = 0, $limit = -1)
+    {
+        $query = $this->graduateQuery();
+
+        return $this->getQueryResult($query, $offset, $limit);
+    }
+
+    public function undergraduateCourses($offset = 0, $limit = -1)
+    {
+        $query = $this->undergraduateQuery();
+
+        return $this->getQueryResult($query, $offset, $limit);
+    }
+
+    public function byAny($query, $offset = 0, $limit = -1)
+    {
+        $query = $this->filterQuery($query);
+
+        switch (true) {
+            case preg_match('/([\d]+)\scredits?/i', $query, $match):
+                // Credit search
+                $query = $this->creditQuery($match[1]);
+                break;
+            case preg_match('/^ace\s*:?\s*([0-9])(X+|\*+)/i', $query, $matches):
+                // ACE course, and number range, eg: ACE 2XX
+                $query = $this->aceAndNumberPrefixQuery($matches[1]);
+                break;
+            case preg_match('/^ace\s*:?\s*(10|[1-9])$/i', $query, $match):
+                // ACE outcome number
+                $query = $this->aceQuery($match[1]);
+                break;
+            case preg_match('/^([A-Z]{3,4})\s+([0-9])(X+|\*+)?$/i', $query, $matches):
+                // Course subject and number range, eg: MRKT 3XX
+                $subject = strtoupper($matches[1]);
+
+                $query = $this->subjectAndNumberPrefixQuery($subject, $matches[2]);
+                break;
+            case preg_match('/^([A-Z]{3,4})\s+(X+|\*+)([0-9]+)$/i', $query, $matches):
+                // Course subject and number suffix, eg: MUDC *41
+                $subject = strtoupper($matches[1]);
+
+                $query = $this->subjectAndNumberSuffixQuery($subject, $matches[3]);
+                break;
+            case preg_match('/^([A-Z]{3,4})\s+([\d]?[\d]{2,3})([A-Z])?:?.*$/i', $query, $matches):
+                // Course subject code and number
+                $subject = strtoupper($matches[1]);
+                $letter = null;
+                if (isset($matches[3])) {
+                    $letter = $matches[3];
+                }
+                $query = $this->subjectAndNumberQuery($subject, $matches[2], $letter);
+                break;
+            case preg_match('/^([0-9])(X+|\*+)?$/i', $query, $match):
+                // Course number range
+                $query = $this->numberPrefixQuery($match[1]);
+                break;
+            case preg_match('/^(X+|\*+)([0-9]+)?$/i', $query, $match):
+                // Course number suffix
+                $query = $this->numberSuffixQuery($match[1]);
+                break;
+            case preg_match('/^([\d]?[\d]{2,3})([A-Z])?(\*+)?$/i', $query, $matches):
+
+                $letter = null;
+                if (isset($matches[2])) {
+                    $letter = $matches[2];
+                }
+                $query = $this->numberQuery($matches[1], $letter);
+                break;
+            case preg_match('/^([A-Z]{3,4})(\s*:\s*.*)?(\s[Xx]+|\s\*+)?$/', $query, $matches):
+                // Subject code search
+                $query = $this->subjectAreaQuery($matches[1]);
+                break;
+            case preg_match('/^honors$/i', $query):
+                $query = $this->honorsQuery();
+                break;
+            default:
+                // Do a title text search
+                $query = $this->titleQuery($query);
+        }
+
+        return $this->getQueryResult($query, $offset, $limit);
+    }
+
+    abstract function getQueryResult($query, $offset = 0, $limit = -1);
+}
\ No newline at end of file
diff --git a/lib/php/UNL/Services/CourseApproval/SearchInterface/XPath.php b/lib/php/UNL/Services/CourseApproval/SearchInterface/XPath.php
new file mode 100644
index 0000000000000000000000000000000000000000..3119bfb4c225a47f80ea7c2827493080c6d66eb9
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/SearchInterface/XPath.php
@@ -0,0 +1,338 @@
+<?php
+/**
+ * 
+ * Course search driver which uses XPath queries on the course XML data
+ * 
+ * @author Brett Bieber <brett.bieber@gmail.com>
+ *
+ */
+class UNL_Services_CourseApproval_SearchInterface_XPath extends UNL_Services_CourseApproval_SearchInterface
+{
+    /**
+     * SimpleXMLElement for all courses
+     * 
+     * @var SimpleXMLElement
+     */
+    protected static $all_courses;
+
+    protected static $courses = array();
+
+    const XML_BASE = '/default:courses/default:course/';
+
+    /**
+     * Get all courses in a SimpleXMLElement
+     * 
+     * @return SimpleXMLElement
+     */
+    protected static function getCourses()
+    {
+        if (!isset(self::$all_courses)) {
+            $xml = UNL_Services_CourseApproval::getXCRIService()->getAllCourses();
+            self::$all_courses = new SimpleXMLElement($xml);
+
+            //Fetch all namespaces
+            $namespaces = self::$all_courses->getNamespaces(true);
+            self::$all_courses->registerXPathNamespace('default', $namespaces['']);
+
+            //Register the rest with their prefixes
+            foreach ($namespaces as $prefix => $ns) {
+                self::$all_courses->registerXPathNamespace($prefix, $ns);
+            }
+        }
+
+        return self::$all_courses;
+    }
+
+    /**
+     * Get the XML for a specific subject area as a SimpleXMLElement
+     * 
+     * @param string $subjectarea Course subject area e.g. CSCE
+     * 
+     * @return SimpleXMLElement
+     */
+    protected static function getSubjectAreaCourses($subjectarea)
+    {
+        if (!isset(self::$courses[$subjectarea])) {
+            $xml = UNL_Services_CourseApproval::getXCRIService()->getSubjectArea($subjectarea);
+            self::$courses[$subjectarea] = new SimpleXMLElement($xml);
+
+            //Fetch all namespaces
+            $namespaces = self::$courses[$subjectarea]->getNamespaces(true);
+            self::$courses[$subjectarea]->registerXPathNamespace('default', $namespaces['']);
+
+            //Register the rest with their prefixes
+            foreach ($namespaces as $prefix => $ns) {
+                self::$courses[$subjectarea]->registerXPathNamespace($prefix, $ns);
+            }
+        }
+
+        return self::$courses[$subjectarea];
+    }
+
+    /**
+     * Utility method to trim out characters which aren't safe for XPath queries
+     * 
+     * @param string $query Search string
+     * 
+     * @return string
+     */
+    function filterQuery($query)
+    {
+        $query = trim($query);
+
+        $query = str_replace(array('/', '"', '\'', '*'), ' ', $query);
+        return $query;
+    }
+
+    /**
+     * Set the courses data to perform searches on
+     * 
+     * @param SimpleXMLElement $courses Set of courses to search
+     */
+    public function setCourses(SimpleXMLElement $courses)
+    {
+        self::$courses = $courses;
+    }
+
+    /**
+     * Construct a query for courses matching an Achievement Centered Education (ACE) number
+     * 
+     * @param string|int $ace Achievement Centered Education (ACE) number, e.g. 1-10
+     * 
+     * @return string XPath query
+     */
+    function aceQuery($ace)
+    {
+        return "default:aceOutcomes[default:slo='$ace']/parent::*";
+    }
+
+    /**
+     * Construct a query for Achievement Centered Education (ACE) courses which
+     * have a course number prefix
+     * 
+     * @param string|int $number Number prefix, e.g. 1 for 100 level ACE courses
+     * 
+     * @return string XPath query
+     */
+    function aceAndNumberPrefixQuery($number)
+    {
+        return "default:courseCodes/default:courseCode/default:courseNumber[starts-with(., '$number')]/parent::*/parent::*/parent::*/default:aceOutcomes/parent::*";
+    }
+
+    /**
+     * Construct a query for courses matching a subject and number prefix
+     * 
+     * @param string     $subject Subject code, e.g. CSCE
+     * @param string|int $number  Course number prefix, e.g. 2 for 200 level courses
+     * 
+     * @return string XPath query
+     */
+    function subjectAndNumberPrefixQuery($subject, $number)
+    {
+        return "default:courseCodes/default:courseCode[starts-with(default:courseNumber, '$number') and default:subject='$subject']/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for courses matching a subject and number suffix
+     * 
+     * @param string     $subject Subject code, e.g. MUDC
+     * @param string|int $number  Course number prefix, e.g. 41 for 241, 341, 441
+     * 
+     * @return string XPath query
+     */
+    function subjectAndNumberSuffixQuery($subject, $number)
+    {
+        return "default:courseCodes/default:courseCode[('$number' = substring(default:courseNumber,string-length(default:courseNumber)-string-length('$number')+1)) and default:subject='$subject']/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for courses matching a number prefix
+     * 
+     * @param string|int $number  Course number prefix, e.g. 2 for 200 level courses
+     * 
+     * @return string XPath query
+     */
+    function numberPrefixQuery($number)
+    {
+        return "default:courseCodes/default:courseCode/default:courseNumber[starts-with(., '$number')]/parent::*/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for courses matching a number suffix
+     * 
+     * @param string|int $number  Course number suffix, e.g. 41 for 141, 241, 341 etc
+     * 
+     * @return string XPath query
+     */
+    function numberSuffixQuery($number)
+    {
+        return "default:courseCodes/default:courseCode/default:courseNumber['$number' = substring(., string-length(.)-string-length('$number')+1)]/parent::*/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for honors courses
+     * 
+     * @return string XPath query
+     */
+    function honorsQuery()
+    {
+        return "default:courseCodes/default:courseCode[default:courseLetter='H']/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for courses with a title matching the query
+     * 
+     * @param string $title Portion of the title of the course
+     * 
+     * @return string XPath query
+     */
+    function titleQuery($title)
+    {
+        return 'default:title['.$this->caseInsensitiveXPath($title).']/parent::*';
+    }
+
+    /**
+     * Construct a query for courses matching a subject area
+     * 
+     * @param string $subject Subject code, e.g. CSCE
+     * 
+     * @return string XPath query
+     */
+    function subjectAreaQuery($subject)
+    {
+        return "default:courseCodes/default:courseCode[default:subject='$subject']/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for courses matching a subject and number
+     * 
+     * @param string     $subject Subject code, e.g. CSCE
+     * @param string|int $number  Course number, e.g. 201
+     * @param string     $letter  Optional course letter, e.g. H
+     * 
+     * @return string XPath query
+     */
+    function subjectAndNumberQuery($subject, $number, $letter = null)
+    {
+        return "default:courseCodes/default:courseCode[default:courseNumber='$number'{$this->courseLetterCheck($letter)} and default:subject='$subject']/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for courses matching a number
+     * 
+     * @param string|int $number Course number, e.g. 201
+     * @param string     $letter Optional course letter, e.g. H
+     * 
+     * @return string XPath query
+     */
+    function numberQuery($number, $letter = null)
+    {
+        return "default:courseCodes/default:courseCode[default:courseNumber='$number'{$this->courseLetterCheck($letter)}]/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for undergraduate courses
+     * 
+     * @return string XPath query
+     */
+    function undergraduateQuery()
+    {
+        return "default:courseCodes/default:courseCode[default:courseNumber<'500']/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for graduate courses
+     * 
+     * @return string XPath query
+     */
+    function graduateQuery()
+    {
+        return "default:courseCodes/default:courseCode[default:courseNumber>='500']/parent::*/parent::*";
+    }
+
+    /**
+     * Construct part of an XPath query for matching a course letter
+     *
+     * @param string $letter Letter, e.g. H
+     *
+     * @return string
+     */
+    protected function courseLetterCheck($letter = null)
+    {
+        $letter_check = '';
+        if (!empty($letter)) {
+            $letter_check = " and (default:courseLetter='".strtoupper($letter)."' or default:courseLetter='".strtolower($letter)."')";
+        }
+        return $letter_check;
+    }
+
+    /**
+     * Construct a query for courses with the required number of credits
+     * 
+     * @param string|int $credits Course credits
+     * 
+     * @return string XPath query
+     */
+    function creditQuery($credits)
+    {
+        return "default:courseCredits[default:credit='$credits']/parent::*/parent::*";
+    }
+
+    /**
+     * Construct a query for courses with prerequisites matching the query
+     * 
+     * @param string $prereq Query to search prereqs for
+     * 
+     * @return string XPath query
+     */
+    function prerequisiteQuery($prereq)
+    {
+        return 'default:prerequisite['.$this->caseInsensitiveXPath($prereq).']/parent::*';
+    }
+
+    /**
+     * Convert a query to a case-insensitive XPath contains query
+     *
+     * @param string $query The query to search for
+     * 
+     * @return string
+     */
+    protected function caseInsensitiveXPath($query)
+    {
+        $query = strtolower($query);
+        return 'contains(translate(.,"ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"),"'.$query.'")';
+    }
+
+    /**
+     * Combine two XPath queries into one which will return the intersect
+     *
+     * @return string
+     */
+    public function intersectQuery($query1, $query2)
+    {
+        return $query1 . '/' . $query2;
+    }
+
+    /**
+     * Execute the supplied query and return matching results
+     * 
+     * @param string $query  XPath compatible query
+     * @param int    $offset Offset for pagination of search results
+     * @param int    $limit  Limit for the number of results returned
+     * 
+     * @return UNL_Services_CourseApproval_Search_Results
+     */
+    function getQueryResult($query, $offset = 0, $limit = -1)
+    {
+        // prepend XPath XML Base
+        $query = self::XML_BASE . $query;
+
+        $result = self::getCourses()->xpath($query);
+
+        if ($result === false) {
+            $result = array();
+        }
+
+        return new UNL_Services_CourseApproval_Search_Results($result, $offset, $limit);
+    }
+}
\ No newline at end of file
diff --git a/lib/php/UNL/Services/CourseApproval/SubjectArea.php b/lib/php/UNL/Services/CourseApproval/SubjectArea.php
new file mode 100644
index 0000000000000000000000000000000000000000..1f87a61c224a15d36b8516d20c1e73b75fc165d8
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/SubjectArea.php
@@ -0,0 +1,31 @@
+<?php 
+class UNL_Services_CourseApproval_SubjectArea
+{
+    public $subject;
+
+    /**
+     * Collection of courses
+     * 
+     * @var UNL_Services_CourseApproval_SubjectArea_Courses
+     */
+    public $courses;
+    
+    /**
+     * array of groups if any
+     * @var UNL_Services_CourseApproval_SubjectArea_Groups
+     */
+    public $groups;
+    
+    function __construct($subject)
+    {
+        $this->subject = $subject;
+        $this->courses = new UNL_Services_CourseApproval_SubjectArea_Courses($this);
+        $groups = new UNL_Services_CourseApproval_SubjectArea_Groups($this);
+        $this->groups = $groups->groups;
+    }
+    
+    function __toString()
+    {
+        return $this->subject;
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/SubjectArea/Courses.php b/lib/php/UNL/Services/CourseApproval/SubjectArea/Courses.php
new file mode 100644
index 0000000000000000000000000000000000000000..cc83fb24cf9dd83df26fed3e781161067614f474
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/SubjectArea/Courses.php
@@ -0,0 +1,73 @@
+<?php 
+class UNL_Services_CourseApproval_SubjectArea_Courses extends ArrayIterator implements ArrayAccess
+{
+    protected $_subjectArea;
+    
+    protected $_xml;
+    
+    function __construct(UNL_Services_CourseApproval_SubjectArea $subjectarea)
+    {
+        $this->_subjectArea = $subjectarea;
+        $this->_xml = new SimpleXMLElement(UNL_Services_CourseApproval::getXCRIService()->getSubjectArea($subjectarea->subject));
+        //Fetch all namespaces
+        $namespaces = $this->_xml->getNamespaces(true);
+        $this->_xml->registerXPathNamespace('default', $namespaces['']);
+        
+        //Register the rest with their prefixes
+        foreach ($namespaces as $prefix => $ns) {
+            $this->_xml->registerXPathNamespace($prefix, $ns);
+        }
+
+        parent::__construct($this->_xml->xpath('//default:courses/default:course'));
+    }
+    
+    function current()
+    {
+        return new UNL_Services_CourseApproval_Course(parent::current());
+    }
+    
+    function offsetSet($number, $value)
+    {
+        throw new Exception('Not implemented yet');
+    }
+    
+    function offsetUnset($number)
+    {
+        throw new Exception('Not implemented yet');
+    }
+    
+    function offsetExists($number)
+    {
+        throw new Exception('Not implemented yet');
+    }
+    
+    function offsetGet($number)
+    {
+        $parts = array();
+        if (!UNL_Services_CourseApproval_Course::validCourseNumber($number, $parts)) {
+            throw new Exception('Invalid course number format '.$number);
+        }
+        
+        if (!empty($parts['courseLetter'])) {
+            $letter_check = "default:courseLetter='{$parts['courseLetter']}'";
+        } else {
+            $letter_check = 'not(default:courseLetter)';
+        }
+        
+        $xpath = "//default:courses/default:course/default:courseCodes/default:courseCode[default:subject='{$this->_subjectArea->subject}' and default:courseNumber='{$parts['courseNumber']}' and $letter_check]/parent::*/parent::*";
+        $courses = $this->_xml->xpath($xpath);
+
+        if (false === $courses
+            || !isset($courses[0])) {
+            throw new Exception('No course was found matching '.$this->_subjectArea->subject.' '.$number, 404);
+        }
+
+        if (count($courses) > 1) {
+            // Whoah whoah whoah, more than one course?
+            throw new Exception('More than one course was found matching '.$this->_subjectArea->subject.' '.$number, 500);
+        }
+
+        return new UNL_Services_CourseApproval_Course($courses[0]);
+    }
+}
+
diff --git a/lib/php/UNL/Services/CourseApproval/SubjectArea/Groups.php b/lib/php/UNL/Services/CourseApproval/SubjectArea/Groups.php
new file mode 100644
index 0000000000000000000000000000000000000000..6b6035d1079279bf7489eb62c65ea64b618f4348
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/SubjectArea/Groups.php
@@ -0,0 +1,48 @@
+<?php
+class UNL_Services_CourseApproval_SubjectArea_Groups implements Countable
+{
+    /**
+     * The XCRI as a SimpleXMLElement
+     * 
+     * @var SimpleXMLElement
+     */
+    public $groups = array();
+    
+    /**
+     * subject area
+     * 
+     * @var UNL_Services_CourseApproval_SubjectArea
+     */
+    protected $_subjectArea;
+    
+    function __construct(UNL_Services_CourseApproval_SubjectArea $subjectarea)
+    {
+        $this->_subjectArea = $subjectarea;
+        $this->_xcri = new SimpleXMLElement(UNL_Services_CourseApproval::getXCRIService()->getSubjectArea($subjectarea->subject));
+        
+        //Fetch all namespaces
+        $namespaces = $this->_xcri->getNamespaces(true);
+        $this->_xcri->registerXPathNamespace('default', $namespaces['']);
+        
+        //Register the rest with their prefixes
+        foreach ($namespaces as $prefix => $ns) {
+            $this->_xcri->registerXPathNamespace($prefix, $ns);
+        }
+        
+        $xpath = "//default:subject[.='{$subjectarea->subject}']/../default:courseGroup";
+        $groups = $this->_xcri->xpath($xpath);
+        if ($groups) {
+            foreach ($groups as $group) {
+                $this->groups[] = (string)$group;
+            }
+            
+            $this->groups = array_unique($this->groups);
+            asort($this->groups);
+        }
+    }
+    
+    function count()
+    {
+        return count($this->groups);
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/XCRIService.php b/lib/php/UNL/Services/CourseApproval/XCRIService.php
new file mode 100644
index 0000000000000000000000000000000000000000..8a3150545c3cd86fe5555795e0ac469f694a1521
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/XCRIService.php
@@ -0,0 +1,6 @@
+<?php 
+interface UNL_Services_CourseApproval_XCRIService
+{
+    function getAllCourses();
+    function getSubjectArea($subjectarea);
+}
diff --git a/lib/php/UNL/Services/CourseApproval/XCRIService/MockService.php b/lib/php/UNL/Services/CourseApproval/XCRIService/MockService.php
new file mode 100644
index 0000000000000000000000000000000000000000..325fe51f6ef73a1a24808dab3ce0bf777a71bfb9
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/XCRIService/MockService.php
@@ -0,0 +1,440 @@
+<?php 
+class UNL_Services_CourseApproval_XCRIService_MockService implements UNL_Services_CourseApproval_XCRIService
+{    
+    public $xml_header = '<?xml version="1.0" encoding="UTF-8"?>
+<courses xmlns="http://courseapproval.unl.edu/courses" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://courseapproval.unl.edu/courses /schema/courses.xsd">';
+    
+    public $xml_footer = '</courses>';
+    
+    public $mock_data = array();
+    
+    function __construct()
+    {
+
+		$this->mock_data['MATH'] = <<<MATH
+  <course>
+    <title>Calculus for Managerial and Social Sciences</title>
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>MATH</subject>
+        <courseNumber>104</courseNumber>
+        <courseGroup>Introductory Mathematics Courses</courseGroup>
+      </courseCode>
+      <courseCode type="crosslisting">
+        <subject>MATH</subject>
+        <courseNumber>104</courseNumber>
+        <courseLetter>X</courseLetter>
+      </courseCode>
+    </courseCodes>
+    <gradingType>unrestricted</gradingType>
+    <dfRemoval>false</dfRemoval>
+    <effectiveSemester>1108</effectiveSemester>
+    <prerequisite>
+      <div xmlns="http://www.w3.org/1999/xhtml">Appropriate placement exam score or a grade of P (pass), or C or better in MATH 101.</div>
+    </prerequisite>
+    <notes>
+      <div xmlns="http://www.w3.org/1999/xhtml">Credit for both MATH 104 and 106 is not allowed.</div>
+    </notes>
+    <description>
+      <div xmlns="http://www.w3.org/1999/xhtml">Rudiments of differential and integral calculus with applications to problems from business, economics, and social sciences.</div>
+    </description>
+    <campuses>
+      <campus>UNL</campus>
+    </campuses>
+    <deliveryMethods>
+      <deliveryMethod>Classroom</deliveryMethod>
+      <deliveryMethod>Web</deliveryMethod>
+    </deliveryMethods>
+    <termsOffered>
+      <term>Fall</term>
+      <term>Spring</term>
+      <term>Summer</term>
+    </termsOffered>
+    <activities/>
+    <credits>
+      <credit type="Single Value">3</credit>
+    </credits>
+    <aceOutcomes>
+      <slo>3</slo>
+    </aceOutcomes>
+  </course>
+MATH;
+    	
+        $this->mock_data['ENSC'] = <<<ENSC
+  <course>
+    <title>Energy in Perspective</title>
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>ENSC</subject>
+        <courseNumber>110</courseNumber>
+
+      </courseCode>
+    </courseCodes>
+    <gradingType>letter grade only</gradingType>
+    <dfRemoval>false</dfRemoval>
+    <effectiveSemester>20082</effectiveSemester>
+    <description>
+      <div xmlns="http://www.w3.org/1999/xhtml">Scientific principles and historical interpretation to place energy use in the context of pressing societal, environmental and climate issues.</div>
+
+    </description>
+    <campuses>
+      <campus>UNL</campus>
+    </campuses>
+    <deliveryMethods>
+      <deliveryMethod>Classroom</deliveryMethod>
+    </deliveryMethods>
+    <termsOffered>
+
+      <term>Fall</term>
+    </termsOffered>
+    <activities>
+      <activity>
+        <type>lec</type>
+        <hours>3</hours>
+      </activity>
+
+    </activities>
+    <credits>
+      <credit type="Single Value">3</credit>
+    </credits>
+  </course>
+ENSC;
+        
+        $this->mock_data['ACCT'] = <<<ACCT
+<course>
+    <title>Introductory Accounting I</title>
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>ACCT</subject>
+        <courseNumber>201</courseNumber>
+
+      </courseCode>
+    </courseCodes>
+    <gradingType>letter grade only</gradingType>
+    <dfRemoval>false</dfRemoval>
+    <effectiveSemester>20101</effectiveSemester>
+    <prerequisite>
+      <div xmlns="http://www.w3.org/1999/xhtml">Math 104 with a grade of 'C' or better;  14 cr hrs at UNL with a 2.5 GPA.</div>
+
+    </prerequisite>
+    <notes>
+      <div xmlns="http://www.w3.org/1999/xhtml">ACCT 201 is 'Letter grade only'.</div>
+    </notes>
+    <description>
+      <div xmlns="http://www.w3.org/1999/xhtml">Fundamentals of accounting, reporting, and analysis to understand financial, managerial, and business concepts and practices. Provides foundation for advanced courses.</div>
+    </description>
+    <campuses>
+
+      <campus>UNL</campus>
+    </campuses>
+    <deliveryMethods>
+      <deliveryMethod>Classroom</deliveryMethod>
+    </deliveryMethods>
+    <termsOffered>
+      <term>Fall</term>
+
+      <term>Spring</term>
+      <term>Summer</term>
+    </termsOffered>
+    <activities>
+      <activity>
+        <type>lec</type>
+        <hours>3</hours>
+
+      </activity>
+    </activities>
+    <credits>
+      <credit type="Single Value">3</credit>
+    </credits>
+  </course>
+  <course>
+    <title>Honors: Introductory Accounting I</title>
+
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>ACCT</subject>
+        <courseNumber>201</courseNumber>
+        <courseLetter>H</courseLetter>
+      </courseCode>
+    </courseCodes>
+
+    <gradingType>unrestricted</gradingType>
+    <dfRemoval>false</dfRemoval>
+    <effectiveSemester>20081</effectiveSemester>
+    <prerequisite>
+      <div xmlns="http://www.w3.org/1999/xhtml">Good standing in the University Honors Program or by invitation; freshman standing; 3.5 GPA over at least 14 credit hours earned at UNL.</div>
+    </prerequisite>
+    <description>
+
+      <div xmlns="http://www.w3.org/1999/xhtml">For course description, see ACCT 201.</div>
+    </description>
+    <campuses>
+      <campus>UNL</campus>
+    </campuses>
+    <deliveryMethods>
+      <deliveryMethod>Classroom</deliveryMethod>
+
+    </deliveryMethods>
+    <termsOffered>
+      <term>Fall</term>
+      <term>Spring</term>
+      <term>Summer</term>
+    </termsOffered>
+    <activities>
+
+      <activity>
+        <type>lec</type>
+      </activity>
+    </activities>
+    <credits>
+      <credit type="Single Value">3</credit>
+    </credits>
+  </course>
+ACCT;
+        $this->mock_data['AECN'] = <<<AECN
+  <course>
+    <title>Agricultural Marketing in a Multinational Environment</title>
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>AECN</subject>
+        <courseNumber>425</courseNumber>
+      </courseCode>
+    </courseCodes>
+    <gradingType>unrestricted</gradingType>
+    <dfRemoval>true</dfRemoval>
+    <effectiveSemester>20091</effectiveSemester>
+    <prerequisite>
+      <div xmlns="http://www.w3.org/1999/xhtml">9 hrs agricultural economics and/or economics or permission.</div>
+    </prerequisite>
+    <notes>
+      <div xmlns="http://www.w3.org/1999/xhtml">Capstone course.</div>
+    </notes>
+    <description>
+      <div xmlns="http://www.w3.org/1999/xhtml">Systems approach to evaulating the effects of current domestic and international political and economic events on agricultural markets.</div>
+    </description>
+    <campuses>
+      <campus>UNL</campus>
+    </campuses>
+    <deliveryMethods>
+      <deliveryMethod>Classroom</deliveryMethod>
+    </deliveryMethods>
+    <termsOffered>
+      <term>Fall</term>
+    </termsOffered>
+    <activities/>
+    <credits>
+      <credit type="Single Value">3</credit>
+    </credits>
+    <aceOutcomes>
+      <slo>9</slo>
+      <slo>10</slo>
+    </aceOutcomes>
+  </course>
+  <course>
+    <title>Agricultural and Natural Resource Policy Analysis</title>
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>AECN</subject>
+        <courseNumber>445</courseNumber>
+      </courseCode>
+      <courseCode type="crosslisting">
+        <subject>NREE</subject>
+        <courseNumber>445</courseNumber>
+      </courseCode>
+    </courseCodes>
+    <gradingType>unrestricted</gradingType>
+    <dfRemoval>false</dfRemoval>
+    <effectiveSemester>20091</effectiveSemester>
+    <prerequisite>
+      <div xmlns="http://www.w3.org/1999/xhtml">ECON 211; ECON 212 or AECN 141. ECON 311 and 312 recommended.</div>
+    </prerequisite>
+    <notes>
+      <div xmlns="http://www.w3.org/1999/xhtml">Capstone course. <br/></div>
+    </notes>
+    <description>
+      <div xmlns="http://www.w3.org/1999/xhtml">Introduction to the application of economic concepts and tools to the analysis and evaluation of public policies. Economic approaches to policy evaluation derived from welfare economics. Social benefit-cost analysis described and illustrated through applications to current agricultural and natural resource policy issues.</div>
+    </description>
+    <campuses>
+      <campus>UNL</campus>
+    </campuses>
+    <deliveryMethods>
+      <deliveryMethod>Classroom</deliveryMethod>
+    </deliveryMethods>
+    <termsOffered>
+      <term>Spring</term>
+    </termsOffered>
+    <activities>
+      <activity>
+        <type>lec</type>
+        <hours>3</hours>
+      </activity>
+    </activities>
+    <credits>
+      <credit type="Single Value">3</credit>
+    </credits>
+    <aceOutcomes>
+      <slo>8</slo>
+      <slo>10</slo>
+    </aceOutcomes>
+  </course>
+AECN;
+        $this->mock_data['CSCE'] = <<<CSCE
+  <course>
+    <title>Introduction to Problem Solving with Computers</title>
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>CSCE</subject>
+        <courseNumber>150</courseNumber>
+        <courseLetter>A</courseLetter>
+
+      </courseCode>
+    </courseCodes>
+    <gradingType>unrestricted</gradingType>
+    <dfRemoval>true</dfRemoval>
+    <effectiveSemester>20083</effectiveSemester>
+    <prerequisite>
+      <div xmlns="http://www.w3.org/1999/xhtml">Four years high school mathematics.</div>
+
+    </prerequisite>
+    <notes>
+      <div xmlns="http://www.w3.org/1999/xhtml">
+        <em>CSCE 150A is designed to develop skills in programming and problem solving to prepare for CSCE 155.</em>
+        <em>CSCE 150A will not count toward the requirements for the major in computer science and computer engineering. </em>
+        <em>
+          <em>Credit towards the degree may be earned in only one of: CSCE 150A or CSCE 150E or CSCE 150M or CSCE 252A.</em>
+
+        </em>
+      </div>
+    </notes>
+    <description>
+      <div xmlns="http://www.w3.org/1999/xhtml">Problem solving with a computer and programming fundamentals using a popular high-level language. Logic and functions that apply to computer science; elementary programming constructs, type, and algorithmic techniques.</div>
+    </description>
+    <campuses>
+      <campus>UNL</campus>
+
+    </campuses>
+    <deliveryMethods>
+      <deliveryMethod>Classroom</deliveryMethod>
+    </deliveryMethods>
+    <termsOffered>
+      <term>Fall</term>
+      <term>Spring</term>
+
+      <term>Summer</term>
+    </termsOffered>
+    <activities>
+      <activity>
+        <type>lec</type>
+        <hours>3</hours>
+      </activity>
+
+    </activities>
+    <credits>
+      <credit type="Single Value">3</credit>
+    </credits>
+  </course>
+  <course>
+    <title>Special Topics in Computer Science</title>
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>CSCE</subject>
+        <courseNumber>196</courseNumber>
+
+      </courseCode>
+    </courseCodes>
+    <gradingType>unrestricted</gradingType>
+    <dfRemoval>false</dfRemoval>
+    <effectiveSemester>20081</effectiveSemester>
+    <prerequisite>
+      <div xmlns="http://www.w3.org/1999/xhtml">Permission.</div>
+
+    </prerequisite>
+    <description>
+      <div xmlns="http://www.w3.org/1999/xhtml">Aspects of computers and computing for computer science and computer engineering majors and minors. Topics vary.</div>
+    </description>
+    <campuses>
+      <campus>UNL</campus>
+    </campuses>
+    <deliveryMethods>
+
+      <deliveryMethod>Classroom</deliveryMethod>
+    </deliveryMethods>
+    <termsOffered>
+      <term>Fall</term>
+      <term>Spring</term>
+      <term>Summer</term>
+    </termsOffered>
+
+    <activities/>
+    <credits>
+      <credit type="Lower Range Limit">1</credit>
+      <credit type="Upper Range Limit">3</credit>
+      <credit type="Per Semester Limit">6</credit>
+    </credits>
+  </course>
+CSCE;
+        $this->mock_data['NREE'] = <<<NREE
+<course>
+    <title>Agricultural and Natural Resource Policy Analysis</title>
+    <courseCodes>
+      <courseCode type="home listing">
+        <subject>NREE</subject>
+        <courseNumber>445</courseNumber>
+      </courseCode>
+      <courseCode type="crosslisting">
+        <subject>NREE</subject>
+        <courseNumber>845</courseNumber>
+      </courseCode>
+    </courseCodes>
+    <gradingType>unrestricted</gradingType>
+    <dfRemoval>false</dfRemoval>
+    <effectiveSemester>20091</effectiveSemester>
+    <prerequisite>
+      <div xmlns="http://www.w3.org/1999/xhtml">ECON 211; ECON 212 or AECN 141. ECON 311 and 312 recommended.</div>
+    </prerequisite>
+    <notes>
+      <div xmlns="http://www.w3.org/1999/xhtml">Capstone course. <br/></div>
+    </notes>
+    <description>
+      <div xmlns="http://www.w3.org/1999/xhtml">Introduction to the application of economic concepts and tools to the analysis and evaluation of public policies. Economic approaches to policy evaluation derived from welfare economics. Social benefit-cost analysis described and illustrated through applications to current agricultural and natural resource policy issues.</div>
+    </description>
+    <campuses>
+      <campus>UNL</campus>
+    </campuses>
+    <deliveryMethods>
+      <deliveryMethod>Classroom</deliveryMethod>
+    </deliveryMethods>
+    <termsOffered>
+      <term>Spring</term>
+    </termsOffered>
+    <activities>
+      <activity>
+        <type>lec</type>
+        <hours>3</hours>
+      </activity>
+    </activities>
+    <credits>
+      <credit type="Single Value">3</credit>
+    </credits>
+    <aceOutcomes>
+      <slo>8</slo>
+      <slo>10</slo>
+    </aceOutcomes>
+  </course>
+NREE;
+    }
+    
+    function getAllCourses()
+    {
+        return $this->xml_header.implode($this->mock_data).$this->xml_footer;
+    }
+    
+    function getSubjectArea($subjectarea)
+    {
+        if (!isset($this->mock_data[$subjectarea])) {
+            throw new Exception('Could not get data.', 500);
+        }
+        
+        return $this->xml_header.$this->mock_data[$subjectarea].$this->xml_footer;
+    }
+}
diff --git a/lib/php/UNL/Services/CourseApproval/XCRIService/creq.php b/lib/php/UNL/Services/CourseApproval/XCRIService/creq.php
new file mode 100644
index 0000000000000000000000000000000000000000..584ad0c57a2cbceb2152c69f172564c4339d0f70
--- /dev/null
+++ b/lib/php/UNL/Services/CourseApproval/XCRIService/creq.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Course data driver for the Course Requisition system at UNL (CREQ)
+ * 
+ * @author Brett Bieber <brett.bieber@gmail.com>
+ */
+class UNL_Services_CourseApproval_XCRIService_creq implements UNL_Services_CourseApproval_XCRIService
+{
+
+    /**
+     * URL to the public creq XML data service endpoint
+     * 
+     * @var string
+     */
+    const URL = 'http://creq.unl.edu/courses/public-view/all-courses';
+
+    /**
+     * The caching service.
+     * 
+     * @var UNL_Services_CourseApproval_CachingService
+     */
+    protected $_cache;
+
+    /**
+     * Constructor for the creq service
+     */
+    function __construct()
+    {
+        $this->_cache = UNL_Services_CourseApproval::getCachingService();
+    }
+
+    /**
+     * Get all course data
+     * 
+     * @return string XML course data
+     */
+    function getAllCourses()
+    {
+        return $this->_getData('creq_allcourses', self::URL);
+    }
+
+    /**
+     * Get the XML for a specific subject area, e.g. CSCE
+     * 
+     * @param string $subjectarea Subject area/code to retrieve courses for e.g. CSCE
+     * 
+     * @return string XML data
+     */
+    function getSubjectArea($subjectarea)
+    {
+        return $this->_getData('creq_subject_'.$subjectarea, self::URL.'/subject/'.$subjectarea);
+    }
+
+    /**
+     * Generic data retrieval method which grabs a URL and caches the data
+     * 
+     * @param string $key A unique key for this piece of data
+     * @param string $url The URL to retrieve data from
+     * 
+     * @return string The data from the URL
+     * 
+     * @throws Exception
+     */
+    protected function _getData($key, $url)
+    {
+        if ($data = $this->_cache->get($key)) {
+            return $data;
+        }
+        
+        if ($data = file_get_contents($url)) {
+            if ($this->_cache->save($key, $data)) {
+                return $data;
+            }
+            throw new Exception('Could not save data for '.$url);
+        }
+        
+        throw new Exception('Could not get data from '.$url);
+    }
+}
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/activities.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/activities.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..3d410119b5456dc05a2cfada470be02dcda15692
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/activities.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Course activities test 
+--FILE--
+<?php
+require_once 'test_framework.php';
+$listing = new UNL_Services_CourseApproval_Listing('ACCT', 201);
+$test->assertTrue($listing->activities instanceof Iterator, 'Activities returned is an iterator');
+$test->assertEquals(1, count($listing->activities), 'Count the number of activities');
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/array_details.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/array_details.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..c905b4bb7af83d8f69bdedff40ad573a1189ab93
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/array_details.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Course details returned as arrays
+--FILE--
+<?php
+require_once 'test_framework.php';
+$listing = new UNL_Services_CourseApproval_Listing('ACCT', 201);
+
+$test->assertTrue(is_array($listing->campuses), 'Campuses');
+$test->assertTrue(is_array($listing->deliveryMethods), 'Delivery methods');
+$test->assertTrue(is_array($listing->termsOffered), 'Terms offered');
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/credits.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/credits.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..966bfe3080a3b993bbe0e6e8ea6f72e703049db2
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/credits.phpt
@@ -0,0 +1,26 @@
+--TEST--
+Test course credit information
+--FILE--
+<?php
+require_once 'test_framework.php';
+$listing = new UNL_Services_CourseApproval_Listing('CSCE', 196);
+$test->assertTrue($listing->credits instanceof Countable, 'Credits is a countable object.');
+$test->assertEquals(3, count($listing->credits), 'Three types of credits for this course.');
+
+$test->assertEquals(1, $listing->credits['Lower Range Limit'], 'Array access by type.');
+$test->assertEquals(3, $listing->credits['Upper Range Limit'], 'Array access by type 2.');
+$test->assertEquals(6, $listing->credits['Per Semester Limit'], 'Array access by type 3.');
+$test->assertFalse(isset($listing->credits['Single Value']), 'Course has no credit of this type.');
+$test->assertTrue(isset($listing->credits['Lower Range Limit']), 'Course has credit of this type.');
+
+$listing = new UNL_Services_CourseApproval_Listing('ACCT', 201);
+$test->assertTrue($listing->credits instanceof Countable, 'Credits is a countable object.');
+$test->assertEquals(1, count($listing->credits), 'Three types of credits for this course.');
+
+$test->assertTrue(isset($listing->credits['Single Value']), 'Course has credit of this type.');
+$test->assertEquals(3, $listing->credits['Single Value'], 'Array access by type.');
+
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/dfremoval.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/dfremoval.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..e237e37038fba2af7f045ea3b420867aaa04c5ae
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/dfremoval.phpt
@@ -0,0 +1,19 @@
+--TEST--
+Sample Test
+--FILE--
+<?php
+require_once 'test_framework.php';
+$listing = new UNL_Services_CourseApproval_Listing('ACCT', 201);
+$test->assertFalse($listing->dfRemoval, 'D or F removal');
+
+$listing = new UNL_Services_CourseApproval_Listing('ENSC', 110);
+$test->assertFalse($listing->dfRemoval, 'D or F removal');
+
+$listing = new UNL_Services_CourseApproval_Listing('CSCE', '150A');
+$test->assertTrue($listing->dfRemoval, 'D or F removal');
+
+
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/isset.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/isset.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..3bc42005c1905bdd473c20011ec383a4fb594f51
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/isset.phpt
@@ -0,0 +1,13 @@
+--TEST--
+Sample Test
+--FILE--
+<?php
+require_once 'test_framework.php';
+$listing = new UNL_Services_CourseApproval_Listing('ACCT', 201);
+$test->assertTrue(isset($listing->notes), 'Course has notes.');
+$test->assertTrue(isset($listing->description), 'Course has description.');
+$test->assertFalse(isset($listing->aceOutcomes), 'Course does NOT have ACE outcomes.');
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/listing.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/listing.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..5905f634f6d28c55fe8105eff1d6849187fb0e20
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/listing.phpt
@@ -0,0 +1,28 @@
+--TEST--
+Sample Test
+--FILE--
+<?php
+require_once 'test_framework.php';
+$listing = new UNL_Services_CourseApproval_Listing('ACCT', 201);
+
+$test->assertEquals('ACCT', $listing->subjectArea, 'Subject area');
+$test->assertEquals(201, $listing->courseNumber, 'Course number');
+$test->assertEquals('Introductory Accounting I', $listing->title, 'Course title');
+$test->assertEquals('<div>Fundamentals of accounting, reporting, and analysis to understand financial, managerial, and business concepts and practices. Provides foundation for advanced courses.</div>', $listing->description, 'Course description');
+$test->assertEquals('<div>Math 104 with a grade of \'C\' or better;  14 cr hrs at UNL with a 2.5 GPA.</div>', $listing->prerequisite, 'Prerequisite');
+$test->assertEquals('<div>ACCT 201 is \'Letter grade only\'.</div>', $listing->notes, 'Notes');
+$test->assertEquals('letter grade only', $listing->gradingType, 'Grading type.');
+$test->assertEquals('20101', $listing->effectiveSemester, 'Effective semester');
+
+
+// Now let's test getting an honors course, with H courseLetter.
+$listing = new UNL_Services_CourseApproval_Listing('ACCT', '201H');
+
+$test->assertEquals('ACCT', $listing->subjectArea, 'Subject area');
+$test->assertEquals('201H', $listing->courseNumber, 'Course number');
+$test->assertEquals('Honors: Introductory Accounting I', $listing->title, 'Course title');
+
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/sample.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/sample.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..716939a795014a2b00a196cb00baabd90c7d4691
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/sample.phpt
@@ -0,0 +1,9 @@
+--TEST--
+Sample Test
+--FILE--
+<?php
+require_once 'test_framework.php';
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/search.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/search.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..439de2e0aa9b20cc4c87b0e809489311d156d231
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/search.phpt
@@ -0,0 +1,43 @@
+--TEST--
+Search test
+--FILE--
+<?php
+require_once 'test_framework.php';
+$search = new UNL_Services_CourseApproval_Search();
+
+$courses = $search->byNumber('201');
+$test->assertEquals(2, count($courses), 'Two results returned');
+
+$courses = $search->byTitle('Accounting');
+$test->assertEquals(2, count($courses), 'Two results returned');
+
+foreach ($courses as $course) {
+    $test->assertNotFalse(
+        strpos($course->title, 'Accounting'),
+        'Course title contains the word Accounting'
+    );
+}
+
+$courses = $search->numberSuffixQuery('04');
+$test->assertEquals(1, count($courses), 'One *04 result returned');
+
+$query1 = $search->subjectAreaQuery('NREE');
+$courses = $search->driver->getQueryResult($query1);
+$test->assertEquals(2, count($courses), 'Two results returned for NREE');
+
+$query2 = $search->subjectAreaQuery('AECN');
+$courses = $search->driver->getQueryResult($query2);
+$test->assertEquals(2, count($courses), 'Two results returned for AECN');
+
+$query = $search->intersectQuery($query1, $query2);
+$courses = $search->driver->getQueryResult($query);
+$test->assertEquals(1, count($courses), 'Intersection of two queries');
+
+$courses = $search->graduateCourses();
+$test->assertEquals(1, count($courses), 'One graduate course returned');
+
+
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/subjects.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/subjects.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..e0281e53ff8eeb20995bcbc50240555a31365b0f
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/subjects.phpt
@@ -0,0 +1,14 @@
+--TEST--
+Test Subjects
+--FILE--
+<?php
+require_once 'test_framework.php';
+$subject = new UNL_Services_CourseApproval_SubjectArea('ACCT');
+$test->assertEquals('ACCT', $subject->subject, 'Returns correct subject code');
+$test->assertEquals('ACCT', $subject->__toString(), '__tostring() Returns correct subject code');
+$test->assertTrue($subject->courses instanceof ArrayAccess, 'Listings is an array');
+$test->assertEquals(2, count($subject->courses), 'Count the number of courses.');
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/subsequent_courses.phpt b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/subsequent_courses.phpt
new file mode 100644
index 0000000000000000000000000000000000000000..e2add55ed7a472f86723fb662051acbbb90db734
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/subsequent_courses.phpt
@@ -0,0 +1,18 @@
+--TEST--
+Subsequent course test
+--FILE--
+<?php
+require_once 'test_framework.php';
+$listing = new UNL_Services_CourseApproval_Listing('MATH', '104');
+
+$courses = $listing->course->getSubsequentCourses();
+
+$test->assertEquals(1, count($courses), 'One subsequent course returned');
+foreach ($courses as $course) {
+    $test->assertEquals('Introductory Accounting I', $course->title, 'Course title');
+}
+
+?>
+===DONE===
+--EXPECT--
+===DONE===
\ No newline at end of file
diff --git a/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/test_framework.php b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/test_framework.php
new file mode 100644
index 0000000000000000000000000000000000000000..0041c98a39977f8eb9f6961be2d4eb6f46a45e53
--- /dev/null
+++ b/lib/tests/pear.unl.edu/UNL_Services_CourseApproval/test_framework.php
@@ -0,0 +1,228 @@
+<?php
+$__e = error_reporting();
+error_reporting(E_ERROR|E_NOTICE|E_WARNING);
+
+ini_set('display_errors', true);
+error_reporting(E_ALL|E_STRICT);
+
+function autoload($class)
+{
+    $class = str_replace('_', '/', $class);
+    include $class . '.php';
+}
+    
+spl_autoload_register("autoload");
+
+set_include_path(dirname(__DIR__).'/src/');
+
+@include_once 'Text/Diff.php';
+@include_once 'Text/Diff/Renderer.php';
+@include_once 'Text/Diff/Renderer/unified.php';
+chdir(dirname(dirname(__FILE__)));
+
+UNL_Services_CourseApproval::setCachingService(new UNL_Services_CourseApproval_CachingService_Null());
+UNL_Services_CourseApproval::setXCRIService(new UNL_Services_CourseApproval_XCRIService_MockService());
+
+error_reporting($__e);
+class PEAR2_PHPT
+{
+    var $_diffonly;
+    function __construct($diffonly = false)
+    {
+        $this->_diffonly = $diffonly;
+        $this->_errors = array();
+    }
+
+    function assertTrue($test, $message)
+    {
+        if ($test === true) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpected non-true value: \n";
+        var_export($test);
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertIsa($control, $test, $message)
+    {
+        if (is_a($test, $control)) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpected non-$control object: \n";
+        var_export($test);
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertNull($test, $message)
+    {
+        if ($test === null) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpected non-null value: \n";
+        var_export($test);
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertNotNull($test, $message)
+    {
+        if ($test !== null) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpected null: \n";
+        var_export($test);
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertSame($test, $test1, $message)
+    {
+        if ($test === $test1) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpectedly two vars are not the same thing: \n";
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertNotSame($test, $test1, $message)
+    {
+        if ($test !== $test1) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpectedly two vars are the same thing: \n";
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertFalse($test, $message)
+    {
+        if ($test === false) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpected non-false value: \n";
+        var_export($test);
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertNotTrue($test, $message)
+    {
+        if (!$test) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpected loose true value: \n";
+        var_export($test);
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertNotFalse($test, $message)
+    {
+        if ($test) {
+            return true;
+        }
+        $this->_failTest(debug_backtrace(), $message);
+        echo "Unexpected loose false value: \n";
+        var_export($test);
+        echo "\n'$message'\n";
+        return false;
+    }
+
+    function assertRegex($regex, $test, $message)
+    {
+        if (!preg_match($regex, $test)) {
+            $this->_failTest(debug_backtrace(), $message);
+            echo "Expecting:\nText Matching Regular Expression $regex\n";
+            echo "\nReceived:\n";
+            var_export($test);
+            echo "\n";
+            return false;
+        }
+        return true;
+    }
+
+    function assertException($exception, $class, $emessage, $message)
+    {
+        if (!($exception instanceof $class)) {
+            $this->_failTest(debug_backtrace(), $message);
+            echo "Expecting class $class, got ", get_class($exception);
+        }
+        $this->assertEquals($emessage, $exception->getMessage(), $message, debug_backtrace());
+    }
+
+    function assertEquals($control, $test, $message, $trace = null)
+    {
+        if (!$trace) {
+            $trace = debug_backtrace();
+        }
+        if (str_replace(array("\r", "\n"), array('', ''),
+            var_export($control, true)) != str_replace(array("\r", "\n"), array('', ''),
+            var_export($test, true))) {
+            $this->_failTest($trace, $message);
+            if (class_exists('Text_Diff')) {
+                echo "Diff of expecting/received:\n";
+                $diff = new Text_Diff(
+                    explode("\n", var_export($control, true)),
+                    explode("\n", var_export($test, true)));
+
+                // Output the diff in unified format.
+                $renderer = new Text_Diff_Renderer_unified();
+                echo $renderer->render($diff);
+                if ($this->_diffonly) {
+                    return false;
+                }
+            }
+            echo "Expecting:\n";
+            var_export($control);
+            echo "\nReceived:\n";
+            var_export($test);
+            echo "\n";
+            return false;
+        }
+        return true;
+    }
+
+    function assertFileExists($fname, $message)
+    {
+        if (!@file_exists($fname)) {
+            $this->_failTest(debug_backtrace(), $message);
+            echo "File '$fname' does not exist, and should\n";
+            return false;
+        }
+        return true;
+    }
+
+    function assertFileNotExists($fname, $message)
+    {
+        if (@file_exists($fname)) {
+            $this->_failTest(debug_backtrace(), $message);
+            echo "File '$fname' exists, and should not\n";
+            return false;
+        }
+        return true;
+    }
+
+    function _failTest($trace, $message)
+    {
+        echo 'Test Failure: "' . $message  . "\"\n in " . $trace[0]['file'] . ' line ' .
+            $trace[0]['line'] . "\n";
+    }
+
+    function showAll()
+    {
+        $this->_diffonly = false;
+    }
+}
+$test = new PEAR2_PHPT();
+?>