diff --git a/CHANGES.md b/CHANGES.md index 192a84342..e334a079e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Features -------- * [#1696](https://github.com/java-native-access/jna/pull/1696): Add `LARGE_INTEGER.ByValue` to `LARGE_INTEGER` in `WinNT.java` - [@baier233](https://github.com/baier233). * [#1697](https://github.com/java-native-access/jna/pull/1697): Add WlanApi module - [@eranl](https://github.com/eranl). +* [#1718](https://github.com/java-native-access/jna/pull/1718): Add `Cups` to `c.s.j.p.unix` providing CUPS printing system bindings for destinations, jobs, options, and server configuration - [@dbwiddis](https://github.com/dbwiddis). Bug Fixes --------- diff --git a/contrib/platform/src/com/sun/jna/platform/unix/Cups.java b/contrib/platform/src/com/sun/jna/platform/unix/Cups.java new file mode 100644 index 000000000..a47fbfed9 --- /dev/null +++ b/contrib/platform/src/com/sun/jna/platform/unix/Cups.java @@ -0,0 +1,489 @@ +/* Copyright (c) 2026 Daniel Widdis, All Rights Reserved + * + * The contents of this file is dual-licensed under 2 + * alternative Open Source/Free licenses: LGPL 2.1 or later and + * Apache License 2.0. (starting with JNA version 4.0.0). + * + * You can freely decide which license you want to apply to + * the project. + * + * You may obtain a copy of the LGPL License at: + * + * http://www.gnu.org/licenses/licenses.html + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "LGPL2.1". + * + * You may obtain a copy of the Apache License at: + * + * http://www.apache.org/licenses/ + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "AL2.0". + */ +package com.sun.jna.platform.unix; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import com.sun.jna.NativeLong; +import com.sun.jna.Structure.FieldOrder; +import com.sun.jna.ptr.PointerByReference; + +/** + * Bindings for the CUPS (Common UNIX Printing System) API defined in + * {@code }. + *

+ * CUPS provides a portable printing layer for UNIX-based operating systems. It + * uses the Internet Printing Protocol (IPP) as the basis for managing print + * jobs, queues, and printers. + *

+ * Reference: CUPS Programming + * Manual + * + * @see OpenPrinting CUPS + */ +public interface Cups extends Library { + + Cups INSTANCE = Native.load("cups", Cups.class); + + // Printer state constants (IPP printer-state values) + + /** The printer is idle and ready to accept jobs. */ + int IPP_PRINTER_IDLE = 3; + /** The printer is currently processing a job. */ + int IPP_PRINTER_PROCESSING = 4; + /** The printer is stopped (paused or has an error). */ + int IPP_PRINTER_STOPPED = 5; + + // Printer type/capability bit constants (cups_ptype_t) + + /** Printer class (group of printers). */ + int CUPS_PRINTER_CLASS = 0x0001; + /** Remote printer or class. */ + int CUPS_PRINTER_REMOTE = 0x0002; + /** Can do B&W printing. */ + int CUPS_PRINTER_BW = 0x0004; + /** Can do color printing. */ + int CUPS_PRINTER_COLOR = 0x0008; + /** Can do two-sided printing. */ + int CUPS_PRINTER_DUPLEX = 0x0010; + /** Can staple output. */ + int CUPS_PRINTER_STAPLE = 0x0020; + /** Can do copies in hardware. */ + int CUPS_PRINTER_COPIES = 0x0040; + /** Can quickly collate copies. */ + int CUPS_PRINTER_COLLATE = 0x0080; + /** Can punch output. */ + int CUPS_PRINTER_PUNCH = 0x0100; + /** Can cover output. */ + int CUPS_PRINTER_COVER = 0x0200; + /** Can bind output. */ + int CUPS_PRINTER_BIND = 0x0400; + /** Can sort output. */ + int CUPS_PRINTER_SORT = 0x0800; + /** Can print on Letter/Legal/A4-size media. */ + int CUPS_PRINTER_SMALL = 0x1000; + /** Can print on Tabloid/B/C/A3/A2-size media. */ + int CUPS_PRINTER_MEDIUM = 0x2000; + /** Can print on D/E/A1/A0-size media. */ + int CUPS_PRINTER_LARGE = 0x4000; + /** Can print on rolls and custom-size media. */ + int CUPS_PRINTER_VARIABLE = 0x8000; + /** Default printer on network. */ + int CUPS_PRINTER_DEFAULT = 0x20000; + /** Fax queue. */ + int CUPS_PRINTER_FAX = 0x40000; + /** Printer is rejecting jobs. */ + int CUPS_PRINTER_REJECTING = 0x80000; + /** Printer is not shared. @since CUPS 1.2 */ + int CUPS_PRINTER_NOT_SHARED = 0x200000; + /** Printer requires authentication. @since CUPS 1.2 */ + int CUPS_PRINTER_AUTHENTICATED = 0x400000; + /** Printer supports maintenance commands. @since CUPS 1.2 */ + int CUPS_PRINTER_COMMANDS = 0x800000; + /** Printer was discovered (not configured locally). @since CUPS 1.2 */ + int CUPS_PRINTER_DISCOVERED = 0x1000000; + + // Job state constants (ipp_jstate_t) + + /** Job is waiting to be printed. */ + int IPP_JSTATE_PENDING = 3; + /** Job has been held for printing. */ + int IPP_JSTATE_HELD = 4; + /** Job is currently printing. */ + int IPP_JSTATE_PROCESSING = 5; + /** Job has been stopped. */ + int IPP_JSTATE_STOPPED = 6; + /** Job has been canceled. */ + int IPP_JSTATE_CANCELED = 7; + /** Job has been aborted due to an error. */ + int IPP_JSTATE_ABORTED = 8; + /** Job has completed successfully. */ + int IPP_JSTATE_COMPLETED = 9; + + // Job filter constants for cupsGetJobs2 + + /** Return all jobs regardless of state. */ + int CUPS_WHICHJOBS_ALL = -1; + /** Return active (pending, processing, held) jobs. */ + int CUPS_WHICHJOBS_ACTIVE = 0; + /** Return completed (stopped, canceled, aborted, completed) jobs. */ + int CUPS_WHICHJOBS_COMPLETED = 1; + + /** + * CUPS option (name/value pair) structure. + *

+ * Corresponds to {@code cups_option_t} in {@code }. + * + * @see cups_option_s + */ + @FieldOrder({ "name", "value" }) + class CupsOption extends Structure { + /** Name of option. */ + public String name; + /** Value of option. */ + public String value; + + public CupsOption() { + super(); + } + + public CupsOption(Pointer p) { + super(p); + read(); + } + } + + /** + * CUPS destination (printer or class) structure. + *

+ * Corresponds to {@code cups_dest_t} in {@code }. Each + * destination represents a printer or printer class known to the CUPS + * system. + *

+ * Starting with CUPS 1.2, the options array includes attributes such as + * "printer-info", "printer-is-accepting-jobs", "printer-is-shared", + * "printer-make-and-model", "printer-state", "printer-state-change-time", + * "printer-state-reasons", "printer-type", and "printer-uri-supported". + * + * @see cups_dest_s + */ + @FieldOrder({ "name", "instance", "is_default", "num_options", "options" }) + class CupsDest extends Structure { + /** Printer or class name. */ + public String name; + /** Local instance name or {@code null} for the primary instance. */ + public String instance; + /** Non-zero if this is the default destination. */ + public int is_default; + /** Number of options. */ + public int num_options; + /** Pointer to options array ({@code cups_option_t *}). */ + public Pointer options; + + public CupsDest() { + super(); + } + + public CupsDest(Pointer p) { + super(p); + read(); + } + } + + /** + * CUPS job structure. + *

+ * Corresponds to {@code cups_job_t} in {@code }. Represents a + * print job in the CUPS system. + * + * @see cups_job_s + */ + @FieldOrder({ "id", "dest", "title", "user", "format", "state", "size", "priority", "completed_time", + "creation_time", "processing_time" }) + class CupsJob extends Structure { + /** The job ID. */ + public int id; + /** Printer or class name. */ + public String dest; + /** Title of the job. */ + public String title; + /** User who submitted the job. */ + public String user; + /** Document format (MIME type). */ + public String format; + /** Job state ({@code ipp_jstate_t}). */ + public int state; + /** Size in kilobytes. */ + public int size; + /** Priority (1-100). */ + public int priority; + /** Time the job was completed ({@code time_t}, Unix epoch). */ + public NativeLong completed_time; + /** Time the job was created ({@code time_t}, Unix epoch). */ + public NativeLong creation_time; + /** Time the job began processing ({@code time_t}, Unix epoch). */ + public NativeLong processing_time; + + public CupsJob() { + super(); + } + + public CupsJob(Pointer p) { + super(p); + read(); + } + } + + /** + * Gets all available destinations (printers and classes) from the default + * server. + *

+ * The returned list includes options containing printer attributes. Use + * {@link #cupsGetOption} to retrieve specific attribute values from a + * destination's options. + *

+ * Use {@link #cupsFreeDests} to free the returned destination list. + * + * @param dests pointer to receive the destination array + * @return the number of destinations + * @see cupsGetDests2 + */ + int cupsGetDests(PointerByReference dests); + + /** + * Gets all available destinations (printers and classes) from the specified + * server. + *

+ * Pass {@code null} for the {@code http} parameter to use the default + * server connection (equivalent to {@code CUPS_HTTP_DEFAULT}). + *

+ * Use {@link #cupsFreeDests} to free the returned destination list. + * + * @param http connection to server or {@code null} for the default + * connection + * @param dests pointer to receive the destination array + * @return the number of destinations + * @see cupsGetDests2 + * @since CUPS 1.1.21 + */ + int cupsGetDests2(Pointer http, PointerByReference dests); + + /** + * Frees the memory used by a destination array returned by + * {@link #cupsGetDests} or {@link #cupsGetDests2}. + * + * @param num_dests number of destinations + * @param dests pointer to the destination array + * @see cupsFreeDests + */ + void cupsFreeDests(int num_dests, Pointer dests); + + /** + * Gets the named destination from a destination list. + *

+ * Use {@link #cupsGetDests} or {@link #cupsGetDests2} to get the list, then + * call this function to find a specific destination by name. + * + * @param name destination name or {@code null} for the default + * destination + * @param instance instance name or {@code null} + * @param num_dests number of destinations in the list + * @param dests pointer to the destination array + * @return pointer to the matching destination or {@code null} if not found + * @see cupsGetDest + */ + Pointer cupsGetDest(String name, String instance, int num_dests, Pointer dests); + + /** + * Gets options for the named destination, optimized for retrieving a single + * destination. + *

+ * This function is preferred over {@link #cupsGetDests2} followed by + * {@link #cupsGetDest} when you know the destination name or want the + * default destination. + *

+ * The returned destination must be freed using {@link #cupsFreeDests} with a + * {@code num_dests} value of 1. + * + * @param http connection to server or {@code null} for the default + * connection + * @param name destination name or {@code null} for the default + * destination + * @param instance instance name or {@code null} + * @return pointer to the destination or {@code null} if not found + * @see cupsGetNamedDest + * @since CUPS 1.4 + */ + Pointer cupsGetNamedDest(Pointer http, String name, String instance); + + /** + * Gets the default printer name. + * + * @return the default printer name or {@code null} if no default is set + * @see cupsGetDest + */ + String cupsGetDefault(); + + /** + * Gets an option value from a destination's options array. + *

+ * Common option names include "printer-info", "printer-state", + * "printer-make-and-model", "printer-type", "printer-state-reasons", + * "printer-is-accepting-jobs", and "printer-uri-supported". + * + * @param name option name to look up + * @param num_options number of options in the array + * @param options pointer to the options array + * @return the option value or {@code null} if not found + * @see cupsGetOption + */ + String cupsGetOption(String name, int num_options, Pointer options); + + /** + * Adds an option to an options array. + *

+ * If the named option already exists, its value is replaced. + * + * @param name option name + * @param value option value + * @param num_options current number of options + * @param options pointer to the options array (updated on return) + * @return the new number of options + * @see cupsAddOption + */ + int cupsAddOption(String name, String value, int num_options, PointerByReference options); + + /** + * Frees the memory used by an options array. + * + * @param num_options number of options + * @param options pointer to the options array + * @see cupsFreeOptions + */ + void cupsFreeOptions(int num_options, Pointer options); + + /** + * Gets the jobs from the specified server. + *

+ * Use {@link #cupsFreeJobs} to free the returned job array. + * + * @param http connection to server or {@code null} for the default + * connection + * @param jobs pointer to receive the job array + * @param name destination name or {@code null} for all destinations + * @param myjobs 0 for all users' jobs, 1 for only the current user's + * jobs + * @param whichjobs one of {@link #CUPS_WHICHJOBS_ALL}, + * {@link #CUPS_WHICHJOBS_ACTIVE}, or + * {@link #CUPS_WHICHJOBS_COMPLETED} + * @return the number of jobs + * @see cupsGetJobs2 + * @since CUPS 1.1.21 + */ + int cupsGetJobs2(Pointer http, PointerByReference jobs, String name, int myjobs, int whichjobs); + + /** + * Frees the memory used by a job array returned by {@link #cupsGetJobs2}. + * + * @param num_jobs number of jobs + * @param jobs pointer to the job array + * @see cupsFreeJobs + */ + void cupsFreeJobs(int num_jobs, Pointer jobs); + + /** + * Cancels a job on a destination. + * + * @param http connection to server or {@code null} for the default + * connection + * @param name destination name + * @param job_id the job ID to cancel + * @return 0 on success, non-zero on failure + * @see cupsCancelDestJob + */ + int cupsCancelJob(String name, int job_id); + + /** + * Returns the hostname or address of the current CUPS server. + *

+ * The default server comes from the {@code CUPS_SERVER} environment + * variable, then {@code ~/.cups/client.conf}, and finally + * {@code /etc/cups/client.conf}. If not set, the default is + * "localhost" or a domain socket path. + * + * @return the server name + * @see cupsServer + */ + String cupsServer(); + + /** + * Sets the default CUPS server name and port. + *

+ * The server string can be a fully-qualified hostname, a numeric IPv4 or + * IPv6 address, or a domain socket pathname. Hostnames and numeric IP + * addresses can optionally be followed by a colon and port number. Pass + * {@code null} to restore the default server. + * + * @param server server name or {@code null} to restore default + * @see cupsSetServer + */ + void cupsSetServer(String server); + + /** + * Returns the current user's name as known to CUPS. + *

+ * Note: The current user name is tracked separately for each thread. + * + * @return the user name + * @see cupsUser + */ + String cupsUser(); + + /** + * Sets the default user name for CUPS operations. + *

+ * Pass {@code null} to restore the default user name. Note: The user name + * is tracked per-thread. + * + * @param user user name or {@code null} to restore default + * @see cupsSetUser + */ + void cupsSetUser(String user); + + /** + * Returns the last IPP status code from a CUPS operation. + * + * @return the IPP status code + */ + int cupsLastError(); + + /** + * Returns a human-readable message for the last IPP error. + * + * @return the error message string + */ + String cupsLastErrorString(); +} diff --git a/contrib/platform/test/com/sun/jna/platform/CupsTest.java b/contrib/platform/test/com/sun/jna/platform/CupsTest.java new file mode 100644 index 000000000..5f8343d32 --- /dev/null +++ b/contrib/platform/test/com/sun/jna/platform/CupsTest.java @@ -0,0 +1,231 @@ +/* Copyright (c) 2026 Daniel Widdis, All Rights Reserved + * + * The contents of this file is dual-licensed under 2 + * alternative Open Source/Free licenses: LGPL 2.1 or later and + * Apache License 2.0. (starting with JNA version 4.0.0). + * + * You can freely decide which license you want to apply to + * the project. + * + * You may obtain a copy of the LGPL License at: + * + * http://www.gnu.org/licenses/licenses.html + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "LGPL2.1". + * + * You may obtain a copy of the Apache License at: + * + * http://www.apache.org/licenses/ + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "AL2.0". + */ +package com.sun.jna.platform; + +import com.sun.jna.Platform; +import com.sun.jna.platform.unix.Cups; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.PointerByReference; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import junit.framework.TestCase; + +/** + * Exercise the {@link Cups} class. + */ +public class CupsTest extends TestCase { + + private boolean cupsAvailable; + + @Override + protected void setUp() { + if (Platform.isWindows()) { + cupsAvailable = false; + return; + } + try { + assertNotNull(Cups.INSTANCE); + cupsAvailable = true; + } catch (UnsatisfiedLinkError e) { + // CUPS may not be installed on all systems + System.out.println("Cups not available: " + e.getMessage()); + cupsAvailable = false; + } + } + + public void testCupsServer() { + if (!cupsAvailable) { + return; + } + String server = Cups.INSTANCE.cupsServer(); + assertNotNull("CUPS server should not be null", server); + assertTrue("CUPS server should not be empty", server.length() > 0); + System.out.println("CUPS server: " + server); + } + + public void testCupsUser() { + if (!cupsAvailable) { + return; + } + String user = Cups.INSTANCE.cupsUser(); + assertNotNull("CUPS user should not be null", user); + assertTrue("CUPS user should not be empty", user.length() > 0); + System.out.println("CUPS user: " + user); + } + + public void testGetDests() { + if (!cupsAvailable) { + return; + } + PointerByReference destsRef = new PointerByReference(); + int numDests = Cups.INSTANCE.cupsGetDests(destsRef); + try { + assertTrue("Number of destinations should be non-negative", numDests >= 0); + System.out.println("Number of CUPS destinations: " + numDests); + + if (numDests > 0) { + Pointer destsPtr = destsRef.getValue(); + assertNotNull("Destinations pointer should not be null", destsPtr); + + // Read the first destination + Cups.CupsDest[] dests = (Cups.CupsDest[]) new Cups.CupsDest(destsPtr).toArray(numDests); + Cups.CupsDest dest = dests[0]; + assertNotNull("First destination name should not be null", dest.name); + System.out.println("First destination: " + dest.name + + (dest.is_default != 0 ? " (default)" : "")); + + // Test cupsGetOption on the first destination + if (dest.num_options > 0) { + String printerInfo = Cups.INSTANCE.cupsGetOption( + "printer-info", dest.num_options, dest.options); + // printer-info may be null if not set, that's OK + System.out.println(" printer-info: " + printerInfo); + + String printerState = Cups.INSTANCE.cupsGetOption( + "printer-state", dest.num_options, dest.options); + System.out.println(" printer-state: " + printerState); + + String printerType = Cups.INSTANCE.cupsGetOption( + "printer-type", dest.num_options, dest.options); + System.out.println(" printer-type: " + printerType + " " + mapPrinterTypeEnumNames(printerType).toString()); + } + } + } finally { + if (numDests > 0) { + Cups.INSTANCE.cupsFreeDests(numDests, destsRef.getValue()); + } + } + } + + public List mapPrinterTypeEnumNames(String printerType) throws RuntimeException, NumberFormatException { + try { + List result = new ArrayList<>(); + + int printerTypeInt = Integer.parseInt(printerType); + + for (Field f : Cups.class.getFields()) { + String fieldName = f.getName(); + if (fieldName.startsWith("CUPS_PRINTER_")) { + int val = f.getInt(null); + if (((printerTypeInt & val) == val)) { + result.add(fieldName); + } + } + } + + return result; + } catch (IllegalAccessException | IllegalArgumentException ex) { + throw new RuntimeException(ex); + } + } + + public void testGetDefault() { + if (!cupsAvailable) { + return; + } + // cupsGetDefault may return null if no default printer is configured + String defaultPrinter = Cups.INSTANCE.cupsGetDefault(); + System.out.println("Default printer: " + defaultPrinter); + } + + public void testGetJobs() { + if (!cupsAvailable) { + return; + } + PointerByReference jobsRef = new PointerByReference(); + int numJobs = Cups.INSTANCE.cupsGetJobs2(null, jobsRef, null, 0, + Cups.CUPS_WHICHJOBS_ALL); + try { + // cupsGetJobs2 may return -1 if the CUPS scheduler is not running + if (numJobs < 0) { + System.out.println("CUPS scheduler not running, skipping job test"); + return; + } + System.out.println("Number of CUPS jobs: " + numJobs); + + if (numJobs > 0) { + Pointer jobsPtr = jobsRef.getValue(); + assertNotNull("Jobs pointer should not be null", jobsPtr); + + Cups.CupsJob[] jobs = (Cups.CupsJob[]) new Cups.CupsJob(jobsPtr).toArray(numJobs); + Cups.CupsJob job = jobs[0]; + assertTrue("Job ID should be positive", job.id > 0); + assertNotNull("Job destination should not be null", job.dest); + System.out.println("First job: id=" + job.id + " dest=" + job.dest + + " title=" + job.title + " state=" + job.state); + } + } finally { + if (numJobs > 0) { + Cups.INSTANCE.cupsFreeJobs(numJobs, jobsRef.getValue()); + } + } + } + + public void testSetAndRestoreServer() { + if (!cupsAvailable) { + return; + } + String originalServer = Cups.INSTANCE.cupsServer(); + try { + Cups.INSTANCE.cupsSetServer("localhost"); + String newServer = Cups.INSTANCE.cupsServer(); + assertEquals("Server should be updated", "localhost", newServer); + } finally { + // Restore original + Cups.INSTANCE.cupsSetServer(null); + } + } + + public void testGetNamedDest() { + if (!cupsAvailable) { + return; + } + // Get the default destination (may be null if none configured) + Pointer destPtr = Cups.INSTANCE.cupsGetNamedDest(null, null, null); + if (destPtr != null) { + try { + Cups.CupsDest dest = new Cups.CupsDest(destPtr); + assertNotNull("Named dest name should not be null", dest.name); + System.out.println("Default named dest: " + dest.name); + } finally { + Cups.INSTANCE.cupsFreeDests(1, destPtr); + } + } else { + System.out.println("No default destination configured"); + } + } + + public void testLastError() { + if (!cupsAvailable) { + return; + } + // Just verify these don't crash; the error state depends on prior calls + int errorCode = Cups.INSTANCE.cupsLastError(); + assertTrue("Error code should be non-negative", errorCode >= 0); + String errorString = Cups.INSTANCE.cupsLastErrorString(); + assertNotNull("Error string should not be null", errorString); + } +}