Index: server/lib/gutenbach/ipp/attributes/operation.py
===================================================================
--- server/lib/gutenbach/ipp/attributes/operation.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
+++ server/lib/gutenbach/ipp/attributes/operation.py	(revision b01b6d11a9f096abe2a8f080b7ea0aeb9b50edbb)
@@ -3,5 +3,13 @@
     'RequestingUserName',
     'IppAttributeFidelity',
-    'LastDocument'
+    'LastDocument',
+    'Limit',
+    'RequestedAttributes',
+    'WhichJobs',
+    'MyJobs',
+    'DocumentName',
+    'Compression',
+    'DocumentFormat',
+    'DocumentNaturalLanguage',
 ]
 
@@ -28,2 +36,42 @@
         'last-document',
         [Value(IntegerTags.BOOLEAN, val)])
+
+def Limit(val):
+    return Attribute(
+        'limit',
+        [Value(IntegerTags.INTEGER, val)])
+
+def RequestedAttributes(*vals):
+    return Attribute(
+        'requested-attributes',
+        [Value(CharacterStringTags.KEYWORD, val) for val in vals])
+
+def WhichJobs(val):
+    return Attribute(
+        'which-jobs',
+        [Value(CharacterStringTags.KEYWORD, val)])
+
+def MyJobs(val):
+    return Attribute(
+        'my-jobs',
+        [Value(IntegerTags.BOOLEAN, val)])
+
+def DocumentName(val):
+    return Attribute(
+        'document-name',
+        [Value(CharacterStringTags.NAME_WITHOUT_LANGUAGE, val)])
+
+def Compression(val):
+    return Attribute(
+        'compression',
+        [Value(CharacterStringTags.KEYWORD, val)])
+
+def DocumentFormat(val):
+    return Attribute(
+        'document-format',
+        [Value(CharacterStringTags.MIME_MEDIA_TYPE, val)])
+
+def DocumentNaturalLanguage(val):
+    return Attribute(
+        'document-natural-language',
+        [Value(CharacterStringTags.NATURAL_LANGUAGE, val)])
Index: server/lib/gutenbach/ipp/core/errors.py
===================================================================
--- server/lib/gutenbach/ipp/core/errors.py	(revision 103711508848c1db8aa0dfc6d977ac327ed6fb21)
+++ server/lib/gutenbach/ipp/core/errors.py	(revision b01b6d11a9f096abe2a8f080b7ea0aeb9b50edbb)
@@ -43,5 +43,6 @@
     
 
-from .constants import ErrorCodes
+from .constants import ErrorCodes, AttributeTags
+from .attributegroup import AttributeGroup
 
 class IPPException(Exception):
@@ -106,8 +107,15 @@
     def __init__(self, message, attrs):
         self.message = message
-        self.bad_attrs = attrs
+        if hasattr(attrs, '__iter__'):
+            self.bad_attrs = attrs
+        else:
+            self.bad_attrs = [attrs]
 
     def update_response(self, response):
-        pass
+        super(ClientErrorAttributes, self).update_response(response)
+        response.attribute_groups.append(
+            AttributeGroup(
+                AttributeTags.UNSUPPORTED,
+                self.bad_attrs))
 
 class ClientErrorUriSchemeNotSupported(IPPClientException):
Index: server/lib/gutenbach/server/errors.py
===================================================================
--- server/lib/gutenbach/server/errors.py	(revision e58af053711ea43159e19ceeb7875364174b2b1f)
+++ server/lib/gutenbach/server/errors.py	(revision b01b6d11a9f096abe2a8f080b7ea0aeb9b50edbb)
@@ -1,5 +1,6 @@
 __all__ = [
     'InvalidJobException',
-    'InvalidPrinterStateException'
+    'InvalidPrinterStateException',
+    'InvalidJobStateException',
     ]
 
@@ -7,12 +8,16 @@
     def __init__(self, jobid):
 	self.jobid = jobid
-
     def __str__(self):
-	return "Job with id '%d' does not exist!" % self.jobid
+	return "Job does not exist: %d" % self.jobid
 
 class InvalidPrinterStateException(Exception):
-    def __init__(self, message):
-	self.message = message
+    def __init__(self, state):
+        self.state = hex(state)
+    def __str__(self):
+        return "Invalid printer state: %s" % self.state
 
+class InvalidJobStateException(Exception):
+    def __init__(self, state):
+        self.state = hex(state)
     def __str__(self):
-	return self.message
+        return "Invalid job state: %s" % self.state
Index: server/lib/gutenbach/server/job.py
===================================================================
--- server/lib/gutenbach/server/job.py	(revision ce2abc5b3d0f21e3ba1b4cfb8e693f55bb957284)
+++ server/lib/gutenbach/server/job.py	(revision b01b6d11a9f096abe2a8f080b7ea0aeb9b50edbb)
@@ -1,3 +1,4 @@
 from . import InvalidJobException, InvalidPrinterStateException
+from gutenbach.ipp import JobStates as States
 import os
 import gutenbach.ipp as ipp
@@ -21,85 +22,100 @@
     ]
 
