Skip to content

Commit

Permalink
Setup IPP printer refactored
Browse files Browse the repository at this point in the history
  • Loading branch information
gmuth committed Oct 10, 2023
1 parent 547d44e commit ec8de8b
Show file tree
Hide file tree
Showing 9 changed files with 82 additions and 74 deletions.
26 changes: 17 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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),
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down
107 changes: 54 additions & 53 deletions src/main/kotlin/de/gmuth/ipp/client/CupsClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }
}
Expand Down Expand Up @@ -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<List<URI>>("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()
}

// ---------------------------
Expand All @@ -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
)
Expand All @@ -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}" }
}
Expand Down
5 changes: 2 additions & 3 deletions src/main/kotlin/de/gmuth/ipp/client/IppClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ open class IppClient(val config: IppConfig = IppConfig()) {
}

//-----------------
// build IppRequest
// Build IppRequest
//-----------------

protected val requestCounter = AtomicInteger(1)
Expand Down Expand Up @@ -107,7 +107,6 @@ open class IppClient(val config: IppConfig = IppConfig()) {
errorStream
}
return decodeContentStream(request, responseCode, responseContentStream)

}
}

Expand Down Expand Up @@ -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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/de/gmuth/ipp/client/IppJob.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions src/main/kotlin/de/gmuth/ipp/client/IppPrinter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down Expand Up @@ -683,7 +685,7 @@ open class IppPrinter(
waitForTermination()
}

if(isAborted()) log(log)
if (isAborted()) log(log)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/de/gmuth/ipp/client/IppSubscription.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/de/gmuth/ipp/core/IppMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 1 addition & 4 deletions src/test/kotlin/de/gmuth/ipp/client/IppClientMock.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit ec8de8b

Please sign in to comment.