From ec8de8b656f896e2912b5487d314f3b921053a4e Mon Sep 17 00:00:00 2001 From: Gerhard Muth Date: Tue, 10 Oct 2023 21:22:11 +0200 Subject: [PATCH] Setup IPP printer refactored --- README.md | 26 +++-- .../kotlin/de/gmuth/ipp/client/CupsClient.kt | 107 +++++++++--------- .../kotlin/de/gmuth/ipp/client/IppClient.kt | 5 +- .../gmuth/ipp/client/IppEventNotification.kt | 2 +- src/main/kotlin/de/gmuth/ipp/client/IppJob.kt | 1 + .../kotlin/de/gmuth/ipp/client/IppPrinter.kt | 6 +- .../de/gmuth/ipp/client/IppSubscription.kt | 2 +- .../kotlin/de/gmuth/ipp/core/IppMessage.kt | 2 +- .../de/gmuth/ipp/client/IppClientMock.kt | 5 +- 9 files changed, 82 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index d06942a4..df156888 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ipp-client +# ipp-client 3.0 A client implementation of the ipp protocol for java and kotlin. RFCs [8010](https://tools.ietf.org/html/rfc8010), @@ -103,7 +103,7 @@ val file = File("A4-blank.pdf") val ippClient = IppClient() val request = IppRequest(IppOperation.PrintJob, uri).apply { - // constructor adds 'attributes-charset', 'attributes-natural-language' and 'printer-uri' + // Constructor adds 'attributes-charset', 'attributes-natural-language' and 'printer-uri' operationGroup.attribute("document-format", IppTag.MimeMediaType, "application/pdf") documentInputStream = FileInputStream(file) } @@ -117,33 +117,41 @@ Use the `CupsClient` to connect to a CUPS server. If you want to access a cups queue you can construct an `IppPrinter` from it's uri. ```kotlin -// connect to default ipp://localhost:631 +// Connect to default ipp://localhost:631 val cupsClient = CupsClient() -// credentials (e.g. for remote connections) +// Credentials (e.g. for remote connections) cupsClient.basicAuth("admin", "secret") -// list all queues +// List all queues cupsClient.getPrinters().forEach { println("${it.name} -> ${it.printerUri}") } -// list all completed jobs for queue +// List all completed jobs for queue cupsClient.getPrinter("ColorJet_HP") .getJobs(WhichJobs.Completed) .forEach { println(it) } -// default printer +// Default printer val defaultPrinter = cupsClient.getDefault() -// check capability +// Check capability if (defaultPrinter.hasCapability(Capability.CanPrintInColor)) { println("${defaultPrinter.name} can print in color") } -// get canceled jobs and save documents +// Get canceled jobs and save documents cupsClient.getJobsAndSaveDocuments(WhichJobs.Canceled) +// Setup IPP Everywhere Printer +cupsClient.setupIppEverywherePrinter( + "myprinter", + URI.create("ipp://myprinter.local:631/ipp/print"), + "My description", + "My location" +) + ``` ### Print jpeg to 2" label printer diff --git a/src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt b/src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt index 99987def..e611a8c3 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt @@ -4,8 +4,8 @@ package de.gmuth.ipp.client * Copyright (c) 2020-2023 Gerhard Muth */ -import de.gmuth.ipp.client.WhichJobs.All import de.gmuth.ipp.client.IppExchangeException.ClientErrorNotFoundException +import de.gmuth.ipp.client.WhichJobs.All import de.gmuth.ipp.core.IppOperation import de.gmuth.ipp.core.IppOperation.* import de.gmuth.ipp.core.IppRequest @@ -115,22 +115,29 @@ class CupsClient( printerInfo: String?, printerLocation: String?, ppdName: String? // virtual PPD 'everywhere' is supported asynchronous - ) = exchange( - cupsPrinterRequest( - CupsCreateLocalPrinter, - printerName, - deviceUri, - printerInfo, - printerLocation, - ppdName - ) - ) + ): IppPrinter { + require(deviceUri.scheme.startsWith("ipp")) { "uri scheme unsupported: $deviceUri" } + require(!printerName.contains("-")) { "printerName must not contain '-'" } + exchange( + cupsPrinterRequest( + CupsCreateLocalPrinter, + printerName, + deviceUri, + printerInfo, + printerLocation, + ppdName + ) + ).run { + log.info { "$statusMessage ${printerGroup["printer-uri-supported"]!!.values}" } + return IppPrinter(printerGroup, ippClient) + } + } // -------------------------------------- // Build request for a named CUPS printer // -------------------------------------- - internal fun cupsPrinterRequest( + protected fun cupsPrinterRequest( operation: IppOperation, printerName: String, deviceUri: URI? = null, @@ -167,7 +174,7 @@ class CupsClient( // Delegate to IppPrinter //----------------------- - internal val ippPrinter: IppPrinter by lazy { + protected val ippPrinter: IppPrinter by lazy { IppPrinter(cupsUri, ippClient = ippClient, getPrinterAttributesOnInit = false) .apply { workDirectory = cupsClientWorkDirectory } } @@ -207,45 +214,31 @@ class CupsClient( deviceUri: URI, printerInfo: String? = null, printerLocation: String? = null - ): IppPrinter { - - // validate ipp scheme - require(deviceUri.scheme.startsWith("ipp")) { "uri scheme unsupported: $deviceUri" } - - createLocalPrinter(printerName, deviceUri, printerInfo, printerLocation, ppdName = "everywhere").apply { - log.info { - "$statusMessage ${ - printerGroup.getValues>("printer-uri-supported").joinToString(",") - }" - } - } - - return getPrinter(printerName).apply { - - // https://github.com/apple/cups/issues/5919 - log.info { "Waiting for CUPS to generate IPP Everywhere PPD." } - log.info { toString() } - do { - Thread.sleep(1000) - updateAttributes("printer-make-and-model") - } while (!makeAndModel.text.lowercase().contains("everywhere")) - log.info { toString() } - - // make printer permanent - exchange( - cupsPrinterRequest(CupsAddModifyPrinter, printerName).apply { - createAttributesGroup(Printer).run { - attribute("printer-is-temporary", IppTag.Boolean, false) - } + ) = createLocalPrinter( + printerName, + deviceUri, + printerInfo, + printerLocation, + ppdName = "everywhere" + ).apply { + updateAttributes("printer-name") + log.info(toString()) + log.info { "CUPS now generates IPP Everywhere PPD." } + do { // https://github.com/apple/cups/issues/5919 + updateAttributes("printer-make-and-model") + } while (!makeAndModel.text.lowercase().contains("everywhere")) + log.info { "Make printer permanent." } + exchange( + cupsPrinterRequest(CupsAddModifyPrinter, printerName).apply { + createAttributesGroup(Printer).run { + attribute("printer-is-temporary", IppTag.Boolean, false) } - ) - - // make printer operational - enable() - resume() - updateAttributes() - log.info { toString() } - } + } + ) + log.info { "Make printer operational." } + enable() + resume() + updateAttributes() } // --------------------------- @@ -268,7 +261,7 @@ class CupsClient( "job-name", "job-state", "job-state-reasons", if (version < "1.6.0") "document-count" else "number-of-documents" ) - // wired: do not modify above set + // weird: do not modify above set // job-originating-user-name is missing when document-count or job-originating-host-name ist requested // once hidden in response, wait for one minute and user-name should show up again ) @@ -284,7 +277,15 @@ class CupsClient( } } .apply { - with(jobOwners) { log.info { "Found $size job ${if (size <= 1) "owner" else "owners"}: ${joinToString(", ")}" } } + with(jobOwners) { + log.info { + "Found $size job ${if (size <= 1) "owner" else "owners"}: ${ + joinToString( + ", " + ) + }" + } + } log.info { "Found $size jobs (which=$whichJobs) where $numberOfJobsWithoutDocuments jobs have no documents" } log.info { "Saved $numberOfSavedDocuments documents of ${size.minus(numberOfJobsWithoutDocuments.toInt())} jobs with documents to directory: ${ippPrinter.workDirectory}" } } diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppClient.kt b/src/main/kotlin/de/gmuth/ipp/client/IppClient.kt index 4f8a2259..348d45c6 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppClient.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppClient.kt @@ -44,7 +44,7 @@ open class IppClient(val config: IppConfig = IppConfig()) { } //----------------- - // build IppRequest + // Build IppRequest //----------------- protected val requestCounter = AtomicInteger(1) @@ -107,7 +107,6 @@ open class IppClient(val config: IppConfig = IppConfig()) { errorStream } return decodeContentStream(request, responseCode, responseContentStream) - } } @@ -152,7 +151,7 @@ open class IppClient(val config: IppConfig = IppConfig()) { ) = when { responseCode == 401 -> with(request) { - "User '$requestingUserName' is unauthorized for operation '$operation'" + "User \"$requestingUserName\" is not authorized for operation $operation on $printerUri" } responseCode == 426 -> { diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppEventNotification.kt b/src/main/kotlin/de/gmuth/ipp/client/IppEventNotification.kt index 8995704e..e4724175 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppEventNotification.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppEventNotification.kt @@ -59,7 +59,7 @@ class IppEventNotification( @SuppressWarnings("kotlin:S3776") override fun toString() = StringBuilder().run { - append("event #$sequenceNumber:") + append("Event #$sequenceNumber:") append(" [$subscribedEvent] $text") with(attributes) { if (containsKey("notify-job-id")) append(", job #$jobId") diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppJob.kt b/src/main/kotlin/de/gmuth/ipp/client/IppJob.kt index 35af7fa8..cdec6523 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppJob.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppJob.kt @@ -289,6 +289,7 @@ class IppJob( // https://openprinting.github.io/cups/doc/man-cupsd.conf.html //------------------------------------------------------------------------------------- + @JvmOverloads fun cupsGetDocument(documentNumber: Int = 1): IppDocument { log.fine { "cupsGetDocument #$documentNumber for job #$id" } val response = exchange(ippRequest(CupsGetDocument).apply { diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt b/src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt index 1419840c..cee65e05 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt @@ -485,10 +485,12 @@ open class IppPrinter( override fun toString() = StringBuilder("IppPrinter:").run { if (attributes.containsKey("printer-name")) append(" name=$name") - append(", makeAndModel=$makeAndModel") + if (attributes.containsKey("printer-make-and-model")) append(", makeAndModel=$makeAndModel") append(", state=$state, stateReasons=$stateReasons") stateMessage?.let { if (it.text.isNotEmpty()) append(", stateMessage=$stateMessage") } if (attributes.containsKey("printer-is-accepting-jobs")) append(", isAcceptingJobs=$isAcceptingJobs") + if (attributes.containsKey("printer-location")) append(", location=$location") + if (attributes.containsKey("printer-info")) append(", info=$info") toString() } @@ -683,7 +685,7 @@ open class IppPrinter( waitForTermination() } - if(isAborted()) log(log) + if (isAborted()) log(log) } } diff --git a/src/main/kotlin/de/gmuth/ipp/client/IppSubscription.kt b/src/main/kotlin/de/gmuth/ipp/client/IppSubscription.kt index 35bb5561..7af5c749 100644 --- a/src/main/kotlin/de/gmuth/ipp/client/IppSubscription.kt +++ b/src/main/kotlin/de/gmuth/ipp/client/IppSubscription.kt @@ -151,7 +151,7 @@ class IppSubscription( // Logging // ------- - override fun toString() = StringBuilder("subscription #$id:").run { + override fun toString() = StringBuilder("Subscription #$id:").run { if (hasJobId()) append(" job #$jobId") if (attributes.containsKey("notify-events")) append(" events=${events.joinToString(",")}") if (attributes.containsKey("notify-time-interval")) append(" time-interval=$timeInterval") diff --git a/src/main/kotlin/de/gmuth/ipp/core/IppMessage.kt b/src/main/kotlin/de/gmuth/ipp/core/IppMessage.kt index bba4a285..a29e61a4 100644 --- a/src/main/kotlin/de/gmuth/ipp/core/IppMessage.kt +++ b/src/main/kotlin/de/gmuth/ipp/core/IppMessage.kt @@ -13,7 +13,7 @@ import java.util.logging.Logger.getLogger abstract class IppMessage() { - internal val log = getLogger(javaClass.name) + protected val log = getLogger(javaClass.name) var code: Int? = null // unsigned short (16 bits) var requestId: Int? = null var version: String? = null diff --git a/src/test/kotlin/de/gmuth/ipp/client/IppClientMock.kt b/src/test/kotlin/de/gmuth/ipp/client/IppClientMock.kt index 4c08b451..a35775e0 100644 --- a/src/test/kotlin/de/gmuth/ipp/client/IppClientMock.kt +++ b/src/test/kotlin/de/gmuth/ipp/client/IppClientMock.kt @@ -10,12 +10,9 @@ import de.gmuth.ipp.core.IppStatus import de.gmuth.log.Logging import java.io.ByteArrayOutputStream import java.io.File -import java.io.OutputStream import java.net.URI -class IppClientMock( - var directory: String = "printers" -) : IppClient() { +class IppClientMock(var directory: String = "printers") : IppClient() { init { Logging.configure()