Index: server/lib/gutenbach/server/job.py
===================================================================
--- server/lib/gutenbach/server/job.py	(revision d21198f3d35a65b2e9854e95aed74003e47acb36)
+++ server/lib/gutenbach/server/job.py	(revision 34a4e5d730962d108163c14fafd961f93db9c5db)
@@ -69,4 +69,13 @@
 
     @property
+    def uri(self):
+        return self.uris[0]
+
+    @property
+    def uris(self):
+        return ["ipp://localhost/jobs/%d" % self.id,
+                "ipp://localhost:8000/jobs/%d" % self.id]
+
+    @property
     def creator(self):
         """The user who created the job; analogous to the IPP
@@ -118,5 +127,7 @@
     @property
     def is_finished(self):
-        return self.state != States.PENDING and self.state != States.PROCESSING
+        return self.state != States.PENDING and \
+               self.state != States.PROCESSING and \
+               self.state != States.HELD
         
     ######################################################################
@@ -141,15 +152,21 @@
 
     def pause(self):
-        if self.player:
+        if self.is_playing:
             self.player.mplayer_pause()
-
-    def stop(self):
-        if self.player:
-            self.player.callback = self._stopped
+            self.state = States.STOPPED
+
+    def cancel(self):
+        if self.is_playing:
+            self.player.callback = self._canceled
             self.player.mplayer_stop()
+        self.state = States.CANCELLED
+
+    def abort(self):
+        if self.is_playing:
+            self.player.callback = self._aborted
+            self.player.mplayer_stop()
+        self.state = states.ABORTED
 
     def _completed(self):
-        if self.state != States.PROCESSING:
-            raise InvalidJobStateException(self.state)
         logger.info("completed job %s" % str(self))
 	self.state = States.COMPLETE
@@ -157,20 +174,9 @@
 
     def _canceled(self):
-        if self.state != States.PROCESSING:
-            raise InvalidJobStateException(self.state)
         logger.info("canceled job %s" % str(self))
         self.state = States.CANCELLED
         self.player = None
 
-    def _stopped(self):
-        if self.state != States.PROCESSING:
-            raise InvalidJobStateException(self.state)
-        logger.info("stopped job %s" % str(self))
-        self.state = States.STOPPED
-        self.player = None
-
     def _aborted(self):
-        if self.state != States.PROCESSING:
-            raise InvalidJobStateException(self.state)
         logger.info("aborted job %s" % str(self))
         self.state = States.ABORTED
Index: server/lib/gutenbach/server/printer.py
===================================================================
--- server/lib/gutenbach/server/printer.py	(revision d21198f3d35a65b2e9854e95aed74003e47acb36)
+++ server/lib/gutenbach/server/printer.py	(revision 34a4e5d730962d108163c14fafd961f93db9c5db)
@@ -131,7 +131,5 @@
                     job_id = heapq.heappop(self.pending_jobs)
                     self.current_job = self.get_job(job_id)
-                    print "before play"
                     self.current_job.play()
-                    print "after play"
                 except IndexError:
                     self.current_job = None
@@ -265,6 +263,7 @@
         # Filter by the which-jobs attribute
         if which_jobs is None:
-            jobs = self.jobs.values()
-        elif which_jobs == "completed":
+            which_jobs = "not-completed"
+
+        if which_jobs == "completed":
             jobs = [self.jobs[job_id] for job_id in self.finished_jobs]
         elif which_jobs == "not-completed":
Index: server/lib/gutenbach/server/requests.py
===================================================================
--- server/lib/gutenbach/server/requests.py	(revision eee389a2ef90375dc33268c1828d860ef7be2907)
+++ server/lib/gutenbach/server/requests.py	(revision 34a4e5d730962d108163c14fafd961f93db9c5db)
@@ -1,2 +1,3 @@
+from . import InvalidJobException, InvalidPrinterStateException, InvalidJobStateException
 import gutenbach.ipp as ipp
 import logging
@@ -446,5 +447,101 @@
     @handler_for(ipp.OperationCodes.CANCEL_JOB)
     def cancel_job(self, request, response):
-        raise ipp.errors.ServerErrorOperationNotSupported
+        """3.3.3 Cancel-Job Operation
+
+        This REQUIRED operation allows a client to cancel a Print Job from
+        the time the job is created up to the time it is completed, canceled,
+        or aborted. Since a Job might already be printing by the time a
+        Cancel-Job is received, some media sheet pages might be printed
+        before the job is actually terminated.
+
+        The IPP object MUST accept or reject the request based on the job's
+        current state and transition the job to the indicated new state as
+        follows:
+
+        Current State       New State           Response
+        -----------------------------------------------------------------
+        pending             canceled            successful-ok
+        pending-held        canceled            successful-ok
+        processing          canceled            successful-ok
+        processing          processing          successful-ok               See Rule 1
+        processing          processing          client-error-not-possible   See Rule 2
+        processing-stopped  canceled            successful-ok
+        processing-stopped  processing-stopped  successful-ok               See Rule 1
+        processing-stopped  processing-stopped  client-error-not-possible   See Rule 2
+        completed           completed           client-error-not-possible
+        canceled            canceled            client-error-not-possible
+        aborted             aborted             client-error-not-possible
+
+        Rule 1: If the implementation requires some measurable time to
+        cancel the job in the 'processing' or 'processing-stopped' job
+        states, the IPP object MUST add the 'processing-to-stop-point'
+        value to the job's 'job-state-reasons' attribute and then
+        transition the job to the 'canceled' state when the processing
+        ceases (see section 4.3.8).
+
+        Rule 2: If the Job object already has the
+        'processing-to-stop-point' value in its 'job-state-reasons'
+        attribute, then the Printer object MUST reject a Cancel-Job
+        operation.
+
+        Access Rights: The authenticated user (see section 8.3)
+        performing this operation must either be the job owner 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' (integer(1:MAX)) and 'printer-uri' (uri)
+              -or-   'job-uri' (uri)
+            OPTIONAL 'requesting-user-name' (name(MAX))
+            OPTIONAL 'message' (text(127))
+            
+        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
+
+        """
+
+        operation = request.attribute_groups[0]
+
+        job_id = None
+        printer_uri = None
+        requesting_user_name = None
+        message = None
+
+        # required attributes
+        if 'job-id' in operation and 'printer-uri' in operation:
+            job_id = verify_attribute(operation['job-id'], ipp.JobId)[0]
+            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'])
+
+        elif 'job-uri' in operation:
+            job_uri = verify_attribute(operation['job-uri'], ipp.JobUri)[0]
+            job_id = int(job_uri.split("/")[-1])
+
+        if 'requesting-user-name' in operation:
+            user_name = verify_attribute(
+                operation['requesting-user-name'], ipp.RequestingUserName)[0]
+
+        try:
+            job = self.printer.get_job(job_id)
+            job.cancel()
+        except InvalidJobException:
+            raise ipp.errors.ClientErrorNotFound("bad job: %d" % job_id)
 
     @handler_for(ipp.OperationCodes.SEND_DOCUMENT)
@@ -504,6 +601,6 @@
             REQUIRED 'attributes-charset' 
             REQUIRED 'attributes-natural-language' 
-            REQUIRED 'job-id' (uri)
-            OPTIONAL 'printer-uri' (uri)
+            REQUIRED 'job-id' (integer(1:MAX)) and 'printer-uri' (uri)
+              -or-   'job-uri' (uri)
             OPTIONAL 'requesting-user-name' (name(MAX))
             OPTIONAL 'document-name' (name(MAX))
@@ -583,16 +680,20 @@
                 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)
+        try:
+            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)
+            attrs = job.get_job_attributes()
+        except InvalidJobException:
+            raise ipp.errors.ClientErrorNotFound("bad job: %d" % job_id)
 
         response.attribute_groups.append(ipp.AttributeGroup(
-            ipp.AttributeTags.JOB, job.get_job_attributes()))
+            ipp.AttributeTags.JOB, attrs))
 
     @handler_for(ipp.OperationCodes.SEND_URI)
@@ -647,6 +748,6 @@
             REQUIRED 'attributes-charset' 
             REQUIRED 'attributes-natural-language' 
-            REQUIRED 'job-id' (uri)
-            OPTIONAL 'printer-uri' (uri)
+            REQUIRED 'job-id' (integer(1:MAX)) and 'printer-uri' (uri)
+              -or-   'job-uri' (uri)
             OPTIONAL 'requesting-user-name' (name(MAX))
             OPTIONAL 'requested-attributes' (1setOf keyword)
@@ -694,6 +795,10 @@
 
         # get the job attributes and add them to the response
-        job = self.printer.get_job(job_id)
-        attrs = job.get_job_attributes(requested_attributes=requested_attributes)
+        try:
+            job = self.printer.get_job(job_id)
+            attrs = job.get_job_attributes(requested_attributes=requested_attributes)
+        except InvalidJobException:
+            raise ipp.errors.ClientErrorNotFound("bad job: %d" % job_id)
+
         response.attribute_groups.append(ipp.AttributeGroup(
             ipp.AttributeTags.JOB, attrs))