-    def __init__(self, jid, printer, creator="", name="", size=0):
-	"""Initialize a Gutenbach job.
-
-	This sets the status to 'initializing' and optionally sets the
-	document to print to the value of document.
+    def __init__(self, job_id=-1, printer=None, creator=None, name=None, size=None):
+	"""Create an empty Gutenbach job.
 
 	"""
 
-	self.jid      = jid
-	self.printer  = printer
-
-        self.creator  = creator
-        self.name     = name
-        self.size     = size
+	self.printer = printer
+        self.player = None
+
+	self.id = job_id
+        self.creator = creator
+        self.name = name
+        self.size = size
+	self.status = States.HELD
 
         self.document = None
         self.document_name = None
-        self.player = None
-
-	self.status   = ipp.JobStates.HELD
-
-    def __getattr__(self, attr):
+        self.document_format = None
+        self.document_natural_language = None
+        self.compression = None
+    
+    def __repr__(self):
+	return str(self)
+
+    def __str__(self):
+        return "<Job %d '%s'>" % (self.id, self.name)
+
+    ######################################################################
+    ###                          Properties                            ###
+    ######################################################################
+
+    @property
+    def id(self):
+        """Unique job identifier.  Should be a positive integer,
+        except when unassigned, when it defaults to -1.
+        
+        """
+        return self._id
+    @id.setter
+    def id(self, val):
         try:
-            return self.__getattribute__(attr)
-        except AttributeError:
-            pass
-        return self.__getattribute__(attr.replace("-", "_"))
-
-    def __hasattr__(self, attr):
+            self._id = int(val)
+        except TypeError:
+            self._id = -1
+
+    @property
+    def creator(self):
+        """The user who created the job; analogous to the IPP
+        requesting-user-name.
+
+        """
+        return self._creator
+    @creator.setter
+    def creator(self, val):
+        if val is None:
+            self._creator = ""
+        else:
+            self._creator = str(val)
+
+    @property
+    def name(self):
+        """The job's name.
+
+        """
+        return self._name
+    @name.setter
+    def name(self, val):
+        if val is None:
+            self._name = ""
+        else:
+            self._name = str(val)
+
+    @property
+    def size(self):
+        """The size of the job in bytes.
+
+        """
+        if self.document:
+            size = os.path.getsize(self.document.name)
+        else:
+            size = self._size
+        return size
+    @size.setter
+    def size(self, val):
         try:
-            getattr(self, attr)
-            return True
-        except AttributeError:
-            return False
-
-    #### Job attributes
-
-    @property
-    def job_id(self):
-        return ipp.JobId(self.jid)
-
-    @property
-    def job_name(self):
-        return ipp.JobName(self.name)
-
-    # XXX: we need to actually calculate this!
-    @property
-    def job_originating_user_name(self):
-        return ipp.JobOriginatingUserName(self.creator)
-
-    # XXX: we need to actually calculate this!
-    @property
-    def job_k_octets(self):
-        return ipp.JobKOctets(self.size)
-
-    @property
-    def job_state(self):
-        return ipp.JobState(self.status)
-
-    @property
-    def job_printer_uri(self):
-        return ipp.JobPrinterUri(self.printer.uri)
-
-    def get_job_attributes(self, request=None):
-        if request and 'requested-attributes' in request:
-            requested = []
-            for value in request['requested-attributes'].values:
-                if value.value in self.attributes:
-                    requested.append(value.value)
-        else:
-            requested = self.attributes
-            
-        attributes = [getattr(self, attr) for attr in requested]
-        return attributes
-    
-    #######
+            self._size = int(val)
+        except TypeError:
+            self._size = 0
+
+    ######################################################################
+    ###                            Methods                             ###
+    ######################################################################
 
     def play(self):
         logger.info("playing job %s" % str(self))
 	# TODO: add external call to music player
-	self.status = ipp.JobStates.PROCESSING
+	self.status = States.PROCESSING
         self.player = subprocess.Popen(
             "/usr/bin/mplayer -quiet %s" % self.document.name,
@@ -114,13 +130,89 @@
         #logger.debug(self.player.stdout)
         self.player = None
-	self.printer.complete_job(self.jid)
+	self.printer.complete_job(self.id)
 
     def finish(self):
         logger.info("finished job %s" % str(self))
-	self.status = ipp.JobStates.COMPLETE
-
-    def __repr__(self):
-	return str(self)
-
-    def __str__(self):
-        return "<Job %d '%s'>" % (self.jid if self.jid is not None else -1, self.name)
+	self.status = States.COMPLETE
+
+    ######################################################################
+    ###                        IPP Attributes                          ###
+    ######################################################################
+
+    @property
+    def job_id(self):
+        return ipp.JobId(self.id)
+
+    @property
+    def job_name(self):
+        return ipp.JobName(self.name)
+
+    # XXX: we need to actually calculate this!
+    @property
+    def job_originating_user_name(self):
+        return ipp.JobOriginatingUserName(self.creator)
+
+    # XXX: we need to actually calculate this!
+    @property
+    def job_k_octets(self):
+        return ipp.JobKOctets(self.size)
+
+    @property
+    def job_state(self):
+        return ipp.JobState(self.status)
+
+    @property
+    def job_printer_uri(self):
+        return ipp.JobPrinterUri(self.printer.uri)
+
+
+    ######################################################################
+    ###                        IPP Operations                          ###
+    ######################################################################
+
+    def cancel_job(self):
+        pass
+
+    def send_document(self,
+                      document,
+                      document_name=None,
+                      document_format=None,
+                      document_natural_language=None,
+                      requesting_user_name=None,
+                      compression=None,
+                      last_document=None):
+
+        if self.status != States.HELD:
+            raise InvalidJobStateException(self.status)
+        
+        self.document = document
+        self.document_name = str(document_name)
+        self.document_format = str(document_format)
+        self.document_natural_language = str(document_natural_language)
+        self.creator = str(requesting_user_name)
+        self.compression = str(compression)
+        self.status = States.PENDING
+
+        logger.debug("document for job %d is '%s'" % (self.id, self.document_name))
+
+    def send_uri(self):
+        pass
+
+    def get_job_attributes(self, requested_attributes=None):
+        if requested_attributes is None:
+            requested = self.attributes
+        else:
+            requested = [a for a in self.attributes if a in requested_attributes]
+
+        _attributes = [attr.replace("-", "_") for attr in requested]
+        attributes = [getattr(self, attr) for attr in _attributes]
+        return attributes
+
+    def set_job_attributes(self):
+        pass
+
+    def restart_job(self):
+        pass
+
+    def promote_job(self):
+        pass
Index: server/lib/gutenbach/server/printer.py
===================================================================
--- server/lib/gutenbach/server/printer.py	(revision ffbe41dc14056c0f5a55064f93b535e455ba5f70)
+++ server/lib/gutenbach/server/printer.py	(revision b01b6d11a9f096abe2a8f080b7ea0aeb9b50edbb)
@@ -1,4 +1,5 @@
 from . import InvalidJobException, InvalidPrinterStateException
 from . import Job
+from gutenbach.ipp import PrinterStates as States
 import gutenbach.ipp as ipp
 import logging
@@ -43,11 +44,9 @@
     ]
         
-
     def __init__(self, name):
 
 	self.name = name
-        self.uri = "ipp://localhost:8000/printers/" + self.name
         self.time_created = int(time.time())
-        self.state = "idle"
+        self.state = States.IDLE
 
 	self.finished_jobs = []
@@ -56,168 +55,37 @@
 
         # cups ignores jobs with id 0, so we have to start at 1
-	self._next_jobid = 1 
-
-    def __getattr__(self, attr):
-        try:
-            return self.__getattribute__(attr)
-        except AttributeError:
-            pass
-        return self.__getattribute__(attr.replace("-", "_"))
-
-    def __hasattr__(self, attr):
-        try:
-            getattr(self, attr)
-            return True
-        except AttributeError:
-            return False
-
-    ## Printer attributes
-
-    @property
-    def printer_uri_supported(self):
-        return ipp.PrinterUriSupported(self.uri)
-
-    @property
-    def uri_authentication_supported(self):
-        return ipp.UriAuthenticationSupported("none")
-
-    @property
-    def uri_security_supported(self):
-        return ipp.UriSecuritySupported("none")
-
-    @property
-    def printer_name(self):
-        return ipp.PrinterName(self.name)
-
-    @property
-    def printer_state(self):
-        return ipp.PrinterState(ipp.constants.PrinterStates.IDLE)
-
-    @property
-    def printer_state_reasons(self):
-        return ipp.PrinterStateReasons("none")
-
-    @property
-    def ipp_versions_supported(self):
-        return ipp.IppVersionsSupported("1.0", "1.1")
-
-    # XXX: We should query ourself for the supported operations
-    @property
-    def operations_supported(self):
-        return ipp.OperationsSupported(ipp.OperationCodes.GET_JOBS)
-
-    @property
-    def charset_configured(self):
-        return ipp.CharsetConfigured("utf-8")
-
-    @property
-    def charset_supported(self):
-        return ipp.CharsetSupported("utf-8")
-
-    @property
-    def natural_language_configured(self):
-        return ipp.NaturalLanguageConfigured("en-us")
-
-    @property
-    def generated_natural_language_supported(self):
-        return ipp.GeneratedNaturalLanguageSupported("en-us")
-
-    @property
-    def document_format_default(self):
-        return ipp.DocumentFormatDefault("application/octet-stream")
-
-    @property
-    def document_format_supported(self):
-        return ipp.DocumentFormatSupported("application/octet-stream", "audio/mp3")
-
-    @property
-    def printer_is_accepting_jobs(self):
-        return ipp.PrinterIsAcceptingJobs(True)
-
-    @property
-    def queued_job_count(self):
-        return ipp.QueuedJobCount(len(self.active_jobs))
-
-    @property
-    def pdl_override_supported(self):
-        return ipp.PdlOverrideSupported("not-attempted")
-
-    @property
-    def printer_up_time(self):
-        return ipp.PrinterUpTime(int(time.time()) - self.time_created)
-
-    @property
-    def compression_supported(self):
-        return ipp.CompressionSupported("none")
-
-    @property
-    def multiple_operation_time_out(self):
-        return ipp.MultipleOperationTimeOut(240)
-
-    @property
-    def multiple_document_jobs_supported(self):
-        return ipp.MultipleDocumentJobsSupported(False)
-
-    ## Printer operations
-
-    def get_printer_attributes(self, request=None):
-        if request and 'requested-attributes' in request:
-            requested = []
-            for value in request['requested-attributes'].values:
-                if value.value in self.attributes:
-                    requested.append(value.value)
-        else:
-            requested = self.attributes
-            
-        attributes = [getattr(self, attr) for attr in requested]
-        return attributes
-
-    def create_job(self, request):
-        operation = request.attribute_groups[0]
-        kwargs = {}
-        
-        # requesting username
-        if 'requesting-user-name' in operation:
-            username_attr = operation['requesting-user-name']
-            username = username_attr.values[0].value
-            if username_attr != ipp.RequestingUserName(username):
-                raise ipp.errors.ClientErrorBadRequest(str(username_attr))
-            kwargs['creator'] = username
-
-        # job name
-        if 'job-name' in operation:
-            job_name_attr = operation['job-name']
-            job_name = job_name_attr.values[0].value
-            if job_name_attr != ipp.JobName(job_name):
-                raise ipp.errors.ClientErrorBadRequest(str(job_name_attr))
-            kwargs['name'] = job_name
-
-        # job size
-        if 'job-k-octets' in operation:
-            job_k_octets_attr = operation['job-k-octets']
-            job_k_octets = job_k_octets_attr.values[0].value
-            if job_k_octets_attr != ipp.JobKOctets(job_k_octets):
-                raise ipp.errors.ClientErrorBadRequest(str(job_k_octets_attr))
-            kwargs['size'] = job_k_octets
-
-        job_id = self._next_jobid
-        self._next_jobid += 1
-        
-        job = Job(job_id, self, **kwargs)
-        self.jobs[job_id] = job
-        self.active_jobs.append(job_id)
+	self._next_jobid = 1
+
+    def __repr__(self):
+        return str(self)
+
+    def __str__(self):
+        return "<Printer '%s'>" % self.name 
+
+    ######################################################################
+    ###                          Properties                            ###
+    ######################################################################
+
+    @property
+    def uris(self):
+        uris = ["ipp://localhost:8000/printers/" + self.name,
+                "ipp://localhost/printers/" + self.name]
+        return uris
+    
+    @property
+    def uri(self):
+        return self.uris[0]
+
+    @property
+    def next_job(self):
+        if len(self.active_jobs) == 0:
+            job = None
+        else:
+            job = self.active_jobs[0]
         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
-        logger.debug("document for job %d is '%s'" % (jobid, job.document.name))
-        job.status = ipp.JobStates.PENDING
-
-    def print_job(self, job):
-        pass
+    ######################################################################
+    ###                            Methods                             ###
+    ######################################################################
 
     def complete_job(self, jobid):
@@ -225,20 +93,11 @@
 	self.finished_jobs.append(job)
 	job.finish()
-	return job.jid
+	return job.id
 
     def start_job(self, jobid):
 	job = self.jobs[self.active_jobs[0]]
 	if job.status != ipp.JobStates.PENDING:
-	    raise InvalidPrinterStateException(
-                "Invalid job state: %s" % job.status)
+	    raise InvalidPrinterStateException(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 stop(self):
@@ -255,12 +114,159 @@
 	return self.jobs[jobid]
 
-    def get_jobs(self):
-        print self.active_jobs
-        jobs = [self.jobs[job_id] for job_id in self.active_jobs]
-        return jobs
-
-    # def __repr__(self):
-    #     return str(self)
-
-    # def __str__(self):
-    #     return "<Printer '%s'>" % self.name
+    ######################################################################
+    ###                        IPP Attributes                          ###
+    ######################################################################
+
+    @property
+    def printer_uri_supported(self):
+        return ipp.PrinterUriSupported(self.uri)
+
+    @property
+    def uri_authentication_supported(self):
+        return ipp.UriAuthenticationSupported("none")
+
+    @property
+    def uri_security_supported(self):
+        return ipp.UriSecuritySupported("none")
+
+    @property
+    def printer_name(self):
+        return ipp.PrinterName(self.name)
+
+    @property
+    def printer_state(self):
+        return ipp.PrinterState(self.state)
+
+    @property
+    def printer_state_reasons(self):
+        return ipp.PrinterStateReasons("none")
+
+    @property
+    def ipp_versions_supported(self):
+        return ipp.IppVersionsSupported("1.0", "1.1")
+
+    # XXX: We should query ourself for the supported operations
+    @property
+    def operations_supported(self):
+        return ipp.OperationsSupported(ipp.OperationCodes.GET_JOBS)
+
+    @property
+    def charset_configured(self):
+        return ipp.CharsetConfigured("utf-8")
+
+    @property
+    def charset_supported(self):
+        return ipp.CharsetSupported("utf-8")
+
+    @property
+    def natural_language_configured(self):
+        return ipp.NaturalLanguageConfigured("en-us")
+
+    @property
+    def generated_natural_language_supported(self):
+        return ipp.GeneratedNaturalLanguageSupported("en-us")
+
+    @property
+    def document_format_default(self):
+        return ipp.DocumentFormatDefault("application/octet-stream")
+
+    @property
+    def document_format_supported(self):
+        return ipp.DocumentFormatSupported("application/octet-stream", "audio/mp3")
+
+    @property
+    def printer_is_accepting_jobs(self):
+        return ipp.PrinterIsAcceptingJobs(True)
+
+    @property
+    def queued_job_count(self):
+        return ipp.QueuedJobCount(len(self.active_jobs))
+
+    @property
+    def pdl_override_supported(self):
+        return ipp.PdlOverrideSupported("not-attempted")
+
+    @property
+    def printer_up_time(self):
+        return ipp.PrinterUpTime(int(time.time()) - self.time_created)
+
+    @property
+    def compression_supported(self):
+        return ipp.CompressionSupported("none")
+
+    @property
+    def multiple_operation_time_out(self):
+        return ipp.MultipleOperationTimeOut(240)
+
+    @property
+    def multiple_document_jobs_supported(self):
+        return ipp.MultipleDocumentJobsSupported(False)
+
+
+    ######################################################################
+    ###                        IPP Operations                          ###
+    ######################################################################
+
+    def print_job(self):
+        pass
+
+    def validate_job(self):
+        pass
+
+    def get_jobs(self, requesting_user_name="", which_jobs=None):
+        # Filter by the which-jobs attribute
+        if which_jobs is None:
+            jobs = self.jobs.values()
+        elif which_jobs == "completed":
+            jobs = [self.jobs[job_id] for job_id in self.finished_jobs]
+        elif which_jobs == "not-completed":
+            jobs = [self.jobs[job_id] for job_id in self.active_jobs]
+        else:
+            raise ipp.errors.ClientErrorAttributes(
+                which_jobs, ipp.WhichJobs(which_jobs))
+
+        # Filter by username
+        if requesting_user_name is None:
+            user_jobs = jobs
+        else:
+            user_jobs = [job for job in jobs if job.creator == requesting_user_name]
+        
+        return user_jobs
+
+    def print_uri(self):
+        pass
+
+    def create_job(self, requesting_user_name="", job_name="", job_k_octets=0):
+        job_id = self._next_jobid
+        self._next_jobid += 1
+        
+        job = Job(job_id,
+                  self,
+                  creator=requesting_user_name,
+                  name=job_name,
+                  size=job_k_octets)
+        
+        self.jobs[job_id] = job
+        self.active_jobs.append(job_id)
+        self.state = States.PROCESSING
+        
+        return job
+
+    def pause_printer(self):
+        pass
+
+    def resume_printer(self):
+        pass
+
+    def get_printer_attributes(self, requested_attributes=None):
+        if requested_attributes is None:
+            requested = self.attributes
+        else:
+            requested = [a for a in self.attributes if a in requested_attributes]
+
+        _attributes = [attr.replace("-", "_") for attr in requested]
+        attributes = [getattr(self, attr) for attr in _attributes]
+        return attributes
+
+    def set_printer_attributes(self):
+        pass
Index: server/lib/gutenbach/server/requests.py
===================================================================
--- server/lib/gutenbach/server/requests.py	(revision ffbe41dc14056c0f5a55064f93b535e455ba5f70)
+++ server/lib/gutenbach/server/requests.py	(revision b01b6d11a9f096abe2a8f080b7ea0aeb9b50edbb)
@@ -37,4 +37,12 @@
     return response
 
+def verify_attribute(attr, cls, length=1):
+    vals = [val.value for val in attr.values]
+    if attr != cls(*vals):
+        raise ipp.errors.ClientErrorBadRequest(str(attr))
+    if length is not None and len(vals) != length:
+        raise ipp.errors.ClientErrorBadRequest(str(attr))
+    return vals
+
 class GutenbachRequestHandler(object):
 
@@ -56,16 +64,12 @@
         # check charset
         charset_attr = operation.attributes[0]
-        expected = ipp.AttributesCharset(charset_attr.values[0].value)
-        if charset_attr != expected:
-            raise ipp.errors.ClientErrorBadRequest(str(charset_attr))
-        if charset_attr.values[0].value != 'utf-8':
+        charset = verify_attribute(charset_attr, ipp.AttributesCharset)[0]
+        if charset != 'utf-8':
             raise ipp.errors.ClientErrorAttributes(str(charset_attr))
 
         # check for attributes-natural-language
         natlang_attr = operation.attributes[1]
-        expected = ipp.AttributesNaturalLanguage(natlang_attr.values[0].value)
-        if natlang_attr != expected:
-            raise ipp.errors.ClientErrorBadRequest(str(natlang_attr))
-        if natlang_attr.values[0].value != 'en-us':
+        natlang = verify_attribute(natlang_attr, ipp.AttributesNaturalLanguage)[0]
+        if natlang != 'en-us':
             raise ipp.errors.ClientErrorAttributes(str(natlang_attr))
     
@@ -213,17 +217,50 @@
         operation = request.attribute_groups[0]
 
+        # initialize operation attribute variables
+        printer_name = None
+        user = None
+        limit = None
+        attributes = None
+        which_jobs = None
+        my_jobs = None
+
         # requested printer uri
         if 'printer-uri' not in operation:
             raise ipp.errors.ClientErrorBadRequest("Missing 'printer-uri' attribute")
-        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_uri = verify_attribute(operation['printer-uri'], ipp.PrinterUri)[0]
+        if printer_uri not in self.printer.uris:
+            raise ipp.errors.ClientErrorAttributes(
+                str(operation['printer-uri']), operation['printer-uri'])
+
+        # optional attributes
+        if 'limit' in operation:
+            limit = verify_attribute(
+                operation['limit'], ipp.Limit)[0]
+            
+        if 'requested-attributes' in operation:
+            attributes = verify_attribute(
+                operation['requested-attributes'], ipp.RequestedAttributes, length=None)
+            
+        if 'which-jobs' in operation:
+            which_jobs = verify_attribute(
+                operation['which-jobs'], ipp.WhichJobs)[0]
+            
+        if 'my-jobs' in operation:
+            my_jobs = verify_attribute(
+                operation['my-jobs'], ipp.MyJobs)[0]
+
+        if 'requesting-user-name' in operation:
+            user = verify_attribute(
+                operation['requesting-user-name'], ipp.RequestingUserName)[0]
+            # ignore if we're not filtering jobs by user
+            if not my_jobs:
+                user = None
+            
         # get the job attributes and add them to the response
-        for job in self.printer.get_jobs():
-            attrs = job.get_job_attributes(operation)
+        jobs = self.printer.get_jobs(
+            which_jobs=which_jobs,
+            requesting_user_name=user)
+        for job in jobs:
+            attrs = job.get_job_attributes(requested_attributes=attributes)
             response.attribute_groups.append(ipp.AttributeGroup(
                 ipp.AttributeTags.JOB, attrs))
@@ -283,18 +320,44 @@
         operation = request.attribute_groups[0]
 
+        printer_uri = None
+        requesting_user_name = None
+        job_name = None
+        ipp_attribute_fidelity=None
+        job_k_octets = None
+
         # requested printer uri
         if 'printer-uri' not in operation:
             raise ipp.errors.ClientErrorBadRequest("Missing 'printer-uri' attribute")
-        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_uri = verify_attribute(operation['printer-uri'], ipp.PrinterUri)[0]
+        if printer_uri not in self.printer.uris:
+            raise ipp.errors.ClientErrorAttributes(
+                str(operation['printer-uri']), operation['printer-uri'])
+
+        if 'requesting-user-name' in operation:
+            user_name = verify_attribute(
+                operation['requesting-user-name'], ipp.RequestingUserName)[0]
+
+        if 'job-name' in operation:
+            job_name = verify_attribute(
+                operation['job-name'], ipp.JobName)[0]
+
+        if 'job-k-octets' in operation:
+            job_k_octets = verify_attribute(
+                operation['job-k-octets'], ipp.JobKOctets)[0]
+
+        if 'ipp-attribute-fidelity' in operation:
+            pass # don't care
+        if 'job-impressions' in operation:
+            pass # don't care
+        if 'job-media-sheets' in operation:
+            pass # don't care
 
         # get attributes from the printer and add to response
-        job = self.printer.create_job(request)
+        job = self.printer.create_job(
+            requesting_user_name=requesting_user_name,
+            job_name=job_name,
+            job_k_octets=job_k_octets)
         response.attribute_groups.append(ipp.AttributeGroup(
-            ipp.AttributeTags.JOB, job.get_job_attributes(operation)))
+            ipp.AttributeTags.JOB, job.get_job_attributes()))
     
     @handler_for(ipp.OperationCodes.PAUSE_PRINTER)
@@ -345,18 +408,34 @@
         operation = request.attribute_groups[0]
 
+        printer_uri = None
+        requesting_user_name = None
+        requested_attributes = None
+        document_format = None
+
         # requested printer uri
         if 'printer-uri' not in operation:
             raise ipp.errors.ClientErrorBadRequest("Missing 'printer-uri' attribute")
-        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
+        printer_uri = verify_attribute(operation['printer-uri'], ipp.PrinterUri)[0]
+        if printer_uri not in self.printer.uris:
+            raise ipp.errors.ClientErrorAttributes(
+                str(operation['printer-uri']), operation['printer-uri'])
+
+        # optional attributes
+        if 'requesting-user-name' in operation:
+            user_name = verify_attribute(
+                operation['requesting-user-name'], ipp.RequestingUserName)[0]
+            
+        if 'requested-attributes' in operation:
+            requested_attributes = verify_attribute(
+                operation['requested-attributes'], ipp.RequestedAttributes, length=None)
+
+        if 'document-format' in operation:
+            pass # XXX: todo
 
         # get attributes from the printer and add to response
         response.attribute_groups.append(ipp.AttributeGroup(
-            ipp.AttributeTags.PRINTER, printer.get_printer_attributes(operation)))
+            ipp.AttributeTags.PRINTER,
+            self.printer.get_printer_attributes(
+                requested_attributes=requested_attributes)))
 
     @handler_for(ipp.OperationCodes.SET_PRINTER_ATTRIBUTES)
@@ -372,39 +451,149 @@
     @handler_for(ipp.OperationCodes.SEND_DOCUMENT)
     def send_document(self, request, response):
+        """3.3.1 Send-Document Operation
+        
+        This OPTIONAL operation allows a client to create a
+        multi-document Job object that is initially 'empty' (contains
+        no documents). In the Create-Job response, the Printer object
+        returns the Job object's URI (the 'job-uri' attribute) and the
+        Job object's 32-bit identifier (the 'job-id' attribute). For
+        each new document that the client desires to add, the client
+        uses a Send-Document operation. Each Send- Document Request
+        contains the entire stream of document data for one document.
+
+        If the Printer supports this operation but does not support
+        multiple documents per job, the Printer MUST reject subsequent
+        Send-Document operations supplied with data and return the
+        'server-error-multiple- document-jobs-not-supported'. However,
+        the Printer MUST accept the first document with a 'true' or
+        'false' value for the 'last-document' operation attribute (see
+        below), so that clients MAY always submit one document jobs
+        with a 'false' value for 'last-document' in the first
+        Send-Document and a 'true' for 'last-document' in the second
+        Send-Document (with no data).
+        
+        Since the Create-Job and the send operations (Send-Document or
+        Send- URI operations) that follow could occur over an
+        arbitrarily long period of time for a particular job, a client
+        MUST send another send operation within an IPP Printer defined
+        minimum time interval after the receipt of the previous
+        request for the job. If a Printer object supports the
+        Create-Job and Send-Document operations, the Printer object
+        MUST support the 'multiple-operation-time-out' attribute (see
+        section 4.4.31). This attribute indicates the minimum number
+        of seconds the Printer object will wait for the next send
+        operation before taking some recovery action.
+
+        An IPP object MUST recover from an errant client that does not
+        supply a send operation, sometime after the minimum time
+        interval specified by the Printer object's
+        'multiple-operation-time-out' attribute.
+
+        Access Rights: The authenticated user (see section 8.3)
+        performing this operation must either be the job owner (as
+        determined in the Create-Job operation) or an operator or
+        administrator of the Printer object (see Sections 1 and
+        8.5). Otherwise, the IPP object MUST reject the operation and
+        return: 'client-error-forbidden', 'client-
+        error-not-authenticated', or 'client-error-not-authorized' as
+        appropriate.
+
+        Request
+        -------
+
+        Group 1: Operation Attributes
+            REQUIRED 'attributes-charset' 
+            REQUIRED 'attributes-natural-language' 
+            REQUIRED 'job-id' (uri)
+            OPTIONAL 'printer-uri' (uri)
+            OPTIONAL 'requesting-user-name' (name(MAX))
+            OPTIONAL 'document-name' (name(MAX))
+            OPTIONAL 'compression' (type3 keyword)
+            OPTIONAL 'document-format' (mimeMediaType)
+            OPTIONAL 'document-natural-language' (naturalLanguage)
+            OPTIONAL 'last-document' (boolean)
+        Group 2: Document Content
+            
+        Response
+        --------
+
+        Group 1: Operation Attributes
+            OPTIONAL 'status-message' (text(255))
+            OPTIONAL 'detailed-status-message' (text(MAX))
+            REQUIRED 'attributes-charset'
+            REQUIRED 'attributes-natural-language'
+        Group 2: Unsupported Attributes
+        Group 3: Job Object Attributes
+            REQUIRED 'job-uri' (uri)
+            REQUIRED 'job-id' (integer(1:MAX))
+            REQUIRED 'job-state' (type1 enum)
+            REQUIRED 'job-state-reasons' (1setOf type2 keyword)
+            OPTIONAL 'job-state-message' (text(MAX))
+            OPTIONAL 'number-of-intervening-jobs' (integer(0:MAX))
+
+        """
+        
         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
-
+        job_id = None
+        printer_uri = None
+        requesting_user_name = None
+        document_name = None
+        compression = None
+        document_format = None
+        document_natural_language = None
+        last_document = None
+
+        # required attributes
         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]
+        job_id = verify_attribute(operation['job-id'], ipp.JobId)[0]
 
         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:
+        last_document = verify_attribute(operation['last-document'], ipp.LastDocument)[0]
+        if not last_document:
             raise ipp.errors.ServerErrorMultipleJobsNotSupported
 
-        printer.send_document(job_id, request.data)
-        attrs = job.get_job_attributes()
+        # optional attributes
+        if 'printer-uri' in operation:
+            printer_uri = verify_attribute(operation['printer-uri'], ipp.PrinterUri)[0]
+            if printer_uri not in self.printer.uris:
+                raise ipp.errors.ClientErrorAttributes(
+                    str(operation['printer-uri']), operation['printer-uri'])
+
+        if 'requesting-user-name' in operation:
+            user_name = verify_attribute(
+                operation['requesting-user-name'], ipp.RequestingUserName)[0]
+
+        if 'document-name' in operation:
+            document_name = verify_attribute(
+                operation['document-name'], ipp.DocumentName)[0]
+
+        if 'compression' in operation:
+            compression = verify_attribute(
+                operation['compression'], ipp.Compression)[0]
+
+        if 'document-format' in operation:
+            document_format = verify_attribute(
+                operation['document-format'], ipp.DocumentFormat)[0]
+
+        if 'document-natural-language' in operation:
+            document_natural_language = verify_attribute(
+                operation['document_natural_language'],
+                ipp.DocumentNaturalLanguage)[0]
+
+        job = self.printer.get_job(job_id)
+        job.send_document(
+            request.data,
+            requesting_user_name=user_name,
+            document_name=document_name,
+            compression=compression,
+            document_format=document_format,
+            document_natural_language=document_natural_language,
+            last_document=last_document)
+
         response.attribute_groups.append(ipp.AttributeGroup(
-            ipp.AttributeTags.JOB, attrs))
+            ipp.AttributeTags.JOB, job.get_job_attributes()))
 
     @handler_for(ipp.OperationCodes.SEND_URI)
@@ -414,29 +603,98 @@
     @handler_for(ipp.OperationCodes.GET_JOB_ATTRIBUTES)
     def get_job_attributes(self, request, response):
+        """3.3.4 Get-Job-Attributes Operation
+
+        This REQUIRED operation allows a client to request the values
+        of attributes of a Job object and it is almost identical to
+        the Get- Printer-Attributes operation (see section 3.2.5). The
+        only differences are that the operation is directed at a Job
+        object rather than a Printer object, there is no
+        'document-format' operation attribute used when querying a Job
+        object, and the returned attribute group is a set of Job
+        object attributes rather than a set of Printer object
+        attributes.
+
+        For Jobs, the possible names of attribute groups are:
+          - 'job-template': the subset of the Job Template attributes
+            that apply to a Job object (the first column of the table
+            in Section 4.2) that the implementation supports for Job
+            objects.
+          - 'job-description': the subset of the Job Description
+            attributes specified in Section 4.3 that the
+            implementation supports for Job objects.
+          - 'all': the special group 'all' that includes all
+            attributes that the implementation supports for Job
+            objects.
+
+        Since a client MAY request specific attributes or named
+        groups, there is a potential that there is some overlap. For
+        example, if a client requests, 'job-name' and
+        'job-description', the client is actually requesting the
+        'job-name' attribute once by naming it explicitly, and once by
+        inclusion in the 'job-description' group. In such cases, the
+        Printer object NEED NOT return the attribute only once in the
+        response even if it is requested multiple times. The client
+        SHOULD NOT request the same attribute in multiple ways.
+
+        It is NOT REQUIRED that a Job object support all attributes
+        belonging to a group (since some attributes are
+        OPTIONAL). However it is REQUIRED that each Job object support
+        all these group names.
+
+        Request
+        -------
+
+        Group 1: Operation Attributes
+            REQUIRED 'attributes-charset' 
+            REQUIRED 'attributes-natural-language' 
+            REQUIRED 'job-id' (uri)
+            OPTIONAL 'printer-uri' (uri)
+            OPTIONAL 'requesting-user-name' (name(MAX))
+            OPTIONAL 'requested-attributes' (1setOf keyword)
+            
+        Response
+        --------
+
+        Group 1: Operation Attributes
+            OPTIONAL 'status-message' (text(255))
+            OPTIONAL 'detailed-status-message' (text(MAX))
+            REQUIRED 'attributes-charset'
+            REQUIRED 'attributes-natural-language'
+        Group 2: Unsupported Attributes
+        Group 3: Job Object Attributes
+
+        """
+        
         operation = request.attribute_groups[0]
 
-        # requested printer uri
-        if 'printer-uri' not in operation:
-            raise ipp.errors.ClientErrorBadRequest("Missing 'printer-uri' attribute")
-        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
-
+        job_id = None
+        printer_uri = None
+        requesting_user_name = None
+        requested_attributes = None
+
+        # required attributes
         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.get_job(job_id)
+        job_id = verify_attribute(operation['job-id'], ipp.JobId)[0]
+
+        # optional attributes
+        if 'printer-uri' in operation:
+            printer_uri = verify_attribute(operation['printer-uri'], ipp.PrinterUri)[0]
+            if printer_uri not in self.printer.uris:
+                raise ipp.errors.ClientErrorAttributes(
+                    str(operation['printer-uri']), operation['printer-uri'])
+
+        # optional attributes
+        if 'requesting-user-name' in operation:
+            user_name = verify_attribute(
+                operation['requesting-user-name'], ipp.RequestingUserName)[0]
+            
+        if 'requested-attributes' in operation:
+            requested_attributes = verify_attribute(
+                operation['requested-attributes'], ipp.RequestedAttributes, length=None)
 
         # get the job attributes and add them to the response
-        attrs = job.get_job_attributes(operation)
+        job = self.printer.get_job(job_id)
+        attrs = job.get_job_attributes(requested_attributes=requested_attributes)
         response.attribute_groups.append(ipp.AttributeGroup(
             ipp.AttributeTags.JOB, attrs))
@@ -487,9 +745,15 @@
 
         operation = request.attribute_groups[0]
-        printer = self.printer
+        requested_attributes = None
+        
+        if 'requested-attributes' in operation:
+            requested_attributes = verify_attribute(
+                operation['requested-attributes'], ipp.RequestedAttributes, length=None)
 
         # get attributes from the printer and add to response
+        attrs = self.printer.get_printer_attributes(
+            requested_attributes=requested_attributes)
         response.attribute_groups.append(ipp.AttributeGroup(
-            ipp.AttributeTags.PRINTER, printer.get_printer_attributes(operation)))
+            ipp.AttributeTags.PRINTER, attrs))
 
     @handler_for(ipp.OperationCodes.CUPS_GET_PRINTERS)
@@ -504,9 +768,7 @@
         """
 
-        operation = request.attribute_groups[0]
-
         # get attributes from the printer and add to response
         response.attribute_groups.append(ipp.AttributeGroup(
-            ipp.AttributeTags.PRINTER, self.printer.get_printer_attributes(operation)))
+            ipp.AttributeTags.PRINTER, self.printer.get_printer_attributes()))
 
     @handler_for(ipp.OperationCodes.CUPS_GET_CLASSES)
