Index: server/lib/gutenbach-server.py
===================================================================
--- server/lib/gutenbach-server.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
+++ server/lib/gutenbach-server.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
@@ -0,0 +1,2 @@
+import gutenbach.server
+gutenbach.server.start()
Index: server/lib/gutenbach/ipp/attributes/operation.py
===================================================================
--- server/lib/gutenbach/ipp/attributes/operation.py	(revision ee8e6d08f0bd530f50e1298fc1b78a5923e5eb10)
+++ server/lib/gutenbach/ipp/attributes/operation.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
@@ -2,4 +2,6 @@
     'PrinterUri',
     'RequestingUserName',
+    'IppAttributeFidelity',
+    'LastDocument'
 ]
 
@@ -7,5 +9,5 @@
 from .. import Value
 from .. import errors
-from .. import CharacterStringTags
+from .. import CharacterStringTags, IntegerTags
 
 def PrinterUri(val):
@@ -21,2 +23,7 @@
 def IppAttributeFidelity(val):
     raise errors.ClientErrorAttributes, 'ipp-attribute-fidelity'
+
+def LastDocument(val):
+    return Attribute(
+        'last-document',
+        [Value(IntegerTags.BOOLEAN, val)])
Index: server/lib/gutenbach/server/__init__.py
===================================================================
--- server/lib/gutenbach/server/__init__.py	(revision ee8e6d08f0bd530f50e1298fc1b78a5923e5eb10)
+++ server/lib/gutenbach/server/__init__.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
@@ -1,3 +1,21 @@
-from server import GutenbachIPPServer
+import errors
+from errors import *
+__all__ = ['errors']
+__all__.extend(errors.__all__)
+
+from job import Job
+__all__.append('Job')
+
+from printer import GutenbachPrinter
+__all__.append('GutenbachPrinter')
+
+from requests import make_empty_response, GutenbachRequestHandler
+__all__.append('make_empty_response')
+__all__.append('GutenbachRequestHandler')
+
+from server import GutenbachServer, IPPServer
+__all__.append('GutenbachServer')
+__all__.append('IPPServer')
+
 import BaseHTTPServer
 import logging
@@ -5,20 +23,28 @@
 import traceback
 
-# configure logging
+# configure and initialize logging
 logging.basicConfig(level=logging.DEBUG)
-
-# initialize logger
 logger = logging.getLogger(__name__)
 
-def error(self, request, client_address):
+def error(self, request=None, client_address=None):
     logger.fatal(traceback.format_exc())
+    self.gutenbach_server.request_stop = True
     sys.exit(1)
 
 def start():
+    logger.info("Starting Gutenbach server...")
+    gutenbach = GutenbachServer()
+    gutenbach.start()
+
+    logger.info("Starting IPP server...")
     server_address = ('', 8000)
-    httpd = BaseHTTPServer.HTTPServer(server_address, GutenbachIPPServer)
+    httpd = BaseHTTPServer.HTTPServer(server_address, IPPServer)
     httpd.handle_error = error.__get__(httpd)
-    httpd.serve_forever()
+    httpd.gutenbach_server = gutenbach
+    while gutenbach.isAlive():
+        try:
+            httpd.handle_request()
+        except:
+            error(httpd)
 
-if __name__ == "__main__":
-    start()
+__all__.append('start')
Index: server/lib/gutenbach/server/errors.py
===================================================================
--- server/lib/gutenbach/server/errors.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
+++ server/lib/gutenbach/server/errors.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
@@ -0,0 +1,18 @@
+__all__ = [
+    'InvalidJobException',
+    'InvalidPrinterStateException'
+    ]
+
+class InvalidJobException(Exception):
+    def __init__(self, jobid):
+	self.jobid = jobid
+
+    def __str__(self):
+	return "Job with id '%d' does not exist!" % self.jobid
+
+class InvalidPrinterStateException(Exception):
+    def __init__(self, message):
+	self.message = message
+
+    def __str__(self):
+	return self.message
Index: rver/lib/gutenbach/server/exceptions.py
===================================================================
--- server/lib/gutenbach/server/exceptions.py	(revision c16ebf6aab0285c11d3636eef47fc0c90042c3ba)
+++ 	(revision )
@@ -1,13 +1,0 @@
-class InvalidJobException(Exception):
-    def __init__(self, jobid):
-	self.jobid = jobid
-
-    def __str__(self):
-	return "Job with id '%d' does not exist!" % self.jobid
-
-class InvalidPrinterStateException(Exception):
-    def __init__(self, message):
-	self.message = message
-
-    def __str__(self):
-	return self.message
Index: server/lib/gutenbach/server/job.py
===================================================================
--- server/lib/gutenbach/server/job.py	(revision ee8e6d08f0bd530f50e1298fc1b78a5923e5eb10)
+++ server/lib/gutenbach/server/job.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
@@ -1,3 +1,3 @@
-from exceptions import InvalidJobException, InvalidPrinterStateException
+from . import InvalidJobException, InvalidPrinterStateException
 import os
 import gutenbach.ipp as ipp
@@ -24,4 +24,5 @@
 	This sets the status to 'initializing' and optionally sets the
 	document to print to the value of document.
+
 	"""
 
@@ -33,5 +34,8 @@
         self.size     = size
 
-	self.status   = ipp.JobStates.PENDING
+        self.document = None
+        self.document_name = None
+
+	self.status   = ipp.JobStates.HELD
 
     def __getattr__(self, attr):
@@ -92,15 +96,12 @@
 
     def play(self):
-	if self.status != 'active':
-	    raise InvalidJobException(
-		"Cannot play an inactive job!")
-	
-	self.status = const.JobStates.PROCESSING
+        logger.info("playing job %s" % str(self))
 	# TODO: add external call to music player
-        print "Playing job %s" % str(self)
+	self.status = ipp.JobStates.PROCESSING
 	self.printer.complete_job(self.jid)
 
     def finish(self):
-	self.status = const.JobStates.COMPLETE
+        logger.info("finished job %s" % str(self))
+	self.status = ipp.JobStates.COMPLETE
 
     def __repr__(self):
Index: server/lib/gutenbach/server/printer.py
===================================================================
--- server/lib/gutenbach/server/printer.py	(revision 7c143c9bd7d05dd5dd91e0b39c94e394574d93f8)
+++ server/lib/gutenbach/server/printer.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
@@ -1,5 +1,5 @@
 #import alsaaudio as aa
-from .exceptions import InvalidJobException, InvalidPrinterStateException
-from .job import Job
+from . import InvalidJobException, InvalidPrinterStateException
+from . import Job
 import gutenbach.ipp as ipp
 import logging
@@ -221,4 +221,12 @@
         return job
 
+    def send_document(self, jobid, document):
+        job = self.jobs[jobid]
+        if job.status != ipp.JobStates.HELD:
+            raise InvalidPrinterStateException(
+                "Invalid job state: %d" % job.status)
+        job.document = document
+        job.status = ipp.JobStates.PENDING
+
     def print_job(self, job):
         pass
@@ -226,26 +234,22 @@
     def complete_job(self, jobid):
 	job = self.jobs[self.active_jobs.pop(0)]
-	if job.jobid != jobid:
-	    raise InvalidJobException(
-		"Completed job %d has unexpected job id %d!" % \
-		(job.jobid, jobid))
-	
 	self.finished_jobs.append(job)
 	job.finish()
-	return job.jobid
+	return job.jid
 
     def start_job(self, jobid):
 	job = self.jobs[self.active_jobs[0]]
-	if job.jobid != jobid:
-	    raise InvalidJobException(
-		"Completed job %d has unexpected job id %d!" % \
-		(job.jobid, jobid))
-
-	if job.status == 'playing':
+	if job.status != ipp.JobStates.PENDING:
 	    raise InvalidPrinterStateException(
-		"Next job in queue (id %d) is " + \
-		"already playing!" % jobid)
-
+                "Invalid job state: %s" % job.status)
 	job.play()
+
+    @property
+    def next_job(self):
+        if len(self.active_jobs) == 0:
+            job = None
+        else:
+            job = self.active_jobs[0]
+        return job        
 
     def get_job(self, jobid):
Index: server/lib/gutenbach/server/requests.py
===================================================================
--- server/lib/gutenbach/server/requests.py	(revision 7c143c9bd7d05dd5dd91e0b39c94e394574d93f8)
+++ server/lib/gutenbach/server/requests.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
@@ -1,3 +1,2 @@
-from gutenbach.server.printer import GutenbachPrinter
 import gutenbach.ipp as ipp
 import logging
@@ -40,9 +39,7 @@
 class GutenbachRequestHandler(object):
 
-    def __init__(self):
-        self.printers = {
-            "test": GutenbachPrinter(name="test")
-            }
-        self.default = "test"
+    def __init__(self, gutenbach_server):
+        self.gutenbach_server = gutenbach_server
+        self.printer = gutenbach_server.printer
 
     def generic_handle(self, request):
@@ -223,10 +220,9 @@
         if uri_attr != ipp.PrinterUri(uri_attr.values[0].value):
             raise ipp.errors.ClientErrorBadRequest(str(uri_attr))
-        if printer_name not in self.printers:
+        if printer_name != self.printer.name:
             raise ipp.errors.ClientErrorAttributes(str(uri_attr), uri_attr)
-        jobs = self.printers[printer_name].get_jobs()
 
         # get the job attributes and add them to the response
-        for job in self.printers[printer_name].get_jobs():
+        for job in self.printer.get_jobs():
             attrs = job.get_job_attributes(operation)
             response.attribute_groups.append(ipp.AttributeGroup(
@@ -295,9 +291,9 @@
         if uri_attr != ipp.PrinterUri(uri_attr.values[0].value):
             raise ipp.errors.ClientErrorBadRequest(str(uri_attr))
-        if printer_name not in self.printers:
+        if printer_name != self.printer.name:
             raise ipp.errors.ClientErrorAttributes(str(uri_attr), uri_attr)
 
         # get attributes from the printer and add to response
-        job = self.printers[printer_name].create_job(request)
+        job = self.printer.create_job(request)
         response.attribute_groups.append(ipp.AttributeGroup(
             ipp.AttributeTags.JOB, job.get_job_attributes(operation)))
@@ -357,7 +353,7 @@
         if uri_attr != ipp.PrinterUri(uri_attr.values[0].value):
             raise ipp.errors.ClientErrorBadRequest(str(uri_attr))
-        if printer_name not in self.printers:
+        if printer_name != self.printer.name:
             raise ipp.errors.ClientErrorAttributes(str(uri_attr), uri_attr)
-        printer = self.printers[printer_name]
+        printer = self.printer
 
         # get attributes from the printer and add to response
@@ -377,5 +373,39 @@
     @handler_for(ipp.OperationCodes.SEND_DOCUMENT)
     def send_document(self, request, response):
-        raise ipp.errors.ServerErrorOperationNotSupported
+        operation = request.attribute_groups[0]
+
+        # requested printer uri
+        if 'printer-uri' in operation:
+            uri_attr = operation['printer-uri']
+            printer_name = uri_attr.values[0].value.split("/")[-1]
+            if uri_attr != ipp.PrinterUri(uri_attr.values[0].value):
+                raise ipp.errors.ClientErrorBadRequest(str(uri_attr))
+            if printer_name != self.printer.name:
+                raise ipp.errors.ClientErrorAttributes(str(uri_attr), uri_attr)
+        printer = self.printer
+
+        if 'job-id' not in operation:
+            raise ipp.errors.ClientErrorBadRequest("Missing 'job-id' attribute")
+        job_id_attr = operation['job-id']
+        job_id = job_id_attr.values[0].value
+        if job_id_attr != ipp.JobId(job_id_attr.values[0].value):
+            raise ipp.errors.ClientErrorBadRequest(str(job_id_attr))
+        if job_id not in printer.jobs:
+            raise ipp.errors.ClientErrorAttributes(str(job_id_attr))
+        job = printer.jobs[job_id]
+
+        if 'last-document' not in operation:
+            raise ipp.errors.ClientErrorBadRequest("Missing 'last-document' attribute")
+        last_attr = operation['last-document']
+        last = last_attr.values[0].value
+        if last_attr != ipp.LastDocument(last):
+            raise ipp.errors.ClientErrorBadRequest(str(last_attr))
+        if not last:
+            raise ipp.errors.ServerErrorMultipleJobsNotSupported
+
+        printer.send_document(job_id, request.data)
+        attrs = job.get_job_attributes()
+        response.attribute_groups.append(ipp.AttributeGroup(
+            ipp.AttributeTags.JOB, attrs))
 
     @handler_for(ipp.OperationCodes.SEND_URI)
@@ -385,5 +415,4 @@
     @handler_for(ipp.OperationCodes.GET_JOB_ATTRIBUTES)
     def get_job_attributes(self, request, response):
-        
         operation = request.attribute_groups[0]
 
@@ -395,7 +424,7 @@
         if uri_attr != ipp.PrinterUri(uri_attr.values[0].value):
             raise ipp.errors.ClientErrorBadRequest(str(uri_attr))
-        if printer_name not in self.printers:
+        if printer_name != self.printer.name:
             raise ipp.errors.ClientErrorAttributes(str(uri_attr), uri_attr)
-        printer = self.printers[printer_name]
+        printer = self.printer
 
         if 'job-id' not in operation:
@@ -459,5 +488,5 @@
 
         operation = request.attribute_groups[0]
-        printer = self.printers[self.default]
+        printer = self.printer
 
         # get attributes from the printer and add to response
@@ -479,7 +508,6 @@
 
         # get attributes from the printer and add to response
-        for printer in self.printers.values():
-            response.attribute_groups.append(ipp.AttributeGroup(
-                ipp.AttributeTags.PRINTER, printer.get_printer_attributes(operation)))
+        response.attribute_groups.append(ipp.AttributeGroup(
+            ipp.AttributeTags.PRINTER, self.printer.get_printer_attributes(operation)))
 
     @handler_for(ipp.OperationCodes.CUPS_GET_CLASSES)
Index: server/lib/gutenbach/server/server.py
===================================================================
--- server/lib/gutenbach/server/server.py	(revision 7c143c9bd7d05dd5dd91e0b39c94e394574d93f8)
+++ server/lib/gutenbach/server/server.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
@@ -1,17 +1,39 @@
-from gutenbach.server.requests import GutenbachRequestHandler
+import gutenbach.ipp as ipp
+
+from . import InvalidPrinterStateException, InvalidJobException
+from . import GutenbachPrinter
+from . import GutenbachRequestHandler
+
 import BaseHTTPServer
-import gutenbach.ipp as ipp
 import logging
 import sys
 import traceback
 import tempfile
+import threading
+import time
 
 # initialize logger
 logger = logging.getLogger(__name__)
 
-# initialize handler
-handler = GutenbachRequestHandler()
+class GutenbachServer(threading.Thread):
 
-class GutenbachIPPServer(BaseHTTPServer.BaseHTTPRequestHandler):
+    def run(self):
+        self.printer = GutenbachPrinter(name="test")
+        self.request_stop = False
+
+        while not self.request_stop:
+            job = self.printer.next_job
+            if job is not None:
+                try:
+                    self.printer.start_job(job)
+                except InvalidPrinterStateException:
+                    pass
+                except:
+                    logger.fatal(traceback.format_exc())
+                    sys.exit(1)
+            time.sleep(0.1)
+
+class IPPServer(BaseHTTPServer.BaseHTTPRequestHandler):
+    
     def send_continue(self):
         self.send_response(100, "continue")
@@ -83,4 +105,4 @@
         # throw a fatal error.
         logger.debug("request: %s" % repr(request))
-        response = handler.handle(request)
+        response = GutenbachRequestHandler(self.server.gutenbach_server).handle(request)
         self.send_ok(response)
