updated control access
This commit is contained in:
65
old code/tray/src/qz/communication/H4J_HidUtilities.java
Executable file
65
old code/tray/src/qz/communication/H4J_HidUtilities.java
Executable file
@@ -0,0 +1,65 @@
|
||||
package qz.communication;
|
||||
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.hid4java.HidDevice;
|
||||
import org.hid4java.HidManager;
|
||||
import org.hid4java.HidServices;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class H4J_HidUtilities {
|
||||
private static final HidServices service = HidManager.getHidServices();
|
||||
|
||||
public static List<HidDevice> getHidDevices() {
|
||||
return service.getAttachedHidDevices();
|
||||
}
|
||||
|
||||
public static JSONArray getHidDevicesJSON() throws JSONException {
|
||||
List<HidDevice> devices = getHidDevices();
|
||||
JSONArray devicesJSON = new JSONArray();
|
||||
|
||||
HashSet<String> unique = new HashSet<>();
|
||||
for(HidDevice device : devices) {
|
||||
JSONObject deviceJSON = new JSONObject();
|
||||
|
||||
deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId()))
|
||||
.put("productId", UsbUtil.toHexString(device.getProductId()))
|
||||
.put("usagePage", UsbUtil.toHexString((short)device.getUsagePage()))
|
||||
.put("serial", device.getSerialNumber())
|
||||
.put("manufacturer", device.getManufacturer())
|
||||
.put("product", device.getProduct());
|
||||
|
||||
String uid = String.format("v%sp%su%ss%s", deviceJSON.optString("vendorId"), deviceJSON.optString("productId"), deviceJSON.optString("usagePage"), deviceJSON.optString("serial"));
|
||||
if (!unique.contains(uid)) {
|
||||
devicesJSON.put(deviceJSON);
|
||||
unique.add(uid);
|
||||
}
|
||||
}
|
||||
|
||||
return devicesJSON;
|
||||
}
|
||||
|
||||
public static HidDevice findDevice(DeviceOptions dOpts) {
|
||||
if (dOpts.getVendorId() == null) {
|
||||
throw new IllegalArgumentException("Vendor ID cannot be null");
|
||||
}
|
||||
if (dOpts.getProductId() == null) {
|
||||
throw new IllegalArgumentException("Product ID cannot be null");
|
||||
}
|
||||
|
||||
List<HidDevice> devices = getHidDevices();
|
||||
for(HidDevice device : devices) {
|
||||
if (device.isVidPidSerial(dOpts.getVendorId(), dOpts.getProductId(), dOpts.getSerial())
|
||||
&& (dOpts.getUsagePage() == null || dOpts.getUsagePage() == device.getUsagePage())) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
154
old code/tray/src/qz/communication/PJHA_HidIO.java
Executable file
154
old code/tray/src/qz/communication/PJHA_HidIO.java
Executable file
@@ -0,0 +1,154 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import purejavahidapi.HidDevice;
|
||||
import purejavahidapi.HidDeviceInfo;
|
||||
import purejavahidapi.InputReportListener;
|
||||
import purejavahidapi.PureJavaHidApi;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.Vector;
|
||||
|
||||
public class PJHA_HidIO implements DeviceIO {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PJHA_HidIO.class);
|
||||
|
||||
private HidDeviceInfo deviceInfo;
|
||||
private HidDevice device;
|
||||
|
||||
private static final int BUFFER_SIZE = 32;
|
||||
private Vector<byte[]> dataBuffer;
|
||||
private boolean streaming;
|
||||
private DeviceOptions dOpts;
|
||||
private SocketConnection websocket;
|
||||
|
||||
public PJHA_HidIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||
this(PJHA_HidUtilities.findDevice(dOpts), dOpts, websocket);
|
||||
}
|
||||
|
||||
private PJHA_HidIO(HidDeviceInfo deviceInfo, DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||
this.dOpts = dOpts;
|
||||
this.websocket = websocket;
|
||||
if (deviceInfo == null) {
|
||||
throw new DeviceException("HID device could not be found");
|
||||
}
|
||||
|
||||
this.deviceInfo = deviceInfo;
|
||||
|
||||
dataBuffer = new Vector<byte[]>() {
|
||||
@Override
|
||||
public synchronized boolean add(byte[] e) {
|
||||
while(this.size() >= BUFFER_SIZE) {
|
||||
this.remove(0);
|
||||
}
|
||||
return super.add(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void open() throws DeviceException {
|
||||
if (!isOpen()) {
|
||||
try {
|
||||
device = PureJavaHidApi.openDevice(deviceInfo);
|
||||
device.setInputReportListener(new InputReportListener() {
|
||||
@Override
|
||||
public void onInputReport(HidDevice source, byte id, byte[] data, int len) {
|
||||
byte[] dataCopy = new byte[len];
|
||||
System.arraycopy(data, 0, dataCopy, 0, len);
|
||||
dataBuffer.add(dataCopy);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(IOException ex) {
|
||||
throw new DeviceException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return device != null;
|
||||
}
|
||||
|
||||
public void setStreaming(boolean active) {
|
||||
streaming = active;
|
||||
}
|
||||
|
||||
public boolean isStreaming() {
|
||||
return streaming;
|
||||
}
|
||||
|
||||
public String getVendorId() {
|
||||
return UsbUtil.toHexString(deviceInfo.getVendorId());
|
||||
}
|
||||
|
||||
public String getProductId() {
|
||||
return UsbUtil.toHexString(deviceInfo.getProductId());
|
||||
}
|
||||
|
||||
public byte[] readData(int responseSize, Byte unused) throws DeviceException {
|
||||
byte[] response = new byte[responseSize];
|
||||
if (dataBuffer.isEmpty()) {
|
||||
return new byte[0]; //no data received yet
|
||||
}
|
||||
|
||||
byte[] latestData = dataBuffer.remove(0);
|
||||
if (SystemUtilities.isWindows()) {
|
||||
//windows missing the leading byte
|
||||
System.arraycopy(latestData, 0, response, 1, Math.min(responseSize - 1, latestData.length));
|
||||
} else {
|
||||
System.arraycopy(latestData, 0, response, 0, Math.min(responseSize - 1, latestData.length));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public void sendData(byte[] data, Byte reportId) throws DeviceException {
|
||||
if (reportId == null) { reportId = (byte)0x00; }
|
||||
|
||||
int wrote = device.setOutputReport(reportId, data, data.length);
|
||||
if (wrote == -1) {
|
||||
throw new DeviceException("Failed to write to device");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getFeatureReport(int responseSize, Byte unused) throws DeviceException {
|
||||
byte[] response = new byte[responseSize];
|
||||
int read = device.getFeatureReport(response, responseSize);
|
||||
if (read == -1) {
|
||||
throw new DeviceException("Failed to read from device");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
|
||||
if (reportId == null) { reportId = (byte)0x00; }
|
||||
int wrote = device.setFeatureReport(reportId, data, data.length);
|
||||
|
||||
if (wrote == -1) {
|
||||
throw new DeviceException("Failed to write to device");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
setStreaming(false);
|
||||
// Remove orphaned reference
|
||||
websocket.removeDevice(dOpts);
|
||||
if (isOpen()) {
|
||||
try {
|
||||
device.setInputReportListener(null);
|
||||
device.close();
|
||||
}
|
||||
catch(IllegalStateException e) {
|
||||
log.warn("Device already closed");
|
||||
}
|
||||
}
|
||||
|
||||
device = null;
|
||||
}
|
||||
|
||||
}
|
||||
50
old code/tray/src/qz/communication/PJHA_HidListener.java
Executable file
50
old code/tray/src/qz/communication/PJHA_HidListener.java
Executable file
@@ -0,0 +1,50 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import purejavahidapi.DeviceRemovalListener;
|
||||
import purejavahidapi.HidDevice;
|
||||
import qz.ws.PrintSocketClient;
|
||||
import qz.ws.StreamEvent;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
|
||||
public class PJHA_HidListener implements DeviceListener, DeviceRemovalListener {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PJHA_HidListener.class);
|
||||
|
||||
private Session session;
|
||||
private HidDevice device;
|
||||
|
||||
|
||||
public PJHA_HidListener(Session session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public void setDevice(HidDevice device) {
|
||||
this.device = device;
|
||||
device.setDeviceRemovalListener(this);
|
||||
}
|
||||
|
||||
private StreamEvent createStreamAction(HidDevice device, String action) {
|
||||
return new StreamEvent(StreamEvent.Stream.HID, StreamEvent.Type.ACTION)
|
||||
.withData("vendorId", UsbUtil.toHexString(device.getHidDeviceInfo().getVendorId()))
|
||||
.withData("productId", UsbUtil.toHexString(device.getHidDeviceInfo().getProductId()))
|
||||
.withData("actionType", action);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (device != null) {
|
||||
device.setDeviceRemovalListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceRemoval(HidDevice device) {
|
||||
log.debug("Device detached: {}", device.getHidDeviceInfo().getProductString());
|
||||
PrintSocketClient.sendStream(session, createStreamAction(device, "Device Detached"), this);
|
||||
}
|
||||
}
|
||||
56
old code/tray/src/qz/communication/PJHA_HidUtilities.java
Executable file
56
old code/tray/src/qz/communication/PJHA_HidUtilities.java
Executable file
@@ -0,0 +1,56 @@
|
||||
package qz.communication;
|
||||
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import purejavahidapi.HidDeviceInfo;
|
||||
import purejavahidapi.PureJavaHidApi;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
import java.util.List;
|
||||
|
||||
public class PJHA_HidUtilities {
|
||||
|
||||
public static JSONArray getHidDevicesJSON() throws JSONException {
|
||||
List<HidDeviceInfo> devices = PureJavaHidApi.enumerateDevices();
|
||||
JSONArray devicesJSON = new JSONArray();
|
||||
|
||||
for(HidDeviceInfo device : devices) {
|
||||
JSONObject deviceJSON = new JSONObject();
|
||||
|
||||
deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId()))
|
||||
.put("productId", UsbUtil.toHexString(device.getProductId()))
|
||||
.put("usagePage", UsbUtil.toHexString(device.getUsagePage()))
|
||||
.put("serial", device.getSerialNumberString())
|
||||
.put("manufacturer", device.getManufacturerString())
|
||||
.put("product", device.getProductString());
|
||||
|
||||
devicesJSON.put(deviceJSON);
|
||||
}
|
||||
|
||||
return devicesJSON;
|
||||
}
|
||||
|
||||
public static HidDeviceInfo findDevice(DeviceOptions dOpts) {
|
||||
if (dOpts.getVendorId() == null) {
|
||||
throw new IllegalArgumentException("Vendor ID cannot be null");
|
||||
}
|
||||
if (dOpts.getProductId() == null) {
|
||||
throw new IllegalArgumentException("Product ID cannot be null");
|
||||
}
|
||||
|
||||
|
||||
List<HidDeviceInfo> devList = PureJavaHidApi.enumerateDevices();
|
||||
for(HidDeviceInfo device : devList) {
|
||||
if (device.getVendorId() == dOpts.getVendorId().shortValue() && device.getProductId() == dOpts.getProductId().shortValue()
|
||||
&& (dOpts.getUsagePage() == null || dOpts.getUsagePage().shortValue() == device.getUsagePage())
|
||||
&& (dOpts.getSerial() == null || dOpts.getSerial().equals(device.getSerialNumberString()))) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
297
old code/tray/src/qz/communication/SerialIO.java
Executable file
297
old code/tray/src/qz/communication/SerialIO.java
Executable file
@@ -0,0 +1,297 @@
|
||||
package qz.communication;
|
||||
|
||||
import jssc.*;
|
||||
import org.apache.commons.codec.binary.StringUtils;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.ByteArrayBuilder;
|
||||
import qz.utils.ByteUtilities;
|
||||
import qz.utils.DeviceUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author Tres
|
||||
*/
|
||||
public class SerialIO implements DeviceListener {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(SerialIO.class);
|
||||
|
||||
// Timeout to wait before giving up on reading the specified amount of bytes
|
||||
private static final int TIMEOUT = 1200;
|
||||
|
||||
private String portName;
|
||||
private SerialPort port;
|
||||
private SerialOptions serialOpts;
|
||||
|
||||
private ByteArrayBuilder data = new ByteArrayBuilder();
|
||||
|
||||
private SocketConnection websocket;
|
||||
|
||||
|
||||
/**
|
||||
* Controller for serial communications
|
||||
*
|
||||
* @param portName Port name to open, such as "COM1" or "/dev/tty0/"
|
||||
*/
|
||||
public SerialIO(String portName, SocketConnection websocket) {
|
||||
this.portName = portName;
|
||||
this.websocket = websocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the specified port name.
|
||||
*
|
||||
* @param opts Parsed serial options
|
||||
* @return Boolean indicating success.
|
||||
* @throws SerialPortException If the port fails to open.
|
||||
*/
|
||||
public boolean open(SerialOptions opts) throws SerialPortException {
|
||||
if (isOpen()) {
|
||||
log.warn("Serial port [{}] is already open", portName);
|
||||
return false;
|
||||
}
|
||||
|
||||
port = new SerialPort(portName);
|
||||
port.openPort();
|
||||
|
||||
serialOpts = new SerialOptions();
|
||||
setOptions(opts);
|
||||
|
||||
return port.isOpened();
|
||||
}
|
||||
|
||||
public void applyPortListener(SerialPortEventListener listener) throws SerialPortException {
|
||||
port.addEventListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Boolean indicating if port is currently open.
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return port != null && port.isOpened();
|
||||
}
|
||||
|
||||
public String processSerialEvent(SerialPortEvent event) {
|
||||
SerialOptions.ResponseFormat format = serialOpts.getResponseFormat();
|
||||
|
||||
try {
|
||||
// Receive data
|
||||
if (event.isRXCHAR()) {
|
||||
data.append(port.readBytes(event.getEventValue(), TIMEOUT));
|
||||
|
||||
String response = null;
|
||||
if (format.isBoundNewline()) {
|
||||
//process as line delimited
|
||||
|
||||
// check for CR AND NL
|
||||
Integer endIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r', '\n'});
|
||||
int delimSize = 2;
|
||||
|
||||
// check for CR OR NL
|
||||
if(endIdx == null) {
|
||||
endIdx = min(
|
||||
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r'}),
|
||||
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\n'}));
|
||||
delimSize = 1;
|
||||
}
|
||||
if (endIdx != null) {
|
||||
log.trace("Reading newline-delimited response");
|
||||
byte[] output = new byte[endIdx];
|
||||
System.arraycopy(data.getByteArray(), 0, output, 0, endIdx);
|
||||
String buffer = new String(output, format.getEncoding());
|
||||
|
||||
if (!buffer.isEmpty()) {
|
||||
//send non-empty string
|
||||
response = buffer;
|
||||
}
|
||||
|
||||
data.clearRange(0, endIdx + delimSize);
|
||||
}
|
||||
} else if (format.getBoundStart() != null && format.getBoundStart().length > 0) {
|
||||
//process as formatted response
|
||||
Integer startIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundStart());
|
||||
|
||||
if (startIdx != null) {
|
||||
int startOffset = startIdx + format.getBoundStart().length;
|
||||
|
||||
int copyLength = 0;
|
||||
int endIdx = 0;
|
||||
|
||||
if (format.getBoundEnd() != null && format.getBoundEnd().length > 0) {
|
||||
//process as bounded response
|
||||
Integer boundEnd = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundEnd(), startIdx);
|
||||
|
||||
if (boundEnd != null) {
|
||||
log.trace("Reading bounded response");
|
||||
|
||||
copyLength = boundEnd - startOffset;
|
||||
endIdx = boundEnd + 1;
|
||||
if (format.isIncludeStart()) {
|
||||
//also include the ending bytes
|
||||
copyLength += format.getBoundEnd().length;
|
||||
}
|
||||
}
|
||||
} else if (format.getFixedWidth() > 0) {
|
||||
//process as fixed length prefixed response
|
||||
log.trace("Reading fixed length prefixed response");
|
||||
|
||||
copyLength = format.getFixedWidth();
|
||||
endIdx = startOffset + format.getFixedWidth();
|
||||
} else if (format.getLength() != null) {
|
||||
//process as dynamic formatted response
|
||||
SerialOptions.ByteParam lengthParam = format.getLength();
|
||||
|
||||
if (data.getLength() > startOffset + lengthParam.getIndex() + lengthParam.getLength()) { //ensure there's length bytes to read
|
||||
log.trace("Reading dynamic formatted response");
|
||||
|
||||
int expectedLength = ByteUtilities.parseBytes(data.getByteArray(), startOffset + lengthParam.getIndex(), lengthParam.getLength(), lengthParam.getEndian());
|
||||
log.trace("Found length byte, expecting {} bytes", expectedLength);
|
||||
|
||||
startOffset += lengthParam.getIndex() + lengthParam.getLength(); // don't include the length byte(s) in the response
|
||||
copyLength = expectedLength;
|
||||
endIdx = startOffset + copyLength;
|
||||
|
||||
if (format.getCrc() != null) {
|
||||
SerialOptions.ByteParam crcParam = format.getCrc();
|
||||
|
||||
log.trace("Expecting {} crc bytes", crcParam.getLength());
|
||||
int expand = crcParam.getIndex() + crcParam.getLength();
|
||||
|
||||
//include crc in copy
|
||||
copyLength += expand;
|
||||
endIdx += expand;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//process as header formatted raw response - high risk of lost data, likely unintended settings
|
||||
log.warn("Reading header formatted raw response, are you missing an rx option?");
|
||||
|
||||
copyLength = data.getLength() - startOffset;
|
||||
endIdx = data.getLength();
|
||||
}
|
||||
|
||||
|
||||
if (copyLength > 0 && data.getLength() >= endIdx) {
|
||||
log.debug("Response format readable, starting copy");
|
||||
|
||||
if (format.isIncludeStart()) {
|
||||
//increase length to account for header bytes and bump offset back to include in copy
|
||||
copyLength += (startOffset - startIdx);
|
||||
startOffset = startIdx;
|
||||
}
|
||||
|
||||
byte[] responseData = new byte[copyLength];
|
||||
System.arraycopy(data.getByteArray(), startOffset, responseData, 0, copyLength);
|
||||
|
||||
response = new String(responseData, format.getEncoding());
|
||||
data.clearRange(startIdx, endIdx);
|
||||
}
|
||||
}
|
||||
} else if (format.getFixedWidth() > 0) {
|
||||
if (data.getLength() >= format.getFixedWidth()) {
|
||||
//process as fixed width response
|
||||
log.trace("Reading fixed length response");
|
||||
|
||||
byte[] output = new byte[format.getFixedWidth()];
|
||||
System.arraycopy(data.getByteArray(), 0, output, 0, format.getFixedWidth());
|
||||
|
||||
response = StringUtils.newStringUtf8(output);
|
||||
data.clearRange(0, format.getFixedWidth());
|
||||
}
|
||||
} else {
|
||||
//no processing, return raw
|
||||
log.trace("Reading raw response");
|
||||
|
||||
response = new String(data.getByteArray(), format.getEncoding());
|
||||
data.clear();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
catch(SerialPortException e) {
|
||||
log.error("Exception occurred while reading data from port.", e);
|
||||
}
|
||||
catch(SerialPortTimeoutException e) {
|
||||
log.error("Timeout occurred waiting for port to respond.", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets and caches the properties as to not set them every data call
|
||||
*
|
||||
* @throws SerialPortException If the properties fail to set
|
||||
*/
|
||||
private void setOptions(SerialOptions opts) throws SerialPortException {
|
||||
if (opts == null) { return; }
|
||||
|
||||
SerialOptions.PortSettings ps = opts.getPortSettings();
|
||||
if (ps != null && !ps.equals(serialOpts.getPortSettings())) {
|
||||
log.debug("Applying new port settings");
|
||||
port.setParams(ps.getBaudRate(), ps.getDataBits(), ps.getStopBits(), ps.getParity());
|
||||
port.setFlowControlMode(ps.getFlowControl());
|
||||
serialOpts.setPortSettings(ps);
|
||||
}
|
||||
|
||||
SerialOptions.ResponseFormat rf = opts.getResponseFormat();
|
||||
if (rf != null) {
|
||||
log.debug("Applying new response formatting");
|
||||
serialOpts.setResponseFormat(rf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the port parameters and writes the buffered data to the serial port.
|
||||
*/
|
||||
public void sendData(JSONObject params, SerialOptions opts) throws JSONException, IOException, SerialPortException {
|
||||
if (opts != null) {
|
||||
setOptions(opts);
|
||||
}
|
||||
|
||||
log.debug("Sending data over [{}]", portName);
|
||||
port.writeBytes(DeviceUtilities.getDataBytes(params, serialOpts.getPortSettings().getEncoding()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the serial port, if open.
|
||||
*
|
||||
* @throws SerialPortException If the port fails to close.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
// Remove orphaned reference
|
||||
websocket.removeSerialPort(portName);
|
||||
|
||||
if (!isOpen()) {
|
||||
log.warn("Serial port [{}] is not open.", portName);
|
||||
}
|
||||
|
||||
try {
|
||||
boolean closed = port.closePort();
|
||||
if (closed) {
|
||||
log.info("Serial port [{}] closed successfully.", portName);
|
||||
} else {
|
||||
// Handle ambiguity in JSSCs API
|
||||
throw new SerialPortException(portName, "closePort", "Port not closed");
|
||||
}
|
||||
} catch(SerialPortException e) {
|
||||
log.warn("Serial port [{}] was not closed properly.", portName);
|
||||
}
|
||||
|
||||
port = null;
|
||||
portName = null;
|
||||
}
|
||||
|
||||
private Integer min(Integer a, Integer b) {
|
||||
if (a == null) { return b; }
|
||||
if (b == null) { return a; }
|
||||
return Math.min(a, b);
|
||||
}
|
||||
|
||||
}
|
||||
350
old code/tray/src/qz/communication/SerialOptions.java
Executable file
350
old code/tray/src/qz/communication/SerialOptions.java
Executable file
@@ -0,0 +1,350 @@
|
||||
package qz.communication;
|
||||
|
||||
import jssc.SerialPort;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ByteUtilities;
|
||||
import qz.utils.DeviceUtilities;
|
||||
import qz.utils.LoggerUtilities;
|
||||
import qz.utils.SerialUtilities;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SerialOptions {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(SerialOptions.class);
|
||||
|
||||
private static final String DEFAULT_BEGIN = "0x0002";
|
||||
private static final String DEFAULT_END = "0x000D";
|
||||
|
||||
private PortSettings portSettings = null;
|
||||
private ResponseFormat responseFormat = null;
|
||||
|
||||
/**
|
||||
* Creates an empty/default options object
|
||||
*/
|
||||
public SerialOptions() {
|
||||
portSettings = new PortSettings();
|
||||
responseFormat = new ResponseFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided JSON object into relevant SerialPort constants
|
||||
*/
|
||||
public SerialOptions(JSONObject serialOpts, boolean isOpening) {
|
||||
if (serialOpts == null) { return; }
|
||||
|
||||
//only apply port settings if opening or explicitly set in a send data call
|
||||
if (isOpening || serialOpts.has("baudRate") || serialOpts.has("dataBits") || serialOpts.has("stopBits") || serialOpts.has("parity") || serialOpts.has("flowControl")) {
|
||||
portSettings = new PortSettings();
|
||||
|
||||
if (!serialOpts.isNull("baudRate")) {
|
||||
try { portSettings.baudRate = SerialUtilities.parseBaudRate(serialOpts.getString("baudRate")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "baudRate", serialOpts.opt("baudRate")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("dataBits")) {
|
||||
try { portSettings.dataBits = SerialUtilities.parseDataBits(serialOpts.getString("dataBits")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "dataBits", serialOpts.opt("dataBits")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("stopBits")) {
|
||||
try { portSettings.stopBits = SerialUtilities.parseStopBits(serialOpts.getString("stopBits")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "stopBits", serialOpts.opt("stopBits")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("parity")) {
|
||||
try { portSettings.parity = SerialUtilities.parseParity(serialOpts.getString("parity")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "parity", serialOpts.opt("parity")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("flowControl")) {
|
||||
try { portSettings.flowControl = SerialUtilities.parseFlowControl(serialOpts.getString("flowControl")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "flowControl", serialOpts.opt("flowControl")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("encoding") && !serialOpts.optString("encoding").isEmpty()) {
|
||||
try { portSettings.encoding = Charset.forName(serialOpts.getString("encoding")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "encoding", serialOpts.opt("encoding")); }
|
||||
}
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("rx")) {
|
||||
responseFormat = new ResponseFormat();
|
||||
//Make the response encoding default to the port encoding. If this is removed it will default to UTF-8
|
||||
responseFormat.encoding = portSettings.encoding;
|
||||
|
||||
JSONObject respOpts = serialOpts.optJSONObject("rx");
|
||||
if (respOpts != null) {
|
||||
if (!respOpts.isNull("start")) {
|
||||
try {
|
||||
JSONArray startBits = respOpts.getJSONArray("start");
|
||||
ArrayList<Byte> bytes = new ArrayList<>();
|
||||
for(int i = 0; i < startBits.length(); i++) {
|
||||
byte[] charByte = DeviceUtilities.characterBytes(startBits.getString(i), responseFormat.encoding);
|
||||
for(byte b : charByte) { bytes.add(b); }
|
||||
}
|
||||
responseFormat.boundStart = ArrayUtils.toPrimitive(bytes.toArray(new Byte[0]));
|
||||
}
|
||||
catch(JSONException e) {
|
||||
try { responseFormat.boundStart = DeviceUtilities.characterBytes(respOpts.getString("start"), responseFormat.encoding); }
|
||||
catch(JSONException e2) { LoggerUtilities.optionWarn(log, "string", "start", respOpts.opt("start")); }
|
||||
}
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("includeHeader")) {
|
||||
try { responseFormat.includeStart = respOpts.getBoolean("includeHeader"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "includeHeader", respOpts.opt("includeHeader")); }
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("end")) {
|
||||
try { responseFormat.boundEnd = DeviceUtilities.characterBytes(respOpts.getString("end"), responseFormat.encoding); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "end", respOpts.opt("end")); }
|
||||
|
||||
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||
log.warn("End bound set without start bound defined");
|
||||
}
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("untilNewline")) {
|
||||
try { responseFormat.boundNewline = respOpts.getBoolean("untilNewline"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "untilNewline", respOpts.opt("untilNewline")); }
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("width")) {
|
||||
try { responseFormat.fixedWidth = respOpts.getInt("width"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", respOpts.opt("width")); }
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("lengthBytes")) {
|
||||
try {
|
||||
JSONObject lengthOpts = respOpts.optJSONObject("lengthBytes");
|
||||
responseFormat.length = new ByteParam();
|
||||
|
||||
if (lengthOpts != null) {
|
||||
if (!lengthOpts.isNull("index")) {
|
||||
try { responseFormat.length.index = lengthOpts.getInt("index"); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.index", lengthOpts.opt("index")); }
|
||||
}
|
||||
|
||||
if (!lengthOpts.isNull("length")) {
|
||||
try { responseFormat.length.length = lengthOpts.getInt("length"); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.length", lengthOpts.opt("length")); }
|
||||
}
|
||||
|
||||
if (!lengthOpts.isNull("endian")) {
|
||||
try { responseFormat.length.endian = ByteUtilities.Endian.valueOf(lengthOpts.getString("endian").toUpperCase(Locale.ENGLISH)); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "string", "lengthBytes.endian", lengthOpts.opt("endian")); }
|
||||
}
|
||||
} else {
|
||||
responseFormat.length.index = respOpts.getInt("lengthBytes");
|
||||
}
|
||||
}
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes", respOpts.opt("lengthBytes")); }
|
||||
|
||||
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||
log.warn("Length byte(s) defined without start bound defined");
|
||||
}
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("crcBytes")) {
|
||||
try {
|
||||
JSONObject crcOpts = respOpts.optJSONObject("crcBytes");
|
||||
responseFormat.crc = new ByteParam();
|
||||
|
||||
if (crcOpts != null) {
|
||||
if (!crcOpts.isNull("index")) {
|
||||
try { responseFormat.crc.index = crcOpts.getInt("index"); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.index", crcOpts.opt("index")); }
|
||||
}
|
||||
|
||||
if (!crcOpts.isNull("length")) {
|
||||
try { responseFormat.crc.length = crcOpts.getInt("length"); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.length", crcOpts.opt("length")); }
|
||||
}
|
||||
} else {
|
||||
responseFormat.crc.length = respOpts.getInt("crcBytes");
|
||||
}
|
||||
}
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "crcBytes", respOpts.opt("crcBytes")); }
|
||||
|
||||
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||
log.warn("CRC byte(s) defined without start bound defined");
|
||||
}
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("encoding") && !respOpts.optString("encoding").isEmpty()) {
|
||||
try { responseFormat.encoding = Charset.forName(respOpts.getString("encoding")); }
|
||||
catch(JSONException | IllegalArgumentException e) { LoggerUtilities.optionWarn(log, "charset", "encoding", respOpts.opt("encoding")); }
|
||||
}
|
||||
} else {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "rx", serialOpts.opt("rx"));
|
||||
}
|
||||
} else if (isOpening) {
|
||||
// legacy support - only applies on port open
|
||||
responseFormat = new ResponseFormat();
|
||||
|
||||
// legacy start only supports string, not an array
|
||||
if (!serialOpts.isNull("start")) {
|
||||
responseFormat.boundStart = DeviceUtilities.characterBytes(serialOpts.optString("start", DEFAULT_BEGIN), responseFormat.encoding);
|
||||
} else {
|
||||
responseFormat.boundStart = DeviceUtilities.characterBytes(DEFAULT_BEGIN, responseFormat.encoding);
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("end")) {
|
||||
responseFormat.boundEnd = DeviceUtilities.characterBytes(serialOpts.optString("end", DEFAULT_END), responseFormat.encoding);
|
||||
} else {
|
||||
responseFormat.boundEnd = DeviceUtilities.characterBytes(DEFAULT_END, responseFormat.encoding);
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("width")) {
|
||||
try {
|
||||
responseFormat.fixedWidth = serialOpts.getInt("width");
|
||||
if (responseFormat.boundEnd.length > 0) {
|
||||
log.warn("Combining 'width' property with 'end' property has undefined behavior and should not be used");
|
||||
}
|
||||
}
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", serialOpts.opt("width")); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PortSettings getPortSettings() {
|
||||
return portSettings;
|
||||
}
|
||||
|
||||
public ResponseFormat getResponseFormat() {
|
||||
return responseFormat;
|
||||
}
|
||||
|
||||
public void setPortSettings(PortSettings portSettings) {
|
||||
this.portSettings = portSettings;
|
||||
}
|
||||
|
||||
public void setResponseFormat(ResponseFormat responseFormat) {
|
||||
this.responseFormat = responseFormat;
|
||||
}
|
||||
|
||||
public class PortSettings {
|
||||
|
||||
private Charset encoding = Charset.forName("UTF-8");
|
||||
private int baudRate = SerialPort.BAUDRATE_9600;
|
||||
private int dataBits = SerialPort.DATABITS_8;
|
||||
private int stopBits = SerialPort.STOPBITS_1;
|
||||
private int parity = SerialPort.PARITY_NONE;
|
||||
private int flowControl = SerialPort.FLOWCONTROL_NONE;
|
||||
|
||||
|
||||
public Charset getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
public int getBaudRate() {
|
||||
return baudRate;
|
||||
}
|
||||
|
||||
public int getDataBits() {
|
||||
return dataBits;
|
||||
}
|
||||
|
||||
public int getStopBits() {
|
||||
return stopBits;
|
||||
}
|
||||
|
||||
public int getParity() {
|
||||
return parity;
|
||||
}
|
||||
|
||||
public int getFlowControl() {
|
||||
return flowControl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof PortSettings) {
|
||||
PortSettings that = (PortSettings)o;
|
||||
|
||||
return getEncoding().equals(that.getEncoding()) &&
|
||||
getBaudRate() == that.getBaudRate() &&
|
||||
getDataBits() == that.getDataBits() &&
|
||||
getStopBits() == that.getStopBits() &&
|
||||
getParity() == that.getParity() &&
|
||||
getFlowControl() == that.getFlowControl();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ResponseFormat {
|
||||
|
||||
private Charset encoding = Charset.forName("UTF-8"); //Response charset
|
||||
private byte[] boundStart; //Character(s) denoting start of new response
|
||||
private byte[] boundEnd; //Character denoting end of a response
|
||||
private boolean boundNewline; //If the response should be split on \r?\n
|
||||
private int fixedWidth; //Fixed length response bounds
|
||||
private ByteParam length; //Info about the data length byte(s)
|
||||
private ByteParam crc; //Info about the data crc byte(s)
|
||||
private boolean includeStart; //If the response headers should be sent as well
|
||||
|
||||
|
||||
public Charset getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
public byte[] getBoundStart() {
|
||||
return boundStart;
|
||||
}
|
||||
|
||||
public byte[] getBoundEnd() {
|
||||
return boundEnd;
|
||||
}
|
||||
|
||||
public int getFixedWidth() {
|
||||
return fixedWidth;
|
||||
}
|
||||
|
||||
public ByteParam getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public ByteParam getCrc() {
|
||||
return crc;
|
||||
}
|
||||
|
||||
public boolean isIncludeStart() {
|
||||
return includeStart;
|
||||
}
|
||||
|
||||
public boolean isBoundNewline() {
|
||||
return boundNewline;
|
||||
}
|
||||
}
|
||||
|
||||
public class ByteParam {
|
||||
|
||||
private int index = 0;
|
||||
private int length = 1;
|
||||
private ByteUtilities.Endian endian = ByteUtilities.Endian.BIG;
|
||||
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public ByteUtilities.Endian getEndian() {
|
||||
return endian;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
100
old code/tray/src/qz/communication/SocketIO.java
Executable file
100
old code/tray/src/qz/communication/SocketIO.java
Executable file
@@ -0,0 +1,100 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.DeviceUtilities;
|
||||
import qz.utils.NetworkUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class SocketIO implements DeviceListener {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(SocketIO.class);
|
||||
|
||||
private String host;
|
||||
private int port;
|
||||
private Charset encoding;
|
||||
|
||||
private Socket socket;
|
||||
private DataOutputStream dataOut;
|
||||
private DataInputStream dataIn;
|
||||
|
||||
private SocketConnection websocket;
|
||||
|
||||
public SocketIO(String host, int port, Charset encoding, SocketConnection websocket) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.encoding = encoding;
|
||||
this.websocket = websocket;
|
||||
}
|
||||
|
||||
public boolean open() throws IOException {
|
||||
socket = new Socket(host, port);
|
||||
socket.setSoTimeout(NetworkUtilities.SOCKET_TIMEOUT);
|
||||
dataOut = new DataOutputStream(socket.getOutputStream());
|
||||
dataIn = new DataInputStream(socket.getInputStream());
|
||||
|
||||
return socket.isConnected();
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return socket.isConnected();
|
||||
}
|
||||
|
||||
public void sendData(JSONObject params) throws JSONException, IOException {
|
||||
log.debug("Sending data over [{}:{}]", host, port);
|
||||
dataOut.write(DeviceUtilities.getDataBytes(params, encoding));
|
||||
dataOut.flush();
|
||||
}
|
||||
|
||||
public String processSocketResponse() throws IOException {
|
||||
byte[] response = new byte[1024];
|
||||
ArrayList<Byte> fullResponse = new ArrayList<>();
|
||||
do {
|
||||
int size = dataIn.read(response);
|
||||
for(int i = 0; i < size; i++) {
|
||||
fullResponse.add(response[i]);
|
||||
}
|
||||
}
|
||||
while(dataIn.available() > 0);
|
||||
if(fullResponse.size() > 0) {
|
||||
return new String(ArrayUtils.toPrimitive(fullResponse.toArray(new Byte[0])), encoding);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Remove orphaned reference
|
||||
websocket.removeNetworkSocket(String.format("%s:%s", host, port));
|
||||
|
||||
try {
|
||||
dataOut.close();
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not close socket output stream", e);
|
||||
}
|
||||
try {
|
||||
socket.close();
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not close socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
}
|
||||
153
old code/tray/src/qz/communication/UsbIO.java
Executable file
153
old code/tray/src/qz/communication/UsbIO.java
Executable file
@@ -0,0 +1,153 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.UsbUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import javax.usb.*;
|
||||
import javax.usb.util.UsbUtil;
|
||||
|
||||
public class UsbIO implements DeviceIO {
|
||||
private static final Logger log = LogManager.getLogger(UsbIO.class);
|
||||
private UsbDevice device;
|
||||
private UsbInterface iface;
|
||||
|
||||
private boolean streaming;
|
||||
|
||||
private DeviceOptions dOpts;
|
||||
private SocketConnection websocket;
|
||||
|
||||
public UsbIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||
this.dOpts = dOpts;
|
||||
this.websocket = websocket;
|
||||
UsbDevice device = UsbUtilities.findDevice(dOpts.getVendorId().shortValue(), dOpts.getProductId().shortValue());
|
||||
if (device == null) {
|
||||
throw new DeviceException("USB device could not be found");
|
||||
}
|
||||
if (dOpts.getInterfaceId() == null) {
|
||||
throw new IllegalArgumentException("Device interface cannot be null");
|
||||
}
|
||||
this.iface = device.getActiveUsbConfiguration().getUsbInterface(dOpts.getInterfaceId());
|
||||
if (iface == null) {
|
||||
throw new DeviceException(String.format("Could not find USB interface matching [ vendorId: '%s', productId: '%s', interface: '%s' ]",
|
||||
"0x" + UsbUtil.toHexString(dOpts.getVendorId()),
|
||||
"0x" + UsbUtil.toHexString(dOpts.getProductId()),
|
||||
"0x" + UsbUtil.toHexString(dOpts.getInterfaceId())));
|
||||
}
|
||||
this.device = device;
|
||||
|
||||
}
|
||||
|
||||
public void open() throws DeviceException {
|
||||
try {
|
||||
iface.claim(new UsbInterfacePolicy() {
|
||||
@Override
|
||||
public boolean forceClaim(UsbInterface usbInterface) {
|
||||
// Releases kernel driver for systems that auto-claim usb devices
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(UsbException e) {
|
||||
throw new DeviceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return iface.isClaimed();
|
||||
}
|
||||
|
||||
public void setStreaming(boolean active) {
|
||||
streaming = active;
|
||||
}
|
||||
|
||||
public boolean isStreaming() {
|
||||
return streaming;
|
||||
}
|
||||
|
||||
public String getVendorId() {
|
||||
return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idVendor());
|
||||
}
|
||||
|
||||
public String getProductId() {
|
||||
return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idProduct());
|
||||
}
|
||||
|
||||
public String getInterface() {
|
||||
return UsbUtil.toHexString(iface.getUsbInterfaceDescriptor().iInterface());
|
||||
}
|
||||
|
||||
public byte[] readData(int responseSize, Byte endpoint) throws DeviceException {
|
||||
try {
|
||||
byte[] response = new byte[responseSize];
|
||||
exchangeData(endpoint, response);
|
||||
return response;
|
||||
}
|
||||
catch(UsbException e) {
|
||||
throw new DeviceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendData(byte[] data, Byte endpoint) throws DeviceException {
|
||||
try {
|
||||
exchangeData(endpoint, data);
|
||||
}
|
||||
catch(UsbException e) {
|
||||
throw new DeviceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException {
|
||||
throw new DeviceException("USB feature reports are not supported");
|
||||
}
|
||||
|
||||
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
|
||||
throw new DeviceException("USB feature reports are not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Data will be sent to or received from the open usb device, depending on the {@code endpoint} used.
|
||||
*
|
||||
* @param endpoint Endpoint on the usb device interface to pass data across
|
||||
* @param data Byte array of data to send, or to be written from a receive
|
||||
*/
|
||||
private synchronized void exchangeData(Byte endpoint, byte[] data) throws UsbException, DeviceException {
|
||||
if (endpoint == null) {
|
||||
throw new IllegalArgumentException("Interface endpoint cannot be null");
|
||||
}
|
||||
|
||||
UsbEndpoint usbEndpoint = iface.getUsbEndpoint(endpoint);
|
||||
if(usbEndpoint == null) {
|
||||
throw new DeviceException(String.format("Could not find USB endpoint matching [ endpoint: '%s' ]",
|
||||
"0x" + UsbUtil.toHexString(endpoint)));
|
||||
}
|
||||
UsbPipe pipe = usbEndpoint.getUsbPipe();
|
||||
if (!pipe.isOpen()) { pipe.open(); }
|
||||
|
||||
try {
|
||||
pipe.syncSubmit(data);
|
||||
}
|
||||
finally {
|
||||
if(pipe != null) {
|
||||
pipe.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
setStreaming(false);
|
||||
// Remove orphaned reference
|
||||
websocket.removeDevice(dOpts);
|
||||
if (iface.isClaimed()) {
|
||||
try {
|
||||
iface.release();
|
||||
}
|
||||
catch(UsbException e) {
|
||||
log.error("Unable to close USB device", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
old code/tray/src/qz/communication/WinspoolEx.java
Executable file
28
old code/tray/src/qz/communication/WinspoolEx.java
Executable file
@@ -0,0 +1,28 @@
|
||||
package qz.communication;
|
||||
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.platform.win32.WinNT;
|
||||
import com.sun.jna.platform.win32.Winspool;
|
||||
import com.sun.jna.win32.W32APIOptions;
|
||||
|
||||
/**
|
||||
* TODO: Remove when JNA 5.14.0+ is bundled
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface WinspoolEx extends Winspool {
|
||||
WinspoolEx INSTANCE = Native.load("Winspool.drv", WinspoolEx.class, W32APIOptions.DEFAULT_OPTIONS);
|
||||
|
||||
int JOB_CONTROL_NONE = 0x00000000; // Perform no additional action.
|
||||
int JOB_CONTROL_PAUSE = 0x00000001; // Pause the print job.
|
||||
int JOB_CONTROL_RESUME = 0x00000002; // Resume a paused print job.
|
||||
int JOB_CONTROL_CANCEL = 0x00000003; // Delete a print job.
|
||||
int JOB_CONTROL_RESTART = 0x00000004; // Restart a print job.
|
||||
int JOB_CONTROL_DELETE = 0x00000005; // Delete a print job.
|
||||
int JOB_CONTROL_SENT_TO_PRINTER = 0x00000006; // Used by port monitors to signal that a print job has been sent to the printer. This value SHOULD NOT be used remotely.
|
||||
int JOB_CONTROL_LAST_PAGE_EJECTED = 0x00000007; // Used by language monitors to signal that the last page of a print job has been ejected from the printer. This value SHOULD NOT be used remotely.
|
||||
int JOB_CONTROL_RETAIN = 0x00000008; // Keep the print job in the print queue after it prints.
|
||||
int JOB_CONTROL_RELEASE = 0x00000009; // Release the print job, undoing the effect of a JOB_CONTROL_RETAIN action.
|
||||
|
||||
boolean SetJob(WinNT.HANDLE hPrinter, int JobId, int Level, Pointer pJob, int Command);
|
||||
}
|
||||
11
old code/tray/src/qz/exception/InvalidRawImageException.java
Executable file
11
old code/tray/src/qz/exception/InvalidRawImageException.java
Executable file
@@ -0,0 +1,11 @@
|
||||
package qz.exception;
|
||||
|
||||
public class InvalidRawImageException extends Exception {
|
||||
public InvalidRawImageException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public InvalidRawImageException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
3
old code/tray/src/qz/exception/MissingArgException.java
Executable file
3
old code/tray/src/qz/exception/MissingArgException.java
Executable file
@@ -0,0 +1,3 @@
|
||||
package qz.exception;
|
||||
|
||||
public class MissingArgException extends Exception {}
|
||||
10
old code/tray/src/qz/exception/NullCommandException.java
Executable file
10
old code/tray/src/qz/exception/NullCommandException.java
Executable file
@@ -0,0 +1,10 @@
|
||||
package qz.exception;
|
||||
|
||||
public class NullCommandException extends javax.print.PrintException {
|
||||
public NullCommandException() {
|
||||
super();
|
||||
}
|
||||
public NullCommandException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
7
old code/tray/src/qz/exception/NullPrintServiceException.java
Executable file
7
old code/tray/src/qz/exception/NullPrintServiceException.java
Executable file
@@ -0,0 +1,7 @@
|
||||
package qz.exception;
|
||||
|
||||
public class NullPrintServiceException extends javax.print.PrintException {
|
||||
public NullPrintServiceException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
413
old code/tray/src/qz/installer/Installer.java
Executable file
413
old code/tray/src/qz/installer/Installer.java
Executable file
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.auth.Certificate;
|
||||
import qz.build.provision.params.Phase;
|
||||
import qz.installer.certificate.*;
|
||||
import qz.installer.certificate.firefox.FirefoxCertificateInstaller;
|
||||
import qz.installer.provision.ProvisionInstaller;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.ws.WebsocketPorts;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
import static qz.installer.certificate.KeyPairWrapper.Type.CA;
|
||||
import static qz.utils.FileUtilities.*;
|
||||
|
||||
/**
|
||||
* Cross-platform wrapper for install steps
|
||||
* - Used by CommandParser via command line
|
||||
* - Used by PrintSocketServer at startup to ensure SSL is functioning
|
||||
*/
|
||||
public abstract class Installer {
|
||||
protected static final Logger log = LogManager.getLogger(Installer.class);
|
||||
|
||||
// Silence prompts within our control
|
||||
public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent"));
|
||||
public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime";
|
||||
|
||||
WebsocketPorts websocketPorts;
|
||||
|
||||
public enum PrivilegeLevel {
|
||||
USER,
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
public abstract Installer removeLegacyStartup();
|
||||
public abstract Installer addAppLauncher();
|
||||
public abstract Installer addStartupEntry();
|
||||
public abstract Installer addSystemSettings();
|
||||
public abstract Installer removeSystemSettings();
|
||||
public abstract void spawn(List<String> args) throws Exception;
|
||||
|
||||
public abstract void setDestination(String destination);
|
||||
public abstract String getDestination();
|
||||
|
||||
private static Installer instance;
|
||||
|
||||
public static Installer getInstance() {
|
||||
if(instance == null) {
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
instance = new WindowsInstaller();
|
||||
break;
|
||||
case MAC:
|
||||
instance = new MacInstaller();
|
||||
break;
|
||||
default:
|
||||
instance = new LinuxInstaller();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void install(String destination, boolean silent) throws Exception {
|
||||
IS_SILENT |= silent; // preserve environmental variable if possible
|
||||
getInstance();
|
||||
if (destination != null) {
|
||||
instance.setDestination(destination);
|
||||
}
|
||||
install();
|
||||
}
|
||||
|
||||
public static boolean preinstall() {
|
||||
getInstance();
|
||||
log.info("Fixing runtime permissions...");
|
||||
instance.setJrePermissions(SystemUtilities.getAppPath().toString());
|
||||
log.info("Stopping running instances...");
|
||||
return TaskKiller.killAll();
|
||||
}
|
||||
|
||||
public static void install() throws Exception {
|
||||
getInstance();
|
||||
log.info("Installing to {}", instance.getDestination());
|
||||
instance.removeLibs()
|
||||
.removeProvisioning()
|
||||
.deployApp()
|
||||
.removeLegacyStartup()
|
||||
.removeLegacyFiles()
|
||||
.addSharedDirectory()
|
||||
.addAppLauncher()
|
||||
.addStartupEntry()
|
||||
.invokeProvisioning(Phase.INSTALL)
|
||||
.addSystemSettings();
|
||||
}
|
||||
|
||||
public static void uninstall() {
|
||||
log.info("Stopping running instances...");
|
||||
TaskKiller.killAll();
|
||||
getInstance();
|
||||
log.info("Uninstalling from {}", instance.getDestination());
|
||||
instance.removeSharedDirectory()
|
||||
.removeSystemSettings()
|
||||
.removeCerts()
|
||||
.invokeProvisioning(Phase.UNINSTALL);
|
||||
}
|
||||
|
||||
public Installer deployApp() throws IOException {
|
||||
Path src = SystemUtilities.getAppPath();
|
||||
Path dest = Paths.get(getDestination());
|
||||
|
||||
if(!Files.exists(dest)) {
|
||||
Files.createDirectories(dest);
|
||||
}
|
||||
|
||||
// Delete the JDK blindly
|
||||
FileUtils.deleteDirectory(dest.resolve(JRE_LOCATION).toFile());
|
||||
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
|
||||
FileUtils.copyDirectory(src.toFile(), dest.toFile(), false);
|
||||
FileUtilities.setPermissionsRecursively(dest, false);
|
||||
// Fix permissions for provisioned files
|
||||
FileUtilities.setExecutableRecursively(SystemUtilities.isMac() ?
|
||||
dest.resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||
dest.resolve(PROVISION_DIR), false);
|
||||
if(!SystemUtilities.isWindows()) {
|
||||
setExecutable(SystemUtilities.isMac() ? "Contents/Resources/uninstall" : "uninstall");
|
||||
setExecutable(SystemUtilities.isMac() ? "Contents/MacOS/" + ABOUT_TITLE : PROPS_FILE);
|
||||
return setJrePermissions(getDestination());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private Installer setJrePermissions(String dest) {
|
||||
File jreLocation = new File(dest, JRE_LOCATION);
|
||||
File jreBin = new File(jreLocation, "bin");
|
||||
File jreLib = new File(jreLocation, "lib");
|
||||
|
||||
// Set jre/bin/java and friends executable
|
||||
File[] files = jreBin.listFiles(pathname -> !pathname.isDirectory());
|
||||
if(files != null) {
|
||||
for(File file : files) {
|
||||
file.setExecutable(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Set jspawnhelper executable
|
||||
new File(jreLib, "jspawnhelper" + (SystemUtilities.isWindows() ? ".exe" : "")).setExecutable(true, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void setExecutable(String relativePath) {
|
||||
new File(getDestination(), relativePath).setExecutable(true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly purge libs to notify system cache per https://github.com/qzind/tray/issues/662
|
||||
*/
|
||||
public Installer removeLibs() {
|
||||
String[] dirs = { "libs" };
|
||||
for (String dir : dirs) {
|
||||
try {
|
||||
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
|
||||
} catch(IOException ignore) {}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer cleanupLegacyLogs(int rolloverCount) {
|
||||
// Convert old < 2.2.3 log file format
|
||||
Path logLocation = USER_DIR;
|
||||
int oldIndex = 0;
|
||||
int newIndex = 0;
|
||||
File oldFile;
|
||||
do {
|
||||
// Old: debug.log.1
|
||||
oldFile = logLocation.resolve("debug.log." + ++oldIndex).toFile();
|
||||
if(oldFile.exists()) {
|
||||
// New: debug.1.log
|
||||
File newFile;
|
||||
do {
|
||||
newFile = logLocation.resolve("debug." + ++newIndex + ".log").toFile();
|
||||
} while(newFile.exists());
|
||||
|
||||
oldFile.renameTo(newFile);
|
||||
log.info("Migrated log file {} to new location {}", oldFile, newFile);
|
||||
}
|
||||
} while(oldFile.exists() || oldIndex <= rolloverCount);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer removeLegacyFiles() {
|
||||
ArrayList<String> dirs = new ArrayList<>();
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
HashMap<String, String> move = new HashMap<>();
|
||||
|
||||
// QZ Tray 2.0 files
|
||||
dirs.add("demo/js/3rdparty");
|
||||
dirs.add("utils");
|
||||
dirs.add("auth");
|
||||
files.add("demo/js/qz-websocket.js");
|
||||
files.add("windows-icon.ico");
|
||||
|
||||
// QZ Tray 2.2.3-SNAPSHOT accidentally wrote certs in the wrong place
|
||||
dirs.add("ssl");
|
||||
|
||||
// QZ Tray 2.1 files
|
||||
if(SystemUtilities.isMac()) {
|
||||
// Moved to macOS Application Bundle standard https://developer.apple.com/go/?id=bundle-structure
|
||||
dirs.add("demo");
|
||||
dirs.add("libs");
|
||||
files.add(PROPS_FILE + ".jar");
|
||||
files.add("LICENSE.txt");
|
||||
files.add("uninstall");
|
||||
move.put(PROPS_FILE + ".properties", "Contents/Resources/" + PROPS_FILE + ".properties");
|
||||
}
|
||||
|
||||
dirs.forEach(dir -> {
|
||||
try {
|
||||
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
|
||||
} catch(IOException ignore) {}
|
||||
});
|
||||
|
||||
files.forEach(file -> {
|
||||
new File(instance.getDestination() + File.separator + file).delete();
|
||||
});
|
||||
|
||||
move.forEach((src, dest) -> {
|
||||
try {
|
||||
FileUtils.moveFile(new File(instance.getDestination() + File.separator + src),
|
||||
new File(instance.getDestination() + File.separator + dest));
|
||||
} catch(IOException ignore) {}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addSharedDirectory() {
|
||||
try {
|
||||
Files.createDirectories(SHARED_DIR);
|
||||
FileUtilities.setPermissionsRecursively(SHARED_DIR, true);
|
||||
Path ssl = Paths.get(SHARED_DIR.toString(), "ssl");
|
||||
Files.createDirectories(ssl);
|
||||
FileUtilities.setPermissionsRecursively(ssl, true);
|
||||
|
||||
log.info("Created shared directory: {}", SHARED_DIR);
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not create shared directory: {}", SHARED_DIR);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer removeSharedDirectory() {
|
||||
try {
|
||||
FileUtils.deleteDirectory(SHARED_DIR.toFile());
|
||||
log.info("Deleted shared directory: {}", SHARED_DIR);
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not delete shared directory: {}", SHARED_DIR);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, and if needed generates an SSL for the system
|
||||
*/
|
||||
public CertificateManager certGen(boolean forceNew, String... hostNames) throws Exception {
|
||||
CertificateManager certificateManager = new CertificateManager(forceNew, hostNames);
|
||||
boolean needsInstall = certificateManager.needsInstall();
|
||||
try {
|
||||
// Check that the CA cert is installed
|
||||
X509Certificate caCert = certificateManager.getKeyPair(CA).getCert();
|
||||
NativeCertificateInstaller installer = NativeCertificateInstaller.getInstance();
|
||||
|
||||
if (forceNew || needsInstall) {
|
||||
// Remove installed certs per request (usually the desktop installer, or failure to write properties)
|
||||
// Skip if running from IDE, this may accidentally remove sandboxed certs
|
||||
if(SystemUtilities.isJar()) {
|
||||
List<String> matchingCerts = installer.find();
|
||||
installer.remove(matchingCerts);
|
||||
}
|
||||
installer.install(caCert);
|
||||
FirefoxCertificateInstaller.install(caCert, hostNames);
|
||||
} else {
|
||||
// Make sure the certificate is recognized by the system
|
||||
if(caCert == null) {
|
||||
log.info("CA cert is empty, skipping installation checks. This is normal for trusted/3rd-party SSL certificates.");
|
||||
} else {
|
||||
File tempCert = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
|
||||
CertificateManager.writeCert(caCert, tempCert); // temp cert
|
||||
if (!installer.verify(tempCert)) {
|
||||
installer.install(caCert);
|
||||
FirefoxCertificateInstaller.install(caCert, hostNames);
|
||||
}
|
||||
if(!tempCert.delete()) {
|
||||
tempCert.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Something went wrong obtaining the certificate. HTTPS will fail.", e);
|
||||
}
|
||||
|
||||
// Add provisioning steps that come after certgen
|
||||
if(SystemUtilities.isAdmin()) {
|
||||
invokeProvisioning(Phase.CERTGEN);
|
||||
}
|
||||
|
||||
return certificateManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove matching certs from user|system, then Firefox
|
||||
*/
|
||||
public Installer removeCerts() {
|
||||
// System certs
|
||||
NativeCertificateInstaller instance = NativeCertificateInstaller.getInstance();
|
||||
instance.remove(instance.find());
|
||||
// Firefox certs
|
||||
FirefoxCertificateInstaller.uninstall();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user-specific settings
|
||||
* Note: See override usage for platform-specific tasks
|
||||
*/
|
||||
public Installer addUserSettings() {
|
||||
// Check for whitelisted certificates in <install>/whitelist/
|
||||
Path whiteList = SystemUtilities.getJarParentPath().resolve(WHITELIST_CERT_DIR);
|
||||
if(Files.exists(whiteList) && Files.isDirectory(whiteList)) {
|
||||
for(File file : whiteList.toFile().listFiles()) {
|
||||
try {
|
||||
Certificate cert = new Certificate(FileUtilities.readLocalFile(file.getPath()));
|
||||
if (!cert.isSaved()) {
|
||||
FileUtilities.addToCertList(ALLOW_FILE, file);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("Could not add {} to {}", file, ALLOW_FILE, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Installer invokeProvisioning(Phase phase) {
|
||||
try {
|
||||
Path provisionPath = SystemUtilities.isMac() ?
|
||||
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||
Paths.get(getDestination()).resolve(PROVISION_DIR);
|
||||
ProvisionInstaller provisionInstaller = new ProvisionInstaller(provisionPath);
|
||||
provisionInstaller.invoke(phase);
|
||||
|
||||
// Special case for custom websocket ports
|
||||
if(phase == Phase.INSTALL) {
|
||||
websocketPorts = WebsocketPorts.parseFromSteps(provisionInstaller.getSteps());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("An error occurred invoking provision \"phase\": \"{}\"", phase, e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer removeProvisioning() {
|
||||
try {
|
||||
Path provisionPath = SystemUtilities.isMac() ?
|
||||
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||
Paths.get(getDestination()).resolve(PROVISION_DIR);
|
||||
FileUtils.deleteDirectory(provisionPath.toFile());
|
||||
} catch(Exception e) {
|
||||
log.warn("An error occurred removing provision directory", e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public static Properties persistProperties(File oldFile, Properties newProps) {
|
||||
if(oldFile.exists()) {
|
||||
Properties oldProps = new Properties();
|
||||
try(Reader reader = new FileReader(oldFile)) {
|
||||
oldProps.load(reader);
|
||||
for(String key : PERSIST_PROPS) {
|
||||
if (oldProps.containsKey(key)) {
|
||||
String value = oldProps.getProperty(key);
|
||||
log.info("Preserving {}={} for install", key, value);
|
||||
newProps.put(key, value);
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Warning, an error occurred reading the old properties file {}", oldFile, e);
|
||||
}
|
||||
}
|
||||
return newProps;
|
||||
}
|
||||
|
||||
public void spawn(String ... args) throws Exception {
|
||||
spawn(new ArrayList(Arrays.asList(args)));
|
||||
}
|
||||
}
|
||||
371
old code/tray/src/qz/installer/LinuxInstaller.java
Executable file
371
old code/tray/src/qz/installer/LinuxInstaller.java
Executable file
@@ -0,0 +1,371 @@
|
||||
package qz.installer;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.UnixUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
|
||||
public class LinuxInstaller extends Installer {
|
||||
protected static final Logger log = LogManager.getLogger(LinuxInstaller.class);
|
||||
|
||||
public static final String SHORTCUT_NAME = PROPS_FILE + ".desktop";
|
||||
public static final String STARTUP_DIR = "/etc/xdg/autostart/";
|
||||
public static final String STARTUP_LAUNCHER = STARTUP_DIR + SHORTCUT_NAME;
|
||||
public static final String APP_DIR = "/usr/share/applications/";
|
||||
public static final String APP_LAUNCHER = APP_DIR + SHORTCUT_NAME;
|
||||
public static final String UDEV_RULES = "/lib/udev/rules.d/99-udev-override.rules";
|
||||
public static final String[] CHROME_POLICY_DIRS = {"/etc/chromium/policies/managed", "/etc/opt/chrome/policies/managed" };
|
||||
public static final String CHROME_POLICY = "{ \"URLAllowlist\": [\"" + DATA_DIR + "://*\"] }";
|
||||
|
||||
private String destination = "/opt/" + PROPS_FILE;
|
||||
private String sudoer;
|
||||
|
||||
public LinuxInstaller() {
|
||||
super();
|
||||
sudoer = getSudoer();
|
||||
}
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public Installer addAppLauncher() {
|
||||
addLauncher(APP_LAUNCHER, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addStartupEntry() {
|
||||
addLauncher(STARTUP_LAUNCHER, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void addLauncher(String location, boolean isStartup) {
|
||||
HashMap<String, String> fieldMap = new HashMap<>();
|
||||
// Dynamic fields
|
||||
fieldMap.put("%DESTINATION%", destination);
|
||||
fieldMap.put("%LINUX_ICON%", String.format("%s.svg", PROPS_FILE));
|
||||
fieldMap.put("%COMMAND%", String.format("%s/%s", destination, PROPS_FILE));
|
||||
fieldMap.put("%PARAM%", isStartup ? "--honorautostart" : "%u");
|
||||
|
||||
File launcher = new File(location);
|
||||
try {
|
||||
FileUtilities.configureAssetFile("assets/linux-shortcut.desktop.in", launcher, fieldMap, LinuxInstaller.class);
|
||||
launcher.setReadable(true, false);
|
||||
launcher.setExecutable(true, false);
|
||||
} catch(IOException e) {
|
||||
log.warn("Unable to write {} file: {}", isStartup ? "startup":"launcher", location, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Installer removeLegacyStartup() {
|
||||
log.info("Removing legacy autostart entries for all users matching {} or {}", ABOUT_TITLE, PROPS_FILE);
|
||||
// assume users are in /home
|
||||
String[] shortcutNames = {ABOUT_TITLE, PROPS_FILE};
|
||||
for(File file : new File("/home").listFiles()) {
|
||||
if (file.isDirectory()) {
|
||||
File userStart = new File(file.getPath() + "/.config/autostart");
|
||||
if (userStart.exists() && userStart.isDirectory()) {
|
||||
for (String shortcutName : shortcutNames) {
|
||||
File legacyStartup = new File(userStart.getPath() + File.separator + shortcutName + ".desktop");
|
||||
if(legacyStartup.exists()) {
|
||||
legacyStartup.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addSystemSettings() {
|
||||
// Legacy Ubuntu versions only: Patch Unity to show the System Tray
|
||||
if(UnixUtilities.isUbuntu()) {
|
||||
ShellUtilities.execute("gsettings", "set", "com.canonical.Unity.Panel", "systray", "-whitelist", "\"['all']\"");
|
||||
|
||||
if(ShellUtilities.execute("killall", "-w", "unity", "-panel")) {
|
||||
ShellUtilities.execute("nohup", "unity", "-panel");
|
||||
}
|
||||
|
||||
if(ShellUtilities.execute("killall", "-w", "unity", "-2d")) {
|
||||
ShellUtilities.execute("nohup", "unity", "-2d");
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome protocol handler
|
||||
for (String policyDir : CHROME_POLICY_DIRS) {
|
||||
log.info("Installing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
|
||||
try {
|
||||
FileUtilities.setPermissionsParentally(Files.createDirectories(Paths.get(policyDir)), false);
|
||||
} catch(IOException e) {
|
||||
log.warn("An error occurred creating {}", policyDir);
|
||||
}
|
||||
|
||||
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(policy.toFile()))){
|
||||
writer.write(CHROME_POLICY);
|
||||
policy.toFile().setReadable(true, false);
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Unable to write chrome policy: {} ({}:launch will fail)", policy, DATA_DIR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// USB permissions
|
||||
try {
|
||||
File udev = new File(UDEV_RULES);
|
||||
if (udev.exists()) {
|
||||
udev.delete();
|
||||
}
|
||||
FileUtilities.configureAssetFile("assets/linux-udev.rules.in", new File(UDEV_RULES), new HashMap<>(), LinuxInstaller.class);
|
||||
// udev rules should be -rw-r--r--
|
||||
udev.setReadable(true, false);
|
||||
ShellUtilities.execute("udevadm", "control", "--reload-rules");
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not install udev rules, usb support may fail {}", UDEV_RULES, e);
|
||||
}
|
||||
|
||||
// Cleanup incorrectly placed files
|
||||
File badFirefoxJs = new File("/usr/bin/defaults/pref/" + PROPS_FILE + ".js");
|
||||
File badFirefoxCfg = new File("/usr/bin/" + PROPS_FILE + ".cfg");
|
||||
|
||||
if(badFirefoxCfg.exists()) {
|
||||
log.info("Removing incorrectly placed Firefox configuration {}, {}...", badFirefoxJs, badFirefoxCfg);
|
||||
badFirefoxCfg.delete();
|
||||
new File("/usr/bin/defaults").delete();
|
||||
}
|
||||
|
||||
// Cleanup incorrectly placed files
|
||||
File badFirefoxPolicy = new File("/usr/bin/distribution/policies.json");
|
||||
if(badFirefoxPolicy.exists()) {
|
||||
log.info("Removing incorrectly placed Firefox policy {}", badFirefoxPolicy);
|
||||
badFirefoxPolicy.delete();
|
||||
// Delete the distribution folder too, as long as it's empty
|
||||
File badPolicyFolder = badFirefoxPolicy.getParentFile();
|
||||
if(badPolicyFolder.isDirectory() && badPolicyFolder.listFiles().length == 0) {
|
||||
badPolicyFolder.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
log.info("Cleaning up any remaining files...");
|
||||
new File(destination + File.separator + "install").delete();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer removeSystemSettings() {
|
||||
// Chrome protocol handler
|
||||
for (String policyDir : CHROME_POLICY_DIRS) {
|
||||
log.info("Removing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
|
||||
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
|
||||
policy.toFile().delete();
|
||||
}
|
||||
|
||||
// USB permissions
|
||||
File udev = new File(UDEV_RULES);
|
||||
if (udev.exists()) {
|
||||
udev.delete();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Environmental variables for spawning a task using sudo. Order is important.
|
||||
static String[] SUDO_EXPORTS = {"USER", "HOME", "UPSTART_SESSION", "DISPLAY", "DBUS_SESSION_BUS_ADDRESS", "XDG_CURRENT_DESKTOP", "GNOME_DESKTOP_SESSION_ID" };
|
||||
|
||||
/**
|
||||
* Spawns the process as the underlying regular user account, preserving the environment
|
||||
*/
|
||||
public void spawn(List<String> args) throws Exception {
|
||||
if(!SystemUtilities.isAdmin()) {
|
||||
// Not admin, just run as the existing user
|
||||
ShellUtilities.execute(args.toArray(new String[args.size()]));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user's environment from dbus, etc
|
||||
HashMap<String, String> env = getUserEnv(sudoer);
|
||||
if(env.size() == 0) {
|
||||
throw new Exception("Unable to get dbus info; can't spawn instance");
|
||||
}
|
||||
|
||||
// Prepare the environment
|
||||
String[] envp = new String[env.size() + ShellUtilities.envp.length];
|
||||
int i = 0;
|
||||
// Keep existing env
|
||||
for(String keep : ShellUtilities.envp) {
|
||||
envp[i++] = keep;
|
||||
}
|
||||
for(String key :env.keySet()) {
|
||||
envp[i++] = String.format("%s=%s", key, env.get(key));
|
||||
}
|
||||
|
||||
// Concat "sudo|su", sudoer, "nohup", args
|
||||
ArrayList<String> argsList = sudoCommand(sudoer, true, args);
|
||||
|
||||
// Spawn
|
||||
log.info("Executing: {}", Arrays.toString(argsList.toArray()));
|
||||
Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a command to help running as another user using "sudo" or "su"
|
||||
*/
|
||||
public static ArrayList<String> sudoCommand(String sudoer, boolean async, List<String> cmds) {
|
||||
ArrayList<String> sudo = new ArrayList<>();
|
||||
if(StringUtils.isEmpty(sudoer) || !userExists(sudoer)) {
|
||||
throw new UnsupportedOperationException(String.format("Parameter [sudoer: %s] is empty or the provided user was not found", sudoer));
|
||||
}
|
||||
if(ShellUtilities.execute("which", "sudo") // check if sudo exists
|
||||
|| ShellUtilities.execute("sudo", "-u", sudoer, "-v")) { // check if user can login
|
||||
// Pass directly into "sudo"
|
||||
log.info("Guessing that this system prefers \"sudo\" over \"su\".");
|
||||
sudo.add("sudo");
|
||||
|
||||
// Add calling user
|
||||
sudo.add("-E"); // preserve environment
|
||||
sudo.add("-u");
|
||||
sudo.add(sudoer);
|
||||
|
||||
// Add "background" task support
|
||||
if(async) {
|
||||
sudo.add("nohup");
|
||||
}
|
||||
if(cmds != null && cmds.size() > 0) {
|
||||
// Add additional commands
|
||||
sudo.addAll(cmds);
|
||||
}
|
||||
} else {
|
||||
// Build and escape for "su"
|
||||
log.info("Guessing that this system prefers \"su\" over \"sudo\".");
|
||||
sudo.add("su");
|
||||
|
||||
// Add calling user
|
||||
sudo.add(sudoer);
|
||||
|
||||
sudo.add("-c");
|
||||
|
||||
// Add "background" task support
|
||||
if(async) {
|
||||
sudo.add("nohup");
|
||||
}
|
||||
if(cmds != null && cmds.size() > 0) {
|
||||
// Add additional commands
|
||||
sudo.addAll(Arrays.asList(StringUtils.join(cmds, "\" \"") + "\""));
|
||||
}
|
||||
}
|
||||
return sudo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most likely non-root user account that the installer is running from
|
||||
*/
|
||||
private static String getSudoer() {
|
||||
String sudoer = ShellUtilities.executeRaw("logname").trim();
|
||||
if(sudoer.isEmpty() || SystemUtilities.isSolaris()) {
|
||||
sudoer = System.getenv("SUDO_USER");
|
||||
}
|
||||
return sudoer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses two common POSIX techniques for testing if the provided user account exists
|
||||
*/
|
||||
private static boolean userExists(String user) {
|
||||
return ShellUtilities.execute("id", "-u", user) ||
|
||||
ShellUtilities.execute("getent", "passwd", user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extract user environment variables from the dbus process to
|
||||
* allow starting a graphical application as the current user.
|
||||
*
|
||||
* If this fails, items such as the user's desktop theme may not be known to Java
|
||||
* at runtime resulting in the Swing L&F instead of the Gtk L&F.
|
||||
*/
|
||||
private static HashMap<String, String> getUserEnv(String matchingUser) {
|
||||
if(!SystemUtilities.isAdmin()) {
|
||||
throw new UnsupportedOperationException("Administrative access is required");
|
||||
}
|
||||
|
||||
String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="};
|
||||
|
||||
ArrayList<String> pids = new ArrayList<>();
|
||||
for(String dbusMatch : dbusMatches) {
|
||||
pids.addAll(Arrays.asList(ShellUtilities.executeRaw("pgrep", "-f", dbusMatch).split("\\r?\\n")));
|
||||
}
|
||||
|
||||
HashMap<String, String> env = new HashMap<>();
|
||||
HashMap<String, String> tempEnv = new HashMap<>();
|
||||
ArrayList<String> toExport = new ArrayList<>(Arrays.asList(SUDO_EXPORTS));
|
||||
for(String pid : pids) {
|
||||
if(pid.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String[] vars;
|
||||
if(SystemUtilities.isSolaris()) {
|
||||
// Use pargs -e $$ to get environment
|
||||
log.info("Reading environment info from [pargs, -e, {}]", pid);
|
||||
String pargs = ShellUtilities.executeRaw("pargs", "-e", pid);
|
||||
vars = pargs.split("\\r?\\n");
|
||||
String delim = "]: ";
|
||||
for(int i = 0; i < vars.length; i++) {
|
||||
if(vars[i].contains(delim)) {
|
||||
vars[i] = vars[i].substring(vars[i].indexOf(delim) + delim.length()).trim();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Assume /proc/$$/environ
|
||||
String environ = String.format("/proc/%s/environ", pid);
|
||||
String delim = Pattern.compile("\0").pattern();
|
||||
log.info("Reading environment info from {}", environ);
|
||||
vars = new String(Files.readAllBytes(Paths.get(environ))).split(delim);
|
||||
}
|
||||
for(String var : vars) {
|
||||
String[] parts = var.split("=", 2);
|
||||
if(parts.length == 2) {
|
||||
String key = parts[0].trim();
|
||||
String val = parts[1].trim();
|
||||
if(toExport.contains(key)) {
|
||||
tempEnv.put(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("An unexpected error occurred obtaining dbus info", e);
|
||||
}
|
||||
|
||||
// Only add vars for the current user
|
||||
if(matchingUser.trim().equals(tempEnv.get("USER"))) {
|
||||
env.putAll(tempEnv);
|
||||
} else {
|
||||
log.debug("Expected USER={} but got USER={}, skipping results for {}", matchingUser, tempEnv.get("USER"), pid);
|
||||
}
|
||||
|
||||
// Use gtk theme
|
||||
if(env.containsKey("XDG_CURRENT_DESKTOP") && !env.containsKey("GNOME_DESKTOP_SESSION_ID")) {
|
||||
if(env.get("XDG_CURRENT_DESKTOP").toLowerCase(Locale.ENGLISH).contains("gnome")) {
|
||||
env.put("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated");
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
}
|
||||
125
old code/tray/src/qz/installer/MacInstaller.java
Executable file
125
old code/tray/src/qz/installer/MacInstaller.java
Executable file
@@ -0,0 +1,125 @@
|
||||
package qz.installer;
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
|
||||
public class MacInstaller extends Installer {
|
||||
protected static final Logger log = LogManager.getLogger(MacInstaller.class);
|
||||
private static final String PACKAGE_NAME = getPackageName();
|
||||
public static final String LAUNCH_AGENT_PATH = String.format("/Library/LaunchAgents/%s.plist", MacInstaller.PACKAGE_NAME);
|
||||
private String destination = "/Applications/" + ABOUT_TITLE + ".app";
|
||||
|
||||
public Installer addAppLauncher() {
|
||||
// not needed; registered when "QZ Tray.app" is copied
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addStartupEntry() {
|
||||
File dest = new File(LAUNCH_AGENT_PATH);
|
||||
HashMap<String, String> fieldMap = new HashMap<>();
|
||||
// Dynamic fields
|
||||
fieldMap.put("%PACKAGE_NAME%", PACKAGE_NAME);
|
||||
fieldMap.put("%COMMAND%", String.format("%s/Contents/MacOS/%s", destination, ABOUT_TITLE));
|
||||
fieldMap.put("%PARAM%", "--honorautostart");
|
||||
|
||||
try {
|
||||
FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class);
|
||||
// Disable service until reboot
|
||||
if(SystemUtilities.isMac()) {
|
||||
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Unable to write startup file: {}", dest, e);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public Installer addSystemSettings() {
|
||||
// Chrome protocol handler
|
||||
String plist = "/Library/Preferences/com.google.Chrome.plist";
|
||||
if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] {DATA_DIR + "://*" }).isEmpty()) {
|
||||
ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLAllowlist", "-array-add", DATA_DIR +"://*");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public Installer removeSystemSettings() {
|
||||
// Remove startup entry
|
||||
File dest = new File(LAUNCH_AGENT_PATH);
|
||||
dest.delete();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes legacy (<= 2.0) startup entries
|
||||
*/
|
||||
public Installer removeLegacyStartup() {
|
||||
log.info("Removing startup entries for all users matching " + ABOUT_TITLE);
|
||||
String script = "tell application \"System Events\" to delete "
|
||||
+ "every login item where name is \"" + ABOUT_TITLE + "\""
|
||||
+ " or name is \"" + PROPS_FILE + ".jar\"";
|
||||
|
||||
// Run on background thread in case System Events is hung or slow to respond
|
||||
final String finalScript = script;
|
||||
new Thread(() -> {
|
||||
ShellUtilities.executeAppleScript(finalScript);
|
||||
}).run();
|
||||
return this;
|
||||
}
|
||||
|
||||
public static String getPackageName() {
|
||||
String packageName;
|
||||
String[] parts = ABOUT_URL.split("\\W");
|
||||
if (parts.length >= 2) {
|
||||
// Parse io.qz.qz-print from Constants
|
||||
packageName = String.format("%s.%s.%s", parts[parts.length - 1], parts[parts.length - 2], PROPS_FILE);
|
||||
} else {
|
||||
// Fallback on something sane
|
||||
packageName = "local." + PROPS_FILE;
|
||||
}
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void spawn(List<String> args) throws Exception {
|
||||
if(SystemUtilities.isAdmin()) {
|
||||
// macOS unconventionally uses "$USER" during its install process
|
||||
String sudoer = System.getenv("USER");
|
||||
if(sudoer == null || sudoer.isEmpty() || sudoer.equals("root")) {
|
||||
// Fallback, should only fire via Terminal + sudo
|
||||
sudoer = ShellUtilities.executeRaw("logname").trim();
|
||||
}
|
||||
// Start directly without waitFor(...), avoids deadlocking
|
||||
Runtime.getRuntime().exec(new String[] { "su", sudoer, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""});
|
||||
} else {
|
||||
Runtime.getRuntime().exec(args.toArray(new String[args.size()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
227
old code/tray/src/qz/installer/TaskKiller.java
Executable file
227
old code/tray/src/qz/installer/TaskKiller.java
Executable file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package qz.installer;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
|
||||
import static qz.common.Constants.PROPS_FILE;
|
||||
|
||||
public class TaskKiller {
|
||||
protected static final Logger log = LogManager.getLogger(TaskKiller.class);
|
||||
private static final String[] JAR_NAMES = {
|
||||
PROPS_FILE + ".jar",
|
||||
"qz.App", // v2.2.0...
|
||||
"qz.ws.PrintSocketServer" // v2.0.0...v2.1.6
|
||||
};
|
||||
private static final String[] KILL_PID_CMD_POSIX = { "kill", "-9" };
|
||||
private static final String[] KILL_PID_CMD_WIN32 = { "taskkill.exe", "/F", "/PID" };
|
||||
private static final String[] KILL_PID_CMD = SystemUtilities.isWindows() ? KILL_PID_CMD_WIN32 : KILL_PID_CMD_POSIX;
|
||||
|
||||
/**
|
||||
* Kills all QZ Tray processes, being careful not to kill itself
|
||||
*/
|
||||
public static boolean killAll() {
|
||||
boolean success = true;
|
||||
|
||||
// Disable service until reboot
|
||||
if(SystemUtilities.isMac()) {
|
||||
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
|
||||
}
|
||||
|
||||
// Use jcmd to get all java processes
|
||||
HashSet<Integer> pids = findPidsJcmd();
|
||||
if(!SystemUtilities.isWindows()) {
|
||||
// Fallback to pgrep, needed for macOS (See JDK-8319589, JDK-8197387)
|
||||
pids.addAll(findPidsPgrep());
|
||||
} else if(WindowsUtilities.isSystemAccount()) {
|
||||
// Fallback to powershell, needed for Windows
|
||||
pids.addAll(findPidsPwsh());
|
||||
}
|
||||
|
||||
// Careful not to kill ourselves ;)
|
||||
pids.remove(SystemUtilities.getProcessId());
|
||||
|
||||
// Kill each PID
|
||||
String[] killPid = new String[KILL_PID_CMD.length + 1];
|
||||
System.arraycopy(KILL_PID_CMD, 0, killPid, 0, KILL_PID_CMD.length);
|
||||
for (Integer pid : pids) {
|
||||
killPid[killPid.length - 1] = pid.toString();
|
||||
success = success && ShellUtilities.execute(killPid);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private static Path getJcmdPath() throws IOException {
|
||||
Path jcmd;
|
||||
if(SystemUtilities.isWindows()) {
|
||||
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd.exe");
|
||||
} else if (SystemUtilities.isMac()) {
|
||||
jcmd = SystemUtilities.getJarParentPath().resolve("../PlugIns/Java.runtime/Contents/Home/bin/jcmd");
|
||||
} else {
|
||||
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd");
|
||||
}
|
||||
if(!jcmd.toFile().exists()) {
|
||||
log.error("Could not find {}", jcmd);
|
||||
throw new IOException("Could not find jcmd, we can't use it for detecting running instances");
|
||||
}
|
||||
return jcmd;
|
||||
}
|
||||
|
||||
|
||||
static final String[] PWSH_QUERY = { "powershell.exe", "-Command", "\"(Get-CimInstance Win32_Process -Filter \\\"Name = 'java.exe' OR Name = 'javaw.exe'\\\").Where({$_.CommandLine -like '*%s*'}).ProcessId\"" };
|
||||
|
||||
/**
|
||||
* Leverage powershell.exe when run as SYSTEM to workaround https://github.com/qzind/tray/issues/1360
|
||||
* TODO: Remove when jcmd is patched to work as SYSTEM account
|
||||
*/
|
||||
private static HashSet<Integer> findPidsPwsh() {
|
||||
HashSet<Integer> foundPids = new HashSet<>();
|
||||
|
||||
for(String jarName : JAR_NAMES) {
|
||||
String[] pwshQuery = PWSH_QUERY.clone();
|
||||
int lastIndex = pwshQuery.length - 1;
|
||||
// Format the last element to contain the jarName
|
||||
pwshQuery[lastIndex] = String.format(pwshQuery[lastIndex], jarName);
|
||||
String stdout = ShellUtilities.executeRaw(pwshQuery);
|
||||
String[] lines = stdout.split("\\s*\\r?\\n");
|
||||
for(String line : lines) {
|
||||
if(line.trim().isEmpty()) {
|
||||
// Don't try to process blank lines
|
||||
continue;
|
||||
}
|
||||
|
||||
int pid = parsePid(line);
|
||||
if (pid >= 0) {
|
||||
foundPids.add(pid);
|
||||
} else {
|
||||
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundPids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use pgrep to fetch all PIDs to workaround https://github.com/openjdk/jdk/pull/25824
|
||||
* TODO: Remove when jcmd is patched to work properly on macOS
|
||||
*/
|
||||
private static HashSet<Integer> findPidsPgrep() {
|
||||
HashSet<Integer> foundPids = new HashSet<>();
|
||||
|
||||
for(String jarName : JAR_NAMES) {
|
||||
String stdout = ShellUtilities.executeRaw("pgrep", "-f", jarName);
|
||||
String[] lines = stdout.split("\\s*\\r?\\n");
|
||||
for(String line : lines) {
|
||||
if(line.trim().isEmpty()) {
|
||||
// Don't try to process blank lines
|
||||
continue;
|
||||
}
|
||||
|
||||
int pid = parsePid(line);
|
||||
if (pid >= 0) {
|
||||
foundPids.add(pid);
|
||||
} else {
|
||||
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundPids;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Uses jcmd to fetch all PIDs that match this product
|
||||
*/
|
||||
private static HashSet<Integer> findPidsJcmd() {
|
||||
HashSet<Integer> foundPids = new HashSet<>();
|
||||
|
||||
String stdout;
|
||||
String[] lines;
|
||||
try {
|
||||
stdout = ShellUtilities.executeRaw(getJcmdPath().toString(), "-l");
|
||||
if(stdout == null) {
|
||||
log.error("Error calling '{}' {}", getJcmdPath(), "-l");
|
||||
return foundPids;
|
||||
}
|
||||
lines = stdout.split("\\r?\\n");
|
||||
} catch(Exception e) {
|
||||
log.error(e);
|
||||
return foundPids;
|
||||
}
|
||||
|
||||
for(String line : lines) {
|
||||
if (line.trim().isEmpty()) {
|
||||
// Don't try to process blank lines
|
||||
continue;
|
||||
}
|
||||
// e.g. "35446 C:\Program Files\QZ Tray\qz-tray.jar"
|
||||
String[] parts = line.split(" ", 2);
|
||||
int pid = parsePid(parts);
|
||||
if (pid >= 0) {
|
||||
String args = parseArgs(parts);
|
||||
if (args == null) {
|
||||
log.warn("Found PID value '{}' but no args to match. Full line: '{}', Full output: '{}'", pid, line, stdout);
|
||||
continue;
|
||||
}
|
||||
for(String jarName : JAR_NAMES) {
|
||||
if (args.contains(jarName)) {
|
||||
foundPids.add(pid);
|
||||
break; // continue parent loop
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||
}
|
||||
}
|
||||
|
||||
return foundPids;
|
||||
}
|
||||
|
||||
// Returns the second index of a String[], trimmed
|
||||
private static String parseArgs(String[] input) {
|
||||
if(input != null) {
|
||||
if(input.length == 2) {
|
||||
return input[1].trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parses an int value form the first index of a String[], returning -1 if something went wrong
|
||||
private static int parsePid(String[] input) {
|
||||
if(input != null) {
|
||||
if(input.length == 2) {
|
||||
return parsePid(input[0]);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parses an int value form the provided string, returning -1 if something went wrong
|
||||
private static int parsePid(String input) {
|
||||
String pidString = input.trim();
|
||||
if(StringUtils.isNumeric(pidString)) {
|
||||
return Integer.parseInt(pidString);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
208
old code/tray/src/qz/installer/WindowsInstaller.java
Executable file
208
old code/tray/src/qz/installer/WindowsInstaller.java
Executable file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer;
|
||||
|
||||
import com.sun.jna.platform.win32.*;
|
||||
import mslinks.ShellLink;
|
||||
import mslinks.ShellLinkException;
|
||||
import mslinks.ShellLinkHelper;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
import qz.ws.PrintSocketServer;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
import static qz.installer.WindowsSpecialFolders.*;
|
||||
import static com.sun.jna.platform.win32.WinReg.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class WindowsInstaller extends Installer {
|
||||
protected static final Logger log = LogManager.getLogger(WindowsInstaller.class);
|
||||
private String destination = getDefaultDestination();
|
||||
private String destinationExe = getDefaultDestination() + File.separator + PROPS_FILE + ".exe";
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
this.destinationExe = destination + File.separator + PROPS_FILE + ".exe";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycles through registry keys removing legacy (<= 2.0) startup entries
|
||||
*/
|
||||
public Installer removeLegacyStartup() {
|
||||
log.info("Removing legacy startup entries for all users matching " + ABOUT_TITLE);
|
||||
for (String user : Advapi32Util.registryGetKeys(HKEY_USERS)) {
|
||||
WindowsUtilities.deleteRegValue(HKEY_USERS, user.trim() + "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", ABOUT_TITLE);
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.deleteQuietly(new File(STARTUP + File.separator + ABOUT_TITLE + ".lnk"));
|
||||
} catch(Win32Exception ignore) {}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addAppLauncher() {
|
||||
try {
|
||||
// Delete old 2.0 launcher
|
||||
FileUtils.deleteQuietly(new File(COMMON_START_MENU + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
|
||||
Path loc = Paths.get(COMMON_START_MENU.toString(), "Programs", ABOUT_TITLE);
|
||||
loc.toFile().mkdirs();
|
||||
String lnk = loc + File.separator + ABOUT_TITLE + ".lnk";
|
||||
String exe = destination + File.separator + PROPS_FILE+ ".exe";
|
||||
log.info("Creating launcher \"{}\" -> \"{}\"", lnk, exe);
|
||||
ShellLinkHelper.createLink(exe, lnk);
|
||||
} catch(ShellLinkException | IOException | Win32Exception e) {
|
||||
log.warn("Could not create launcher", e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addStartupEntry() {
|
||||
try {
|
||||
String lnk = WindowsSpecialFolders.COMMON_STARTUP + File.separator + ABOUT_TITLE + ".lnk";
|
||||
String exe = destination + File.separator + PROPS_FILE+ ".exe";
|
||||
log.info("Creating startup entry \"{}\" -> \"{}\"", lnk, exe);
|
||||
ShellLink link = ShellLinkHelper.createLink(exe, lnk).getLink();
|
||||
link.setCMDArgs("--honorautostart"); // honors auto-start preferences
|
||||
} catch(ShellLinkException | IOException | Win32Exception e) {
|
||||
log.warn("Could not create startup launcher", e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public Installer removeSystemSettings() {
|
||||
// Cleanup registry
|
||||
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + ABOUT_TITLE);
|
||||
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\" + ABOUT_TITLE);
|
||||
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, DATA_DIR);
|
||||
// Chrome protocol handler
|
||||
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
|
||||
// Deprecated Chrome protocol handler
|
||||
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLWhitelist", String.format("%s://*", DATA_DIR));
|
||||
|
||||
// Cleanup launchers
|
||||
for(WindowsSpecialFolders folder : new WindowsSpecialFolders[] { START_MENU, COMMON_START_MENU, DESKTOP, PUBLIC_DESKTOP, COMMON_STARTUP, RECENT }) {
|
||||
try {
|
||||
new File(folder + File.separator + ABOUT_TITLE + ".lnk").delete();
|
||||
// Since 2.1, start menus use subfolder
|
||||
if (folder.equals(COMMON_START_MENU) || folder.equals(START_MENU)) {
|
||||
FileUtils.deleteQuietly(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
|
||||
FileUtils.deleteDirectory(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE));
|
||||
}
|
||||
} catch(InvalidPathException | IOException | Win32Exception ignore) {}
|
||||
}
|
||||
|
||||
// Cleanup firewall rules
|
||||
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addSystemSettings() {
|
||||
/**
|
||||
* TODO: Upgrade JNA!
|
||||
* 64-bit registry view is currently invoked by nsis (windows-installer.nsi.in) using SetRegView 64
|
||||
* However, newer version of JNA offer direct WinNT.KEY_WOW64_64KEY registry support, safeguarding
|
||||
* against direct calls to "java -jar qz-tray.jar install|keygen|etc", which will be needed moving forward
|
||||
* for support and troubleshooting.
|
||||
*/
|
||||
|
||||
// Mime-type support e.g. qz:launch
|
||||
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "", String.format("URL:%s Protocol", ABOUT_TITLE));
|
||||
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "URL Protocol", "");
|
||||
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\DefaultIcon", DATA_DIR), "", String.format("\"%s\",1", destinationExe));
|
||||
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\shell\\open\\command", DATA_DIR), "", String.format("\"%s\" \"%%1\"", destinationExe));
|
||||
|
||||
/// Uninstall info
|
||||
String uninstallKey = String.format("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", ABOUT_TITLE);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, String.format("Software\\%s", ABOUT_TITLE), "", destination);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayName", String.format("%s %s", ABOUT_TITLE, VERSION));
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "Publisher", ABOUT_COMPANY);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "UninstallString", destination + File.separator + "uninstall.exe");
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayIcon", destinationExe);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "HelpLink", ABOUT_SUPPORT_URL );
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLUpdateInfo", ABOUT_DOWNLOAD_URL);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLInfoAbout", ABOUT_SUPPORT_URL);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayVersion", VERSION.toString());
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "EstimatedSize", FileUtils.sizeOfDirectoryAsBigInteger(new File(destination)).intValue() / 1024);
|
||||
|
||||
// Chrome protocol handler
|
||||
WindowsUtilities.addNumberedRegValue(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
|
||||
|
||||
// Firewall rules
|
||||
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
|
||||
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "add", "rule", String.format("name=%s", ABOUT_TITLE),
|
||||
"dir=in", "action=allow", "profile=any", String.format("localport=%s", websocketPorts.allPortsAsString()), "localip=any", "protocol=tcp");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Installer addUserSettings() {
|
||||
// Whitelist loopback for IE/Edge
|
||||
if(ShellUtilities.execute("CheckNetIsolation.exe", "LoopbackExempt", "-a", "-n=Microsoft.MicrosoftEdge_8wekyb3d8bbwe")) {
|
||||
log.warn("Could not whitelist loopback connections for IE, Edge");
|
||||
}
|
||||
|
||||
try {
|
||||
// Intranet settings; uncheck "include sites not listed in other zones"
|
||||
String key = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones\\1";
|
||||
String value = "Flags";
|
||||
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
|
||||
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
|
||||
// remove value using bitwise XOR
|
||||
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data ^ 16);
|
||||
}
|
||||
|
||||
// Legacy Edge loopback support
|
||||
key = "Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge\\ExperimentalFeatures";
|
||||
value = "AllowLocalhostLoopback";
|
||||
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
|
||||
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
|
||||
// remove value using bitwise OR
|
||||
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data | 1);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("An error occurred configuring the \"Local Intranet Zone\"; connections to \"localhost\" may fail", e);
|
||||
}
|
||||
return super.addUserSettings();
|
||||
}
|
||||
|
||||
public static String getDefaultDestination() {
|
||||
String path = System.getenv("ProgramW6432");
|
||||
if (path == null || path.trim().isEmpty()) {
|
||||
path = System.getenv("ProgramFiles");
|
||||
}
|
||||
return path + File.separator + ABOUT_TITLE;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void spawn(List<String> args) throws Exception {
|
||||
if(SystemUtilities.isAdmin()) {
|
||||
log.warn("Spawning as user isn't implemented; starting process with elevation instead");
|
||||
}
|
||||
ShellUtilities.execute(args.toArray(new String[args.size()]));
|
||||
}
|
||||
}
|
||||
97
old code/tray/src/qz/installer/WindowsSpecialFolders.java
Executable file
97
old code/tray/src/qz/installer/WindowsSpecialFolders.java
Executable file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer;
|
||||
|
||||
import com.sun.jna.platform.win32.*;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
/**
|
||||
* Windows XP-compatible special folder's wrapper for JNA
|
||||
*
|
||||
*/
|
||||
public enum WindowsSpecialFolders {
|
||||
ADMIN_TOOLS(ShlObj.CSIDL_ADMINTOOLS, KnownFolders.FOLDERID_AdminTools),
|
||||
STARTUP_ALT(ShlObj.CSIDL_ALTSTARTUP, KnownFolders.FOLDERID_Startup),
|
||||
ROAMING_APPDATA(ShlObj.CSIDL_APPDATA, KnownFolders.FOLDERID_RoamingAppData),
|
||||
RECYCLING_BIN(ShlObj.CSIDL_BITBUCKET, KnownFolders.FOLDERID_RecycleBinFolder),
|
||||
CD_BURNING(ShlObj.CSIDL_CDBURN_AREA, KnownFolders.FOLDERID_CDBurning),
|
||||
COMMON_ADMIN_TOOLS(ShlObj.CSIDL_COMMON_ADMINTOOLS, KnownFolders.FOLDERID_CommonAdminTools),
|
||||
COMMON_STARTUP_ALT(ShlObj.CSIDL_COMMON_ALTSTARTUP, KnownFolders.FOLDERID_CommonStartup),
|
||||
PROGRAM_DATA(ShlObj.CSIDL_COMMON_APPDATA, KnownFolders.FOLDERID_ProgramData),
|
||||
PUBLIC_DESKTOP(ShlObj.CSIDL_COMMON_DESKTOPDIRECTORY, KnownFolders.FOLDERID_PublicDesktop),
|
||||
PUBLIC_DOCUMENTS(ShlObj.CSIDL_COMMON_DOCUMENTS, KnownFolders.FOLDERID_PublicDocuments),
|
||||
COMMON_FAVORITES(ShlObj.CSIDL_COMMON_FAVORITES, KnownFolders.FOLDERID_Favorites),
|
||||
COMMON_MUSIC(ShlObj.CSIDL_COMMON_MUSIC, KnownFolders.FOLDERID_PublicMusic),
|
||||
COMMON_OEM_LINKS(ShlObj.CSIDL_COMMON_OEM_LINKS, KnownFolders.FOLDERID_CommonOEMLinks),
|
||||
COMMON_PICTURES(ShlObj.CSIDL_COMMON_PICTURES, KnownFolders.FOLDERID_PublicPictures),
|
||||
COMMON_PROGRAMS(ShlObj.CSIDL_COMMON_PROGRAMS, KnownFolders.FOLDERID_CommonPrograms),
|
||||
COMMON_START_MENU(ShlObj.CSIDL_COMMON_STARTMENU, KnownFolders.FOLDERID_CommonStartMenu),
|
||||
COMMON_STARTUP(ShlObj.CSIDL_COMMON_STARTUP, KnownFolders.FOLDERID_CommonStartup),
|
||||
COMMON_TEMPLATES(ShlObj.CSIDL_COMMON_TEMPLATES, KnownFolders.FOLDERID_CommonTemplates),
|
||||
COMMON_VIDEO(ShlObj.CSIDL_COMMON_VIDEO, KnownFolders.FOLDERID_PublicVideos),
|
||||
COMPUTERS_NEAR_ME(ShlObj.CSIDL_COMPUTERSNEARME, KnownFolders.FOLDERID_NetworkFolder),
|
||||
CONNECTIONS_FOLDER(ShlObj.CSIDL_CONNECTIONS, KnownFolders.FOLDERID_ConnectionsFolder),
|
||||
CONTROL_PANEL(ShlObj.CSIDL_CONTROLS, KnownFolders.FOLDERID_ControlPanelFolder),
|
||||
COOKIES(ShlObj.CSIDL_COOKIES, KnownFolders.FOLDERID_Cookies),
|
||||
DESKTOP_VIRTUAL(ShlObj.CSIDL_DESKTOP, KnownFolders.FOLDERID_Desktop),
|
||||
DESKTOP(ShlObj.CSIDL_DESKTOPDIRECTORY, KnownFolders.FOLDERID_Desktop),
|
||||
COMPUTER_FOLDER(ShlObj.CSIDL_DRIVES, KnownFolders.FOLDERID_ComputerFolder),
|
||||
FAVORITES(ShlObj.CSIDL_FAVORITES, KnownFolders.FOLDERID_Favorites),
|
||||
FONTS(ShlObj.CSIDL_FONTS, KnownFolders.FOLDERID_Fonts),
|
||||
HISTORY(ShlObj.CSIDL_HISTORY, KnownFolders.FOLDERID_History),
|
||||
INTERNET_FOLDER(ShlObj.CSIDL_INTERNET, KnownFolders.FOLDERID_InternetFolder),
|
||||
INTERNET_CACHE(ShlObj.CSIDL_INTERNET_CACHE, KnownFolders.FOLDERID_InternetCache),
|
||||
LOCAL_APPDATA(ShlObj.CSIDL_LOCAL_APPDATA, KnownFolders.FOLDERID_LocalAppData),
|
||||
MY_DOCUMENTS(ShlObj.CSIDL_MYDOCUMENTS, KnownFolders.FOLDERID_Documents),
|
||||
MY_MUSIC(ShlObj.CSIDL_MYMUSIC, KnownFolders.FOLDERID_Music),
|
||||
MY_PICTURES(ShlObj.CSIDL_MYPICTURES, KnownFolders.FOLDERID_Pictures),
|
||||
MY_VIDEOS(ShlObj.CSIDL_MYVIDEO, KnownFolders.FOLDERID_Videos),
|
||||
NETWORK_NEIGHBORHOOD(ShlObj.CSIDL_NETHOOD, KnownFolders.FOLDERID_NetHood),
|
||||
NETWORK_FOLDER(ShlObj.CSIDL_NETWORK, KnownFolders.FOLDERID_NetworkFolder),
|
||||
PERSONAL_FOLDDER(ShlObj.CSIDL_PERSONAL, KnownFolders.FOLDERID_Documents),
|
||||
PRINTERS(ShlObj.CSIDL_PRINTERS, KnownFolders.FOLDERID_PrintersFolder),
|
||||
PRINTING_NEIGHBORHOODD(ShlObj.CSIDL_PRINTHOOD, KnownFolders.FOLDERID_PrintHood),
|
||||
PROFILE_FOLDER(ShlObj.CSIDL_PROFILE, KnownFolders.FOLDERID_Profile),
|
||||
PROGRAM_FILES(ShlObj.CSIDL_PROGRAM_FILES, KnownFolders.FOLDERID_ProgramFiles),
|
||||
PROGRAM_FILESX86(ShlObj.CSIDL_PROGRAM_FILESX86, KnownFolders.FOLDERID_ProgramFilesX86),
|
||||
PROGRAM_FILES_COMMON(ShlObj.CSIDL_PROGRAM_FILES_COMMON, KnownFolders.FOLDERID_ProgramFilesCommon),
|
||||
PROGRAM_FILES_COMMONX86(ShlObj.CSIDL_PROGRAM_FILES_COMMONX86, KnownFolders.FOLDERID_ProgramFilesCommonX86),
|
||||
PROGRAMS(ShlObj.CSIDL_PROGRAMS, KnownFolders.FOLDERID_Programs),
|
||||
RECENT(ShlObj.CSIDL_RECENT, KnownFolders.FOLDERID_Recent),
|
||||
RESOURCES(ShlObj.CSIDL_RESOURCES, KnownFolders.FOLDERID_ResourceDir),
|
||||
RESOURCES_LOCALIZED(ShlObj.CSIDL_RESOURCES_LOCALIZED, KnownFolders.FOLDERID_LocalizedResourcesDir),
|
||||
SEND_TO(ShlObj.CSIDL_SENDTO, KnownFolders.FOLDERID_SendTo),
|
||||
START_MENU(ShlObj.CSIDL_STARTMENU, KnownFolders.FOLDERID_StartMenu),
|
||||
STARTUP(ShlObj.CSIDL_STARTUP, KnownFolders.FOLDERID_Startup),
|
||||
SYSTEM(ShlObj.CSIDL_SYSTEM, KnownFolders.FOLDERID_System),
|
||||
SYSTEMX86(ShlObj.CSIDL_SYSTEMX86, KnownFolders.FOLDERID_SystemX86),
|
||||
TEMPLATES(ShlObj.CSIDL_TEMPLATES, KnownFolders.FOLDERID_Templates),
|
||||
WINDOWS(ShlObj.CSIDL_WINDOWS, KnownFolders.FOLDERID_Windows);
|
||||
|
||||
private int csidl;
|
||||
private Guid.GUID guid;
|
||||
WindowsSpecialFolders(int csidl, Guid.GUID guid) {
|
||||
this.csidl = csidl;
|
||||
this.guid = guid;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
if(WindowsUtilities.isWindowsXP()) {
|
||||
return Shell32Util.getSpecialFolderPath(csidl, false);
|
||||
}
|
||||
return Shell32Util.getKnownFolderPath(guid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getPath();
|
||||
}
|
||||
}
|
||||
8
old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in
Executable file
8
old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in
Executable file
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=%ABOUT_TITLE%
|
||||
Exec="%COMMAND%" %PARAM%
|
||||
Path=%DESTINATION%
|
||||
Icon=%DESTINATION%/%LINUX_ICON%
|
||||
MimeType=application/x-qz;x-scheme-handler/qz;
|
||||
Terminal=false
|
||||
2
old code/tray/src/qz/installer/assets/linux-udev.rules.in
Executable file
2
old code/tray/src/qz/installer/assets/linux-udev.rules.in
Executable file
@@ -0,0 +1,2 @@
|
||||
# %ABOUT_TITLE% usb override settings
|
||||
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"
|
||||
18
old code/tray/src/qz/installer/assets/mac-launchagent.plist.in
Executable file
18
old code/tray/src/qz/installer/assets/mac-launchagent.plist.in
Executable file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>%PACKAGE_NAME%</string>
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>SuccessfulExit</key><false/>
|
||||
<key>AfterInitialDemand</key><false/>
|
||||
</dict>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>%COMMAND%</string>
|
||||
<string>%PARAM%</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
147
old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java
Executable file
147
old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java
Executable file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.*;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bouncycastle.asn1.*;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x509.*;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import static qz.installer.certificate.KeyPairWrapper.Type.*;
|
||||
|
||||
public class CertificateChainBuilder {
|
||||
public static final String[] DEFAULT_HOSTNAMES = {"localhost", "localhost.qz.io" };
|
||||
|
||||
private static int KEY_SIZE = 2048;
|
||||
public static int CA_CERT_AGE = 7305; // 20 years
|
||||
public static int SSL_CERT_AGE = 825; // Per https://support.apple.com/HT210176
|
||||
|
||||
private String[] hostNames;
|
||||
|
||||
public CertificateChainBuilder(String ... hostNames) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
if(hostNames.length > 0) {
|
||||
this.hostNames = hostNames;
|
||||
} else {
|
||||
this.hostNames = DEFAULT_HOSTNAMES;
|
||||
}
|
||||
}
|
||||
|
||||
public KeyPairWrapper createCaCert() throws IOException, GeneralSecurityException, OperatorException {
|
||||
KeyPair keyPair = createRsaKey();
|
||||
|
||||
X509v3CertificateBuilder builder = createX509Cert(keyPair, CA_CERT_AGE, hostNames);
|
||||
|
||||
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(1))
|
||||
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign + KeyUsage.cRLSign))
|
||||
.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
|
||||
|
||||
// Signing
|
||||
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate());
|
||||
X509CertificateHolder certHolder = builder.build(sign);
|
||||
|
||||
// Convert to java-friendly format
|
||||
return new KeyPairWrapper(CA, keyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||
}
|
||||
|
||||
public KeyPairWrapper createSslCert(KeyPairWrapper caKeyPairWrapper) throws IOException, GeneralSecurityException, OperatorException {
|
||||
KeyPair sslKeyPair = createRsaKey();
|
||||
X509v3CertificateBuilder builder = createX509Cert(sslKeyPair, SSL_CERT_AGE, hostNames);
|
||||
|
||||
JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils();
|
||||
|
||||
builder.addExtension(Extension.authorityKeyIdentifier, false, utils.createAuthorityKeyIdentifier(caKeyPairWrapper.getCert()))
|
||||
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
|
||||
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature + KeyUsage.keyEncipherment))
|
||||
.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}))
|
||||
.addExtension(Extension.subjectAlternativeName, false, buildSan(hostNames))
|
||||
.addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(sslKeyPair.getPublic()));
|
||||
|
||||
// Signing
|
||||
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caKeyPairWrapper.getKey());
|
||||
X509CertificateHolder certHolder = builder.build(sign);
|
||||
|
||||
// Convert to java-friendly format
|
||||
return new KeyPairWrapper(SSL, sslKeyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||
}
|
||||
|
||||
private static KeyPair createRsaKey() throws GeneralSecurityException {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
|
||||
return keyPairGenerator.generateKeyPair();
|
||||
}
|
||||
|
||||
private static X509v3CertificateBuilder createX509Cert(KeyPair keyPair, int age, String ... hostNames) {
|
||||
String cn = hostNames.length > 0? hostNames[0]:DEFAULT_HOSTNAMES[0];
|
||||
X500Name name = new X500NameBuilder()
|
||||
.addRDN(BCStyle.C, Constants.ABOUT_COUNTRY)
|
||||
.addRDN(BCStyle.ST, Constants.ABOUT_STATE)
|
||||
.addRDN(BCStyle.L, Constants.ABOUT_CITY)
|
||||
.addRDN(BCStyle.O, Constants.ABOUT_COMPANY)
|
||||
.addRDN(BCStyle.OU, Constants.ABOUT_COMPANY)
|
||||
.addRDN(BCStyle.EmailAddress, Constants.ABOUT_EMAIL)
|
||||
.addRDN(BCStyle.CN, cn)
|
||||
.build();
|
||||
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
|
||||
Calendar notBefore = Calendar.getInstance(Locale.ENGLISH);
|
||||
Calendar notAfter = Calendar.getInstance(Locale.ENGLISH);
|
||||
notBefore.add(Calendar.DAY_OF_YEAR, -1);
|
||||
notAfter.add(Calendar.DAY_OF_YEAR, age - 1);
|
||||
|
||||
SystemUtilities.swapLocale();
|
||||
X509v3CertificateBuilder x509builder = new JcaX509v3CertificateBuilder(name, serial, notBefore.getTime(), notAfter.getTime(), name, keyPair.getPublic());
|
||||
SystemUtilities.restoreLocale();
|
||||
return x509builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds subjectAlternativeName extension; iterates and detects IPv4 or hostname
|
||||
*/
|
||||
private static GeneralNames buildSan(String ... hostNames) {
|
||||
GeneralName[] gn = new GeneralName[hostNames.length];
|
||||
for (int i = 0; i < hostNames.length; i++) {
|
||||
int gnType = isIp(hostNames[i]) ? GeneralName.iPAddress : GeneralName.dNSName;
|
||||
gn[i] = new GeneralName(gnType, hostNames[i]);
|
||||
}
|
||||
return GeneralNames.getInstance(new DERSequence(gn));
|
||||
}
|
||||
|
||||
private static boolean isIp(String ip) {
|
||||
try {
|
||||
String[] split = ip.split("\\.");
|
||||
if (split.length != 4) return false;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int p = Integer.parseInt(split[i]);
|
||||
if (p > 255 || p < 0) return false;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception ignore) {}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
478
old code/tray/src/qz/installer/certificate/CertificateManager.java
Executable file
478
old code/tray/src/qz/installer/certificate/CertificateManager.java
Executable file
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
|
||||
import org.bouncycastle.asn1.x500.RDN;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.bouncycastle.operator.OperatorException;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.ArgValue;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.MacUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.*;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.*;
|
||||
|
||||
import static qz.utils.FileUtilities.*;
|
||||
import static qz.installer.certificate.KeyPairWrapper.Type.*;
|
||||
|
||||
/**
|
||||
* Stores and maintains reading and writing of certificate related files
|
||||
*/
|
||||
public class CertificateManager {
|
||||
static List<Path> SAVE_LOCATIONS = new ArrayList<>();
|
||||
static {
|
||||
// Workaround for JDK-8266929
|
||||
// See also https://github.com/qzind/tray/issues/814
|
||||
SystemUtilities.clearAlgorithms();
|
||||
|
||||
// Skip shared location if running from IDE or build directory
|
||||
// Prevents corrupting the version installed per https://github.com/qzind/tray/issues/1200
|
||||
if(SystemUtilities.isJar() && SystemUtilities.isInstalled()) {
|
||||
// Skip install location if running from sandbox (must remain sealed)
|
||||
if(!SystemUtilities.isMac() || !MacUtilities.isSandboxed()) {
|
||||
SAVE_LOCATIONS.add(SystemUtilities.getJarParentPath());
|
||||
}
|
||||
SAVE_LOCATIONS.add(SHARED_DIR);
|
||||
}
|
||||
SAVE_LOCATIONS.add(USER_DIR);
|
||||
}
|
||||
private static final Logger log = LogManager.getLogger(CertificateManager.class);
|
||||
|
||||
public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12";
|
||||
public static String DEFAULT_KEYSTORE_EXTENSION = ".p12";
|
||||
public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt";
|
||||
private static int DEFAULT_PASSWORD_BITS = 100;
|
||||
|
||||
private boolean needsInstall;
|
||||
private SslContextFactory.Server sslContextFactory;
|
||||
private KeyPairWrapper sslKeyPair;
|
||||
private KeyPairWrapper caKeyPair;
|
||||
|
||||
private Properties properties;
|
||||
private char[] password;
|
||||
|
||||
/**
|
||||
* For internal certs
|
||||
*/
|
||||
public CertificateManager(boolean forceNew, String ... hostNames) throws IOException, GeneralSecurityException, OperatorException {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
caKeyPair = new KeyPairWrapper(CA);
|
||||
|
||||
if (!forceNew) {
|
||||
// order is important: ssl, ca
|
||||
properties = loadProperties(sslKeyPair, caKeyPair);
|
||||
}
|
||||
|
||||
if(properties == null) {
|
||||
log.warn("Warning, SSL properties won't be loaded from disk... we'll try to create them...");
|
||||
|
||||
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
|
||||
caKeyPair = cb.createCaCert();
|
||||
sslKeyPair = cb.createSslCert(caKeyPair);
|
||||
|
||||
// Create CA
|
||||
properties = createKeyStore(CA)
|
||||
.writeCert(CA)
|
||||
.writeKeystore(null, CA);
|
||||
|
||||
// Create SSL
|
||||
properties = createKeyStore(SSL)
|
||||
.writeCert(SSL)
|
||||
.writeKeystore(properties, SSL);
|
||||
|
||||
// Save properties
|
||||
saveProperties();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For trusted PEM-formatted certs
|
||||
*/
|
||||
public CertificateManager(File trustedPemKey, File trustedPemCert) throws Exception {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
needsInstall = false;
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
|
||||
// Assumes ssl/privkey.pem, ssl/fullchain.pem
|
||||
properties = createTrustedKeystore(trustedPemKey, trustedPemCert)
|
||||
.writeKeystore(properties, SSL);
|
||||
|
||||
// Save properties
|
||||
saveProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* For trusted PKCS12-formatted certs
|
||||
*/
|
||||
public CertificateManager(File pkcs12File, char[] password) throws Exception {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
needsInstall = false;
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
|
||||
// Assumes direct pkcs12 import
|
||||
this.password = password;
|
||||
sslKeyPair.init(pkcs12File, password);
|
||||
|
||||
// Save it back, but to a location we can find
|
||||
properties = writeKeystore(null, SSL);
|
||||
|
||||
// Save properties
|
||||
saveProperties();
|
||||
}
|
||||
|
||||
public void renewCertChain(String ... hostNames) throws Exception {
|
||||
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
|
||||
sslKeyPair = cb.createSslCert(caKeyPair);
|
||||
createKeyStore(SSL).writeKeystore(properties, SSL);
|
||||
reloadSslContextFactory();
|
||||
}
|
||||
|
||||
public KeyPairWrapper getSslKeyPair() {
|
||||
return sslKeyPair;
|
||||
}
|
||||
|
||||
public KeyPairWrapper getCaKeyPair() {
|
||||
return caKeyPair;
|
||||
}
|
||||
|
||||
public KeyPairWrapper getKeyPair(KeyPairWrapper.Type type) {
|
||||
switch(type) {
|
||||
case SSL:
|
||||
return sslKeyPair;
|
||||
case CA:
|
||||
default:
|
||||
return caKeyPair;
|
||||
}
|
||||
}
|
||||
|
||||
public KeyPairWrapper getKeyPair(String alias) {
|
||||
for(KeyPairWrapper.Type type : KeyPairWrapper.Type.values()) {
|
||||
if (KeyPairWrapper.getAlias(type).equalsIgnoreCase(alias)) {
|
||||
return getKeyPair(type);
|
||||
}
|
||||
}
|
||||
return getKeyPair(KeyPairWrapper.Type.CA);
|
||||
}
|
||||
|
||||
public Properties getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
private char[] getPassword() {
|
||||
if (password == null) {
|
||||
if(caKeyPair != null && caKeyPair.getPassword() != null) {
|
||||
// Reuse existing
|
||||
password = caKeyPair.getPassword();
|
||||
} else {
|
||||
// Create new
|
||||
BigInteger bi = new BigInteger(DEFAULT_PASSWORD_BITS, new SecureRandom());
|
||||
password = bi.toString(16).toCharArray();
|
||||
log.info("Created a random {} bit password: {}", DEFAULT_PASSWORD_BITS, new String(password));
|
||||
}
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
public SslContextFactory.Server configureSslContextFactory() {
|
||||
sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
|
||||
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
|
||||
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
|
||||
return sslContextFactory;
|
||||
}
|
||||
|
||||
public void reloadSslContextFactory() throws Exception {
|
||||
if(isSslActive()) {
|
||||
sslContextFactory.reload(sslContextFactory -> {
|
||||
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
|
||||
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
|
||||
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
|
||||
});
|
||||
} else {
|
||||
log.warn("SSL isn't active, can't reload");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSslActive() {
|
||||
return sslContextFactory != null;
|
||||
}
|
||||
|
||||
public boolean needsInstall() {
|
||||
return needsInstall;
|
||||
}
|
||||
|
||||
public CertificateManager createKeyStore(KeyPairWrapper.Type type) throws IOException, GeneralSecurityException {
|
||||
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||
KeyStore keyStore = KeyStore.getInstance(DEFAULT_KEYSTORE_FORMAT);
|
||||
keyStore.load(null, password);
|
||||
|
||||
List<X509Certificate> chain = new ArrayList<>();
|
||||
chain.add(keyPair.getCert());
|
||||
|
||||
// Add ca to ssl cert chain
|
||||
if (keyPair.getType() == SSL) {
|
||||
chain.add(caKeyPair.getCert());
|
||||
}
|
||||
keyStore.setEntry(caKeyPair.getAlias(), new KeyStore.TrustedCertificateEntry(caKeyPair.getCert()), null);
|
||||
keyStore.setKeyEntry(keyPair.getAlias(), keyPair.getKey(), getPassword(), chain.toArray(new X509Certificate[chain.size()]));
|
||||
keyPair.init(keyStore, getPassword());
|
||||
return this;
|
||||
}
|
||||
|
||||
public CertificateManager createTrustedKeystore(File p12Store, String password) throws Exception {
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
sslKeyPair.init(p12Store, password.toCharArray());
|
||||
return this;
|
||||
}
|
||||
|
||||
public CertificateManager createTrustedKeystore(File pemKey, File pemCert) throws Exception {
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
|
||||
// Private Key
|
||||
PEMParser pem = new PEMParser(new FileReader(pemKey));
|
||||
Object parsedObject = pem.readObject();
|
||||
|
||||
PrivateKeyInfo privateKeyInfo = parsedObject instanceof PEMKeyPair ? ((PEMKeyPair)parsedObject).getPrivateKeyInfo() : (PrivateKeyInfo)parsedObject;
|
||||
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
|
||||
KeyFactory factory = KeyFactory.getInstance("RSA");
|
||||
PrivateKey key = factory.generatePrivate(privateKeySpec);
|
||||
|
||||
List<X509Certificate> certs = new ArrayList<>();
|
||||
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
|
||||
if(certHolder != null) {
|
||||
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||
}
|
||||
|
||||
// Certificate
|
||||
pem = new PEMParser(new FileReader(pemCert));
|
||||
while((certHolder = (X509CertificateHolder)pem.readObject()) != null) {
|
||||
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||
}
|
||||
|
||||
// Keystore
|
||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||
ks.load(null);
|
||||
|
||||
for (int i = 0; i < certs.size(); i++) {
|
||||
ks.setCertificateEntry(sslKeyPair.getAlias() + "_" + i, certs.get(i));
|
||||
}
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||
keyStore.load(null);
|
||||
keyStore.setKeyEntry(sslKeyPair.getAlias(), key, getPassword(), certs.toArray(new X509Certificate[certs.size()]));
|
||||
|
||||
sslKeyPair.init(keyStore, getPassword());
|
||||
return this;
|
||||
}
|
||||
|
||||
public static void writeCert(X509Certificate data, File dest) throws IOException {
|
||||
// PEMWriter doesn't always clear the file, explicitly delete it, see issue #796
|
||||
if(dest.exists()) {
|
||||
dest.delete();
|
||||
}
|
||||
JcaMiscPEMGenerator cert = new JcaMiscPEMGenerator(data);
|
||||
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(dest.toPath(), StandardOpenOption.CREATE)));
|
||||
writer.writeObject(cert.generate());
|
||||
writer.close();
|
||||
FileUtilities.inheritParentPermissions(dest.toPath());
|
||||
log.info("Wrote Cert: \"{}\"", dest);
|
||||
}
|
||||
|
||||
public CertificateManager writeCert(KeyPairWrapper.Type type) throws IOException {
|
||||
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||
File certFile = new File(getWritableLocation("ssl"), keyPair.getAlias() + DEFAULT_CERTIFICATE_EXTENSION);
|
||||
|
||||
writeCert(keyPair.getCert(), certFile);
|
||||
FileUtilities.inheritParentPermissions(certFile.toPath());
|
||||
if(keyPair.getType() == CA) {
|
||||
needsInstall = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) throws GeneralSecurityException, IOException {
|
||||
File sslDir = getWritableLocation("ssl");
|
||||
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||
|
||||
File keyFile = new File(sslDir, keyPair.getAlias() + DEFAULT_KEYSTORE_EXTENSION);
|
||||
keyPair.getKeyStore().store(Files.newOutputStream(keyFile.toPath(), StandardOpenOption.CREATE), getPassword());
|
||||
FileUtilities.inheritParentPermissions(keyFile.toPath());
|
||||
log.info("Wrote {} Key: \"{}\"", DEFAULT_KEYSTORE_FORMAT, keyFile);
|
||||
|
||||
if (props == null) {
|
||||
props = new Properties();
|
||||
}
|
||||
props.putIfAbsent(String.format("%s.keystore", keyPair.propsPrefix()), keyFile.toString());
|
||||
props.putIfAbsent(String.format("%s.storepass", keyPair.propsPrefix()), new String(getPassword()));
|
||||
props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias());
|
||||
|
||||
if (keyPair.getType() == SSL) {
|
||||
props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), ArgValue.SECURITY_WSS_HOST.getDefaultVal());
|
||||
}
|
||||
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
public static File getWritableLocation(String ... suffixes) throws IOException {
|
||||
// Get an array of preferred directories
|
||||
ArrayList<Path> locs = new ArrayList<>();
|
||||
|
||||
if (suffixes.length == 0) {
|
||||
locs.addAll(SAVE_LOCATIONS);
|
||||
// Last, fallback on a directory we won't ever see again :/
|
||||
locs.add(TEMP_DIR);
|
||||
} else {
|
||||
// Same as above, but with suffixes added (usually "ssl"), skipping the install location
|
||||
for(Path saveLocation : SAVE_LOCATIONS) {
|
||||
if(!saveLocation.equals(SystemUtilities.getJarParentPath())) {
|
||||
locs.add(Paths.get(saveLocation.toString(), suffixes));
|
||||
}
|
||||
}
|
||||
// Last, fallback on a directory we won't ever see again :/
|
||||
locs.add(Paths.get(TEMP_DIR.toString(), suffixes));
|
||||
}
|
||||
|
||||
// Find a suitable write location
|
||||
File path;
|
||||
for(Path loc : locs) {
|
||||
if (loc == null) continue;
|
||||
boolean isPreferred = locs.indexOf(loc) == 0;
|
||||
path = loc.toFile();
|
||||
path.mkdirs();
|
||||
if (path.canWrite()) {
|
||||
log.debug("Writing to {}", loc);
|
||||
if(!isPreferred) {
|
||||
log.warn("Warning, {} isn't the preferred write location, but we'll use it anyway", loc);
|
||||
}
|
||||
return path;
|
||||
} else {
|
||||
log.debug("Can't write to {}, trying the next...", loc);
|
||||
}
|
||||
}
|
||||
throw new IOException("Can't find a suitable write location. SSL will fail.");
|
||||
}
|
||||
|
||||
public static Properties loadProperties(KeyPairWrapper... keyPairs) {
|
||||
log.info("Try to find SSL properties file...");
|
||||
|
||||
|
||||
Properties props = null;
|
||||
for(Path loc : SAVE_LOCATIONS) {
|
||||
if (loc == null) continue;
|
||||
try {
|
||||
for(KeyPairWrapper keyPair : keyPairs) {
|
||||
props = loadKeyPair(keyPair, loc, props);
|
||||
}
|
||||
// We've loaded without Exception, return
|
||||
log.info("Found {}/{}.properties", loc, Constants.PROPS_FILE);
|
||||
return props;
|
||||
} catch(Exception ignore) {
|
||||
log.warn("Properties couldn't be loaded at {}, trying fallback...", loc, ignore);
|
||||
}
|
||||
}
|
||||
log.info("Could not get SSL properties from file.");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Properties loadKeyPair(KeyPairWrapper keyPair, Path parent, Properties existing) throws Exception {
|
||||
Properties props;
|
||||
|
||||
if (existing == null) {
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
props = new Properties();
|
||||
props.load(fis = new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties")));
|
||||
} finally {
|
||||
if(fis != null) fis.close();
|
||||
}
|
||||
} else {
|
||||
props = existing;
|
||||
}
|
||||
|
||||
String ks = props.getProperty(String.format("%s.keystore", keyPair.propsPrefix()));
|
||||
String pw = props.getProperty(String.format("%s.storepass", keyPair.propsPrefix()), "");
|
||||
|
||||
if(ks == null || ks.trim().isEmpty()) {
|
||||
if(keyPair.getType() == SSL) {
|
||||
throw new IOException("Missing wss.keystore entry");
|
||||
} else {
|
||||
// CA is only needed for internal certs, return
|
||||
return props;
|
||||
}
|
||||
}
|
||||
File ksFile = Paths.get(ks).isAbsolute()? new File(ks):new File(parent.toFile(), ks);
|
||||
if (ksFile.exists()) {
|
||||
keyPair.init(ksFile, pw.toCharArray());
|
||||
return props;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void saveProperties() throws IOException {
|
||||
File propsFile = new File(getWritableLocation(), Constants.PROPS_FILE + ".properties");
|
||||
Installer.persistProperties(propsFile, properties); // checks for props from previous install
|
||||
properties.store(new FileOutputStream(propsFile), null);
|
||||
FileUtilities.inheritParentPermissions(propsFile.toPath());
|
||||
log.info("Successfully created SSL properties file: {}", propsFile);
|
||||
}
|
||||
|
||||
public static boolean emailMatches(X509Certificate cert) {
|
||||
return emailMatches(cert, false);
|
||||
}
|
||||
|
||||
public static boolean emailMatches(X509Certificate cert, boolean quiet) {
|
||||
try {
|
||||
X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
|
||||
RDN[] emailNames = x500name.getRDNs(BCStyle.E);
|
||||
for(RDN emailName : emailNames) {
|
||||
AttributeTypeAndValue first = emailName.getFirst();
|
||||
if (first != null && first.getValue() != null && Constants.ABOUT_EMAIL.equals(first.getValue().toString())) {
|
||||
if(!quiet) {
|
||||
log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, ExpiryTask.CertProvider.INTERNAL);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ignore) {}
|
||||
if(!quiet) {
|
||||
log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
295
old code/tray/src/qz/installer/certificate/ExpiryTask.java
Executable file
295
old code/tray/src/qz/installer/certificate/ExpiryTask.java
Executable file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.naming.InvalidNameException;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import javax.naming.ldap.Rdn;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
import static qz.utils.FileUtilities.*;
|
||||
|
||||
public class ExpiryTask extends TimerTask {
|
||||
private static final Logger log = LogManager.getLogger(CertificateManager.class);
|
||||
public static final int DEFAULT_INITIAL_DELAY = 60 * 1000; // 1 minute
|
||||
public static final int DEFAULT_CHECK_FREQUENCY = 3600 * 1000; // 1 hour
|
||||
private static final int DEFAULT_GRACE_PERIOD_DAYS = 5;
|
||||
private enum ExpiryState {VALID, EXPIRING, EXPIRED, MANAGED}
|
||||
|
||||
public enum CertProvider {
|
||||
INTERNAL(Constants.ABOUT_COMPANY + ".*"),
|
||||
LETS_ENCRYPT("Let's Encrypt.*"),
|
||||
CA_CERT_ORG("CA Cert Signing.*"),
|
||||
UNKNOWN;
|
||||
String[] patterns;
|
||||
CertProvider(String ... regexPattern) {
|
||||
this.patterns = regexPattern;
|
||||
}
|
||||
}
|
||||
|
||||
private Timer timer;
|
||||
private CertificateManager certificateManager;
|
||||
private String[] hostNames;
|
||||
private CertProvider certProvider;
|
||||
|
||||
public ExpiryTask(CertificateManager certificateManager) {
|
||||
super();
|
||||
this.certificateManager = certificateManager;
|
||||
this.hostNames = parseHostNames();
|
||||
this.certProvider = findCertProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Check for expiration
|
||||
ExpiryState state = getExpiry(certificateManager.getSslKeyPair().getCert());
|
||||
switch(state) {
|
||||
case EXPIRING:
|
||||
case EXPIRED:
|
||||
log.info("Certificate ExpiryState {}, renewing/reloading...", state);
|
||||
switch(certProvider) {
|
||||
case INTERNAL:
|
||||
if(renewInternalCert()) {
|
||||
getExpiry();
|
||||
}
|
||||
break;
|
||||
case CA_CERT_ORG:
|
||||
case LETS_ENCRYPT:
|
||||
if(renewExternalCert(certProvider)) {
|
||||
getExpiry();
|
||||
}
|
||||
break;
|
||||
case UNKNOWN:
|
||||
default:
|
||||
log.warn("Certificate can't be renewed/reloaded; ExpiryState: {}, CertProvider: {}", state, certProvider);
|
||||
}
|
||||
case VALID:
|
||||
default:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean renewInternalCert() {
|
||||
try {
|
||||
log.info("Requesting a new SSL certificate from {} ...", certificateManager.getCaKeyPair().getAlias());
|
||||
certificateManager.renewCertChain(hostNames);
|
||||
log.info("New SSL certificate created. Reloading SslContextFactory...");
|
||||
certificateManager.reloadSslContextFactory();
|
||||
log.info("Reloaded SSL successfully.");
|
||||
return true;
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Could not reload SSL certificate", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ExpiryState getExpiry() {
|
||||
return getExpiry(certificateManager.getSslKeyPair().getCert());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the SSL certificate is generated by QZ Tray and expires inside the GRACE_PERIOD.
|
||||
* GRACE_PERIOD is preferred for scheduling the renewals in advance, such as non-peak hours
|
||||
*/
|
||||
public static ExpiryState getExpiry(X509Certificate cert) {
|
||||
// Invalid
|
||||
if (cert == null) {
|
||||
log.error("Can't check for expiration, certificate is missing.");
|
||||
return ExpiryState.EXPIRED;
|
||||
}
|
||||
|
||||
Date expireDate = cert.getNotAfter();
|
||||
Calendar now = Calendar.getInstance(Locale.ENGLISH);
|
||||
Calendar expires = Calendar.getInstance(Locale.ENGLISH);
|
||||
expires.setTime(expireDate);
|
||||
|
||||
// Expired
|
||||
if (now.after(expires)) {
|
||||
log.info("SSL certificate has expired {}. It must be renewed immediately.", SystemUtilities.toISO(expireDate));
|
||||
return ExpiryState.EXPIRED;
|
||||
}
|
||||
|
||||
// Expiring
|
||||
expires.add(Calendar.DAY_OF_YEAR, -DEFAULT_GRACE_PERIOD_DAYS);
|
||||
if (now.after(expires)) {
|
||||
log.info("SSL certificate will expire in less than {} days: {}", DEFAULT_GRACE_PERIOD_DAYS, SystemUtilities.toISO(expireDate));
|
||||
return ExpiryState.EXPIRING;
|
||||
}
|
||||
|
||||
// Valid
|
||||
int days = (int)Math.round((expireDate.getTime() - new Date().getTime()) / (double)86400000);
|
||||
log.info("SSL certificate is still valid for {} more days: {}. We'll make a new one automatically when needed.", days, SystemUtilities.toISO(expireDate));
|
||||
return ExpiryState.VALID;
|
||||
}
|
||||
|
||||
public void schedule() {
|
||||
schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY);
|
||||
}
|
||||
|
||||
public void schedule(int delayMillis, int freqMillis) {
|
||||
if(timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
}
|
||||
timer = new Timer();
|
||||
timer.scheduleAtFixedRate(this, delayMillis, freqMillis);
|
||||
}
|
||||
|
||||
public String[] parseHostNames() {
|
||||
return parseHostNames(certificateManager.getSslKeyPair().getCert());
|
||||
}
|
||||
|
||||
public CertProvider findCertProvider() {
|
||||
return findCertProvider(certificateManager.getSslKeyPair().getCert());
|
||||
}
|
||||
|
||||
public static CertProvider findCertProvider(X509Certificate cert) {
|
||||
// Internal certs use CN=localhost, trust email instead
|
||||
if (CertificateManager.emailMatches(cert)) {
|
||||
return CertProvider.INTERNAL;
|
||||
}
|
||||
|
||||
String providerDN;
|
||||
|
||||
// check registered patterns to classify certificate
|
||||
if(cert.getIssuerDN() != null && (providerDN = cert.getIssuerDN().getName()) != null) {
|
||||
String cn = null;
|
||||
try {
|
||||
// parse issuer's DN
|
||||
LdapName ldapName = new LdapName(providerDN);
|
||||
for(Rdn rdn : ldapName.getRdns()) {
|
||||
if(rdn.getType().equalsIgnoreCase("CN")) {
|
||||
cn = (String)rdn.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// compare cn to our pattern
|
||||
if(cn != null) {
|
||||
for(CertProvider provider : CertProvider.values()) {
|
||||
for(String pattern : provider.patterns) {
|
||||
if (cn.matches(pattern)) {
|
||||
log.warn("Cert issuer detected as {}", provider.name());
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(InvalidNameException ignore) {}
|
||||
}
|
||||
|
||||
log.warn("A valid issuer couldn't be found, we won't know how to renew this cert when it expires");
|
||||
return CertProvider.UNKNOWN;
|
||||
}
|
||||
|
||||
public static String[] parseHostNames(X509Certificate cert) {
|
||||
// Cache the SAN hosts for recreation
|
||||
List<String> hostNameList = new ArrayList<>();
|
||||
try {
|
||||
Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
|
||||
if (altNames != null) {
|
||||
for(List<?> altName : altNames) {
|
||||
if(altName.size()< 1) continue;
|
||||
switch((Integer)altName.get(0)) {
|
||||
case GeneralName.dNSName:
|
||||
case GeneralName.iPAddress:
|
||||
Object data = altName.get(1);
|
||||
if (data instanceof String) {
|
||||
hostNameList.add(((String)data));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error("getSubjectAlternativeNames is null?");
|
||||
}
|
||||
log.debug("Parsed hostNames: {}", String.join(", ", hostNameList));
|
||||
} catch(CertificateException e) {
|
||||
log.warn("Can't parse hostNames from this cert. Cert renewals will contain default values instead");
|
||||
}
|
||||
return hostNameList.toArray(new String[hostNameList.size()]);
|
||||
}
|
||||
|
||||
public boolean renewExternalCert(CertProvider externalProvider) {
|
||||
switch(externalProvider) {
|
||||
case LETS_ENCRYPT:
|
||||
return renewLetsEncryptCert(externalProvider);
|
||||
case CA_CERT_ORG:
|
||||
default:
|
||||
log.error("Cert renewal for {} is not implemented", externalProvider);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean renewLetsEncryptCert(CertProvider externalProvider) {
|
||||
try {
|
||||
File storagePath = CertificateManager.getWritableLocation("ssl");
|
||||
|
||||
// cerbot is much simpler than acme, let's use it
|
||||
Path root = Paths.get(SHARED_DIR.toString(), "letsencrypt", "config");
|
||||
log.info("Attempting to renew {}. Assuming certs are installed in {}...", externalProvider, root);
|
||||
List<String> cmds = new ArrayList(Arrays.asList("certbot", "--force-renewal", "certonly"));
|
||||
|
||||
cmds.add("--standalone");
|
||||
|
||||
cmds.add("--config-dir");
|
||||
String config = Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "config").toString();
|
||||
cmds.add(config);
|
||||
|
||||
cmds.add("--logs-dir");
|
||||
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "logs").toString());
|
||||
|
||||
cmds.add("--work-dir");
|
||||
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt").toString());
|
||||
|
||||
// append dns names
|
||||
for(String hostName : hostNames) {
|
||||
cmds.add("-d");
|
||||
cmds.add(hostName);
|
||||
}
|
||||
|
||||
if (ShellUtilities.execute(cmds.toArray(new String[cmds.size()]))) {
|
||||
// Assume the cert is stored in a folder called "letsencrypt/config/live/<domain>"
|
||||
Path keyPath = Paths.get(config, "live", hostNames[0], "privkey.pem");
|
||||
Path certPath = Paths.get(config, "live", hostNames[0], "fullchain.pem"); // fullchain required
|
||||
certificateManager.createTrustedKeystore(keyPath.toFile(), certPath.toFile());
|
||||
log.info("Files imported, converted and saved. Reloading SslContextFactory...");
|
||||
certificateManager.reloadSslContextFactory();
|
||||
log.info("Reloaded SSL successfully.");
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Something went wrong renewing the LetsEncrypt certificate. Please run the certbot command manually to learn more.");
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error renewing/reloading LetsEncrypt cert", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
130
old code/tray/src/qz/installer/certificate/KeyPairWrapper.java
Executable file
130
old code/tray/src/qz/installer/certificate/KeyPairWrapper.java
Executable file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import qz.common.Constants;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Enumeration;
|
||||
|
||||
/**
|
||||
* Wrap handling of X509Certificate, PrivateKey and KeyStore conversion
|
||||
*/
|
||||
public class KeyPairWrapper {
|
||||
public enum Type {CA, SSL}
|
||||
|
||||
private Type type;
|
||||
private PrivateKey key;
|
||||
private char[] password;
|
||||
private X509Certificate cert;
|
||||
private KeyStore keyStore; // for SSL
|
||||
|
||||
public KeyPairWrapper(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public KeyPairWrapper(Type type, KeyPair keyPair, X509Certificate cert) {
|
||||
this.type = type;
|
||||
this.key = keyPair.getPrivate();
|
||||
this.cert = cert;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load from disk
|
||||
*/
|
||||
public void init(File keyFile, char[] password) throws IOException, GeneralSecurityException {
|
||||
KeyStore keyStore = KeyStore.getInstance(keyFile.getName().endsWith(".jks") ? "JKS" : "PKCS12");
|
||||
keyStore.load(new FileInputStream(keyFile), password);
|
||||
init(keyStore, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load from memory
|
||||
*/
|
||||
public void init(KeyStore keyStore, char[] password) throws GeneralSecurityException {
|
||||
this.keyStore = keyStore;
|
||||
KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(password);
|
||||
KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(getAlias(), param);
|
||||
// the entry we assume is always wrong for pkcs12 imports, search for it instead
|
||||
if(entry == null) {
|
||||
Enumeration<String> enumerator = keyStore.aliases();
|
||||
while(enumerator.hasMoreElements()) {
|
||||
String alias = enumerator.nextElement();
|
||||
if(keyStore.isKeyEntry(alias)) {
|
||||
this.password = password;
|
||||
this.key = ((KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, param)).getPrivateKey();
|
||||
this.cert = (X509Certificate)keyStore.getCertificate(alias);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new GeneralSecurityException("Could not initialize the KeyStore for internal use");
|
||||
}
|
||||
|
||||
this.password = password;
|
||||
this.key = entry.getPrivateKey();
|
||||
this.cert = (X509Certificate)keyStore.getCertificate(getAlias());
|
||||
}
|
||||
|
||||
public X509Certificate getCert() {
|
||||
return cert;
|
||||
}
|
||||
|
||||
public PrivateKey getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getPasswordString() {
|
||||
return new String(password);
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public static String getAlias(Type type) {
|
||||
switch(type) {
|
||||
case SSL:
|
||||
return Constants.PROPS_FILE; // "qz-tray"
|
||||
case CA:
|
||||
default:
|
||||
return "root-ca";
|
||||
}
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return getAlias(getType());
|
||||
}
|
||||
|
||||
public String propsPrefix() {
|
||||
switch(type) {
|
||||
case SSL:
|
||||
return "wss";
|
||||
case CA:
|
||||
default:
|
||||
return "ca";
|
||||
}
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public KeyStore getKeyStore() {
|
||||
return keyStore;
|
||||
}
|
||||
}
|
||||
365
old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java
Executable file
365
old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java
Executable file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import qz.auth.X509Constants;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.ByteUtilities;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.UnixUtilities;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static qz.installer.Installer.PrivilegeLevel.*;
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
public class LinuxCertificateInstaller extends NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(LinuxCertificateInstaller.class);
|
||||
private static final String CA_CERTIFICATES = "/usr/local/share/ca-certificates/";
|
||||
private static final String CA_CERTIFICATE_NAME = Constants.PROPS_FILE + "-root.crt"; // e.g. qz-tray-root.crt
|
||||
private static final String PK11_KIT_ID = "pkcs11:id=";
|
||||
|
||||
private static String[] NSSDB_URLS = {
|
||||
// Conventional cert store
|
||||
"sql:" + System.getenv("HOME") + "/.pki/nssdb/",
|
||||
|
||||
// Snap-specific cert stores
|
||||
"sql:" + System.getenv("HOME") + "/snap/chromium/current/.pki/nssdb/",
|
||||
"sql:" + System.getenv("HOME") + "/snap/brave/current/.pki/nssdb/",
|
||||
"sql:" + System.getenv("HOME") + "/snap/opera/current/.pki/nssdb/",
|
||||
"sql:" + System.getenv("HOME") + "/snap/opera-beta/current/.pki/nssdb/"
|
||||
};
|
||||
|
||||
private Installer.PrivilegeLevel certType;
|
||||
|
||||
public LinuxCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||
setInstallType(certType);
|
||||
findCertutil();
|
||||
}
|
||||
|
||||
public Installer.PrivilegeLevel getInstallType() {
|
||||
return certType;
|
||||
}
|
||||
|
||||
public void setInstallType(Installer.PrivilegeLevel certType) {
|
||||
this.certType = certType;
|
||||
if (this.certType == SYSTEM) {
|
||||
log.warn("Command \"certutil\" (required for certain browsers) needs to run as USER. We'll try again on launch.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(List<String> idList) {
|
||||
boolean success = true;
|
||||
if(certType == SYSTEM) {
|
||||
boolean first = distrustUsingUpdateCaCertificates(idList);
|
||||
boolean second = distrustUsingTrustAnchor(idList);
|
||||
success = first || second;
|
||||
} else {
|
||||
for(String nickname : idList) {
|
||||
for(String nssdb : NSSDB_URLS) {
|
||||
success = success && ShellUtilities.execute("certutil", "-d", nssdb, "-D", "-n", nickname);
|
||||
}
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public List<String> find() {
|
||||
ArrayList<String> nicknames = new ArrayList<>();
|
||||
if(certType == SYSTEM) {
|
||||
nicknames = findUsingTrustAnchor();
|
||||
nicknames.addAll(findUsingUsingUpdateCaCert());
|
||||
} else {
|
||||
try {
|
||||
for(String nssdb : NSSDB_URLS) {
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"certutil", "-d", nssdb, "-L"});
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while((line = in.readLine()) != null) {
|
||||
if (line.startsWith(Constants.ABOUT_COMPANY + " ")) {
|
||||
nicknames.add(Constants.ABOUT_COMPANY);
|
||||
break; // Stop reading input; nicknames can't appear more than once
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Could not get certificate nicknames", e);
|
||||
}
|
||||
}
|
||||
return nicknames;
|
||||
}
|
||||
|
||||
public boolean verify(File ignore) { return true; } // no easy way to validate a cert, assume it's installed
|
||||
|
||||
public boolean add(File certFile) {
|
||||
boolean success = true;
|
||||
|
||||
if(certType == SYSTEM) {
|
||||
// Attempt two common methods for installing the SSL certificate
|
||||
File systemCertFile;
|
||||
boolean first = (systemCertFile = trustUsingUpdateCaCertificates(certFile)) != null;
|
||||
boolean second = trustUsingTrustAnchor(systemCertFile, certFile);
|
||||
success = first || second;
|
||||
} else if(certType == USER) {
|
||||
// Install certificate to local profile using "certutil"
|
||||
for(String nssdb : NSSDB_URLS) {
|
||||
String[] parts = nssdb.split(":", 2);
|
||||
if (parts.length > 1) {
|
||||
File folder = new File(parts[1]);
|
||||
// If .pki/nssdb doesn't exist yet, don't create it! Per https://github.com/qzind/tray/issues/1003
|
||||
if(folder.exists() && folder.isDirectory()) {
|
||||
if (!ShellUtilities.execute("certutil", "-d", nssdb, "-A", "-t", "TC", "-n", Constants.ABOUT_COMPANY, "-i", certFile.getPath())) {
|
||||
log.warn("Something went wrong creating {}. HTTPS will fail on certain browsers which depend on it.", nssdb);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private boolean findCertutil() {
|
||||
boolean installed = ShellUtilities.execute("which", "certutil");
|
||||
if (!installed) {
|
||||
if (certType == SYSTEM && promptCertutil()) {
|
||||
if(UnixUtilities.isUbuntu() || UnixUtilities.isDebian()) {
|
||||
installed = ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools");
|
||||
} else if(UnixUtilities.isFedora()) {
|
||||
installed = ShellUtilities.execute("dnf", "install", "-y", "nss-tools");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!installed) {
|
||||
log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on certain browsers which depend on it.");
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
|
||||
private boolean promptCertutil() {
|
||||
// Assume silent or headless installs want certutil
|
||||
if(Installer.IS_SILENT || GraphicsEnvironment.isHeadless()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
SystemUtilities.setSystemLookAndFeel(true);
|
||||
return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(null, "A critical component, \"certutil\" wasn't found. Attempt to fetch it now?");
|
||||
} catch(Throwable ignore) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common technique for installing system-wide certificates on Debian-based systems (Ubuntu, etc.)
|
||||
*
|
||||
* This technique is only known to work for select browsers, such as Epiphany. Browsers such as
|
||||
* Firefox and Chromium require different techniques.
|
||||
*
|
||||
* @return Full path to the destination file if successful, otherwise <code>null</code>
|
||||
*/
|
||||
private File trustUsingUpdateCaCertificates(File certFile) {
|
||||
if(hasUpdateCaCertificatesCommand()) {
|
||||
File destFile = new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME);
|
||||
log.debug("Copying SYSTEM SSL certificate {} to {}", certFile.getPath(), destFile.getPath());
|
||||
try {
|
||||
if (new File(CA_CERTIFICATES).isDirectory()) {
|
||||
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
|
||||
FileUtils.copyFile(certFile, destFile, false);
|
||||
if (destFile.isFile()) {
|
||||
// Attempt "update-ca-certificates" (Debian)
|
||||
if (!ShellUtilities.execute("update-ca-certificates")) {
|
||||
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
|
||||
} else {
|
||||
return destFile;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("{} is not a valid directory, skipping", CA_CERTIFICATES);
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Error copying SYSTEM SSL certificate file", e);
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping SYSTEM SSL certificate install using \"update-ca-certificates\", command missing or invalid");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common technique for installing system-wide certificates on Fedora-based systems
|
||||
*
|
||||
* Uses first existing non-null file provided
|
||||
*/
|
||||
private boolean trustUsingTrustAnchor(File ... certFiles) {
|
||||
if (hasTrustAnchorCommand()) {
|
||||
for(File certFile : certFiles) {
|
||||
if (certFile == null || !certFile.exists()) {
|
||||
continue;
|
||||
}
|
||||
// Install certificate to system using "trust anchor" (Fedora)
|
||||
if (ShellUtilities.execute("trust", "anchor", "--store", certFile.getPath())) {
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping SYSTEM SSL certificate install using \"trust anchor\", command missing or invalid");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean distrustUsingUpdateCaCertificates(List<String> paths) {
|
||||
if(hasUpdateCaCertificatesCommand()) {
|
||||
boolean deleted = false;
|
||||
for(String path : paths) {
|
||||
// Process files only; not "trust anchor" URIs
|
||||
if(!path.startsWith(PK11_KIT_ID)) {
|
||||
File certFile = new File(path);
|
||||
if (certFile.isFile() && certFile.delete()) {
|
||||
deleted = true;
|
||||
} else {
|
||||
log.warn("SYSTEM SSL certificate {} does not exist, skipping", certFile.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Attempt "update-ca-certificates" (Debian)
|
||||
if(deleted) {
|
||||
if (ShellUtilities.execute("update-ca-certificates")) {
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping SYSTEM SSL certificate removal using \"update-ca-certificates\", command missing or invalid");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean distrustUsingTrustAnchor(List<String> idList) {
|
||||
if(hasTrustAnchorCommand()) {
|
||||
for(String id : idList) {
|
||||
// only remove by id
|
||||
if (id.startsWith(PK11_KIT_ID) && !ShellUtilities.execute("trust", "anchor", "--remove", id)) {
|
||||
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping SYSTEM SSL certificate removal using \"trust anchor\", command missing or invalid");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for the presence of a QZ certificate in known locations (e.g. /usr/local/share/ca-certificates/
|
||||
* and return the path if found
|
||||
*/
|
||||
private ArrayList<String> findUsingUsingUpdateCaCert() {
|
||||
ArrayList<String> found = new ArrayList<>();
|
||||
File[] systemCertFiles = { new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME) };
|
||||
for(File file : systemCertFiles) {
|
||||
if(file.isFile()) {
|
||||
found.add(file.getPath());
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find QZ installed certificates in the "trust anchor" by searching by email.
|
||||
*
|
||||
* The "trust" utility identifies certificates as URIs:
|
||||
* Example:
|
||||
* pkcs11:id=%7C%5D%02%84%13%D4%CC%8A%9B%81%CE%17%1C%2E%29%1E%9C%48%63%42;type=cert
|
||||
* ... which is an encoded version of the cert's SubjectKeyIdentifier field
|
||||
* To identify a match:
|
||||
* 1. Extract all trusted certificates and look for a familiar email address
|
||||
* 2. If found, construct and store a "trust" compatible URI as the nickname
|
||||
*/
|
||||
private ArrayList<String> findUsingTrustAnchor() {
|
||||
ArrayList<String> uris = new ArrayList<>();
|
||||
File tempFile = null;
|
||||
try {
|
||||
// Temporary location for system certificates
|
||||
tempFile = File.createTempFile("trust-extract-for-qz-", ".pem");
|
||||
// Delete before use: "trust extract" requires an empty file
|
||||
tempFile.delete();
|
||||
if(ShellUtilities.execute("trust", "extract", "--format", "pem-bundle", tempFile.getPath())) {
|
||||
BufferedReader reader = new BufferedReader(new FileReader(tempFile));
|
||||
String line;
|
||||
StringBuilder base64 = new StringBuilder();
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if(line.startsWith(X509Constants.BEGIN_CERT)) {
|
||||
// Beginning of a new certificate
|
||||
base64.setLength(0);
|
||||
} else if(line.startsWith(X509Constants.END_CERT)) {
|
||||
// End of the existing certificate
|
||||
byte[] certBytes = Base64.decode(base64.toString());
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate cert = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
if(CertificateManager.emailMatches(cert, true)) {
|
||||
byte[] extensionValue = cert.getExtensionValue(Extension.subjectKeyIdentifier.getId());
|
||||
byte[] octets = DEROctetString.getInstance(extensionValue).getOctets();
|
||||
SubjectKeyIdentifier subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets);
|
||||
byte[] keyIdentifier = subjectKeyIdentifier.getKeyIdentifier();
|
||||
String hex = ByteUtilities.bytesToHex(keyIdentifier, true);
|
||||
String uri = PK11_KIT_ID + hex.replaceAll("(.{2})", "%$1") + ";type=cert";
|
||||
log.info("Found matching cert: {}", uri);
|
||||
|
||||
uris.add(uri);
|
||||
}
|
||||
} else {
|
||||
base64.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
reader.close();
|
||||
}
|
||||
} catch(IOException | CertificateException e) {
|
||||
log.warn("An error occurred finding preexisting \"trust anchor\" certificates", e);
|
||||
} finally {
|
||||
if(tempFile != null && !tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
return uris;
|
||||
}
|
||||
|
||||
private boolean hasUpdateCaCertificatesCommand() {
|
||||
return ShellUtilities.execute("which", "update-ca-certificates");
|
||||
}
|
||||
|
||||
private boolean hasTrustAnchorCommand() {
|
||||
return ShellUtilities.execute("trust", "anchor", "--help");
|
||||
}
|
||||
}
|
||||
91
old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java
Executable file
91
old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java
Executable file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.ShellUtilities;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MacCertificateInstaller extends NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(MacCertificateInstaller.class);
|
||||
|
||||
public static final String USER_STORE = System.getProperty("user.home") + "/Library/Keychains/login.keychain"; // aka login.keychain-db
|
||||
public static final String SYSTEM_STORE = "/Library/Keychains/System.keychain";
|
||||
private String certStore;
|
||||
|
||||
public MacCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||
setInstallType(certType);
|
||||
}
|
||||
|
||||
public boolean add(File certFile) {
|
||||
if (certStore.equals(USER_STORE)) {
|
||||
// This will prompt the user
|
||||
return ShellUtilities.execute("security", "add-trusted-cert", "-r", "trustRoot", "-k", certStore, certFile.getPath());
|
||||
} else {
|
||||
return ShellUtilities.execute("security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", certStore, certFile.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(List<String> idList) {
|
||||
boolean success = true;
|
||||
for (String certId : idList) {
|
||||
success = success && ShellUtilities.execute("security", "delete-certificate", "-Z", certId, certStore);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public List<String> find() {
|
||||
ArrayList<String> hashList = new ArrayList<>();
|
||||
try {
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"security", "find-certificate", "-a", "-e", Constants.ABOUT_EMAIL, "-Z", certStore});
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
if (line.contains("SHA-1") && line.contains(":")) {
|
||||
hashList.add(line.split(":", 2)[1].trim());
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not get certificate list", e);
|
||||
}
|
||||
return hashList;
|
||||
}
|
||||
|
||||
public boolean verify(File certFile) {
|
||||
return ShellUtilities.execute( "security", "verify-cert", "-c", certFile.getPath());
|
||||
}
|
||||
|
||||
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||
if (type == Installer.PrivilegeLevel.USER) {
|
||||
certStore = USER_STORE;
|
||||
} else {
|
||||
certStore = SYSTEM_STORE;
|
||||
}
|
||||
}
|
||||
|
||||
public Installer.PrivilegeLevel getInstallType() {
|
||||
if (certStore == USER_STORE) {
|
||||
return Installer.PrivilegeLevel.USER;
|
||||
} else {
|
||||
return Installer.PrivilegeLevel.SYSTEM;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java
Executable file
105
old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java
Executable file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(NativeCertificateInstaller.class);
|
||||
protected static NativeCertificateInstaller instance;
|
||||
|
||||
public static NativeCertificateInstaller getInstance() {
|
||||
return getInstance(SystemUtilities.isAdmin() ? Installer.PrivilegeLevel.SYSTEM : Installer.PrivilegeLevel.USER);
|
||||
}
|
||||
public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) {
|
||||
if (instance == null) {
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
instance = new WindowsCertificateInstaller(type);
|
||||
break;
|
||||
case MAC:
|
||||
instance = new MacCertificateInstaller(type);
|
||||
break;
|
||||
case LINUX:
|
||||
default:
|
||||
instance = new LinuxCertificateInstaller(type);
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a certificate from memory
|
||||
*/
|
||||
public boolean install(X509Certificate cert) {
|
||||
File certFile = null;
|
||||
try {
|
||||
certFile = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
|
||||
JcaMiscPEMGenerator generator = new JcaMiscPEMGenerator(cert);
|
||||
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(certFile.toPath(), StandardOpenOption.CREATE)));
|
||||
writer.writeObject(generator.generate());
|
||||
writer.close();
|
||||
|
||||
return install(certFile);
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not install cert from temp file", e);
|
||||
} finally {
|
||||
if(certFile != null && !certFile.delete()) {
|
||||
certFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a certificate from disk
|
||||
*/
|
||||
public boolean install(File certFile) {
|
||||
String helper = instance.getClass().getSimpleName();
|
||||
String store = instance.getInstallType().name();
|
||||
if(SystemUtilities.isJar()) {
|
||||
if (remove(find())) {
|
||||
log.info("Certificate removed from {} store using {}", store, helper);
|
||||
} else {
|
||||
log.warn("Could not remove certificate from {} store using {}", store, helper);
|
||||
}
|
||||
} else {
|
||||
log.info("Skipping {} store certificate removal, IDE detected.", store, helper);
|
||||
}
|
||||
if (add(certFile)) {
|
||||
log.info("Certificate added to {} store using {}", store, helper);
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Could not install certificate to {} store using {}", store, helper);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract boolean add(File certFile);
|
||||
public abstract boolean remove(List<String> idList);
|
||||
public abstract List<String> find();
|
||||
public abstract boolean verify(File certFile);
|
||||
public abstract void setInstallType(Installer.PrivilegeLevel certType);
|
||||
public abstract Installer.PrivilegeLevel getInstallType();
|
||||
}
|
||||
236
old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java
Executable file
236
old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java
Executable file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.Structure;
|
||||
import com.sun.jna.platform.win32.Kernel32Util;
|
||||
import com.sun.jna.platform.win32.WinNT;
|
||||
import com.sun.jna.win32.StdCallLibrary;
|
||||
import com.sun.jna.win32.W32APIOptions;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
|
||||
import java.io.*;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
public class WindowsCertificateInstaller extends NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(WindowsCertificateInstaller.class);
|
||||
private WinCrypt.HCERTSTORE store;
|
||||
private byte[] certBytes;
|
||||
private Installer.PrivilegeLevel certType;
|
||||
|
||||
public WindowsCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||
setInstallType(certType);
|
||||
}
|
||||
|
||||
public boolean add(File certFile) {
|
||||
log.info("Writing certificate {} to {} store using Crypt32...", certFile, certType);
|
||||
try {
|
||||
|
||||
byte[] bytes = getCertBytes(certFile);
|
||||
Pointer pointer = new Memory(bytes.length);
|
||||
pointer.write(0, bytes, 0, bytes.length);
|
||||
|
||||
boolean success = Crypt32.INSTANCE.CertAddEncodedCertificateToStore(
|
||||
openStore(),
|
||||
WinCrypt.X509_ASN_ENCODING,
|
||||
pointer,
|
||||
bytes.length,
|
||||
Crypt32.CERT_STORE_ADD_REPLACE_EXISTING,
|
||||
null
|
||||
);
|
||||
if(!success) {
|
||||
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||
}
|
||||
|
||||
closeStore();
|
||||
|
||||
return success;
|
||||
} catch(IOException e) {
|
||||
log.warn("An error occurred installing the certificate", e);
|
||||
} finally {
|
||||
certBytes = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private byte[] getCertBytes(File certFile) throws IOException {
|
||||
if(certBytes == null) {
|
||||
PEMParser pem = new PEMParser(new FileReader(certFile));
|
||||
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
|
||||
certBytes = certHolder.getEncoded();
|
||||
}
|
||||
return certBytes;
|
||||
}
|
||||
|
||||
private WinCrypt.HCERTSTORE openStore() {
|
||||
if(store == null) {
|
||||
store = openStore(certType);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
private void closeStore() {
|
||||
if(store != null && closeStore(store)) {
|
||||
store = null;
|
||||
} else {
|
||||
log.warn("Unable to close {} cert store", certType);
|
||||
}
|
||||
}
|
||||
|
||||
private static WinCrypt.HCERTSTORE openStore(Installer.PrivilegeLevel certType) {
|
||||
log.info("Opening {} store using Crypt32...", certType);
|
||||
|
||||
WinCrypt.HCERTSTORE store = Crypt32.INSTANCE.CertOpenStore(
|
||||
Crypt32.CERT_STORE_PROV_SYSTEM,
|
||||
0,
|
||||
null,
|
||||
certType == Installer.PrivilegeLevel.USER ? Crypt32.CERT_SYSTEM_STORE_CURRENT_USER : Crypt32.CERT_SYSTEM_STORE_LOCAL_MACHINE,
|
||||
"ROOT"
|
||||
);
|
||||
if(store == null) {
|
||||
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
private static boolean closeStore(WinCrypt.HCERTSTORE certStore) {
|
||||
boolean isClosed = Crypt32.INSTANCE.CertCloseStore(
|
||||
certStore, 0
|
||||
);
|
||||
if(!isClosed) {
|
||||
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||
}
|
||||
return isClosed;
|
||||
}
|
||||
|
||||
public boolean remove(List<String> ignore) {
|
||||
boolean success = true;
|
||||
|
||||
WinCrypt.CERT_CONTEXT hCertContext;
|
||||
WinCrypt.CERT_CONTEXT pPrevCertContext = null;
|
||||
while(true) {
|
||||
hCertContext = Crypt32.INSTANCE.CertFindCertificateInStore(
|
||||
openStore(),
|
||||
WinCrypt.X509_ASN_ENCODING,
|
||||
0,
|
||||
Crypt32.CERT_FIND_SUBJECT_STR,
|
||||
Constants.ABOUT_EMAIL,
|
||||
pPrevCertContext);
|
||||
|
||||
if(hCertContext == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
pPrevCertContext = Crypt32.INSTANCE.CertDuplicateCertificateContext(hCertContext);
|
||||
|
||||
if(success = (success && Crypt32.INSTANCE.CertDeleteCertificateFromStore(hCertContext))) {
|
||||
log.info("Successfully deleted certificate matching {}", Constants.ABOUT_EMAIL);
|
||||
} else {
|
||||
log.info("Could not delete certificate: {}", Kernel32Util.formatMessage(Native.getLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
closeStore();
|
||||
return success;
|
||||
}
|
||||
|
||||
public List<String> find() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||
this.certType = type;
|
||||
}
|
||||
|
||||
public Installer.PrivilegeLevel getInstallType() {
|
||||
return certType;
|
||||
}
|
||||
|
||||
public boolean verify(File certFile) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
md.update(getCertBytes(certFile));
|
||||
WinCrypt.DATA_BLOB thumbPrint = new WinCrypt.DATA_BLOB(md.digest());
|
||||
WinNT.HANDLE cert = Crypt32.INSTANCE.CertFindCertificateInStore(
|
||||
openStore(),
|
||||
WinCrypt.X509_ASN_ENCODING,
|
||||
0,
|
||||
Crypt32.CERT_FIND_SHA1_HASH,
|
||||
thumbPrint,
|
||||
null);
|
||||
|
||||
return cert != null;
|
||||
} catch(IOException | NoSuchAlgorithmException e) {
|
||||
log.warn("An error occurred verifying the cert is installed: {}", certFile, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The JNA's Crypt32 instance oversimplifies store handling, preventing user stores from being used
|
||||
*/
|
||||
interface Crypt32 extends StdCallLibrary {
|
||||
int CERT_SYSTEM_STORE_CURRENT_USER = 65536;
|
||||
int CERT_SYSTEM_STORE_LOCAL_MACHINE = 131072;
|
||||
int CERT_STORE_PROV_SYSTEM = 10;
|
||||
int CERT_STORE_ADD_REPLACE_EXISTING = 3;
|
||||
int CERT_FIND_SUBJECT_STR = 524295;
|
||||
int CERT_FIND_SHA1_HASH = 65536;
|
||||
|
||||
Crypt32 INSTANCE = Native.load("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS);
|
||||
|
||||
WinCrypt.HCERTSTORE CertOpenStore(int lpszStoreProvider, int dwMsgAndCertEncodingType, Pointer hCryptProv, int dwFlags, String pvPara);
|
||||
boolean CertCloseStore(WinCrypt.HCERTSTORE hCertStore, int dwFlags);
|
||||
boolean CertAddEncodedCertificateToStore(WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, Pointer pbCertEncoded, int cbCertEncoded, int dwAddDisposition, Pointer ppCertContext);
|
||||
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, String pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
|
||||
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, Structure pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
|
||||
boolean CertDeleteCertificateFromStore(WinCrypt.CERT_CONTEXT pCertContext);
|
||||
boolean CertFreeCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
|
||||
WinCrypt.CERT_CONTEXT CertDuplicateCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
|
||||
}
|
||||
|
||||
// Polyfill from JNA5+
|
||||
@SuppressWarnings("UnusedDeclaration") //Library class
|
||||
public static class WinCrypt {
|
||||
public static int X509_ASN_ENCODING = 0x00000001;
|
||||
public static class HCERTSTORE extends WinNT.HANDLE {
|
||||
public HCERTSTORE() {}
|
||||
public HCERTSTORE(Pointer p) {
|
||||
super(p);
|
||||
}
|
||||
}
|
||||
public static class CERT_CONTEXT extends WinNT.HANDLE {
|
||||
public CERT_CONTEXT() {}
|
||||
public CERT_CONTEXT(Pointer p) {
|
||||
super(p);
|
||||
}
|
||||
}
|
||||
public static class DATA_BLOB extends com.sun.jna.platform.win32.WinCrypt.DATA_BLOB {
|
||||
// Wrap the constructor for code readability
|
||||
public DATA_BLOB() {
|
||||
super();
|
||||
}
|
||||
public DATA_BLOB(byte[] data) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
Executable file
136
old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
Executable file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Command Line technique for installing certificates on Windows
|
||||
* Fallback class for when JNA is not available (e.g. Windows on ARM)
|
||||
*/
|
||||
@SuppressWarnings("UnusedDeclaration") //Library class
|
||||
public class WindowsCertificateInstallerCli extends NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(WindowsCertificateInstallerCli.class);
|
||||
private Installer.PrivilegeLevel certType;
|
||||
|
||||
public WindowsCertificateInstallerCli(Installer.PrivilegeLevel certType) {
|
||||
setInstallType(certType);
|
||||
}
|
||||
|
||||
public boolean add(File certFile) {
|
||||
if (WindowsUtilities.isWindowsXP()) return false;
|
||||
if (certType == Installer.PrivilegeLevel.USER) {
|
||||
// This will prompt the user
|
||||
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "-user", "Root", certFile.getPath());
|
||||
} else {
|
||||
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "Root", certFile.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(List<String> idList) {
|
||||
if (WindowsUtilities.isWindowsXP()) return false;
|
||||
boolean success = true;
|
||||
for (String certId : idList) {
|
||||
if (certType == Installer.PrivilegeLevel.USER) {
|
||||
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "-user", "Root", certId);
|
||||
} else {
|
||||
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "Root", certId);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of serials, if found
|
||||
*/
|
||||
public List<String> find() {
|
||||
ArrayList<String> serialList = new ArrayList<>();
|
||||
try {
|
||||
Process p;
|
||||
if (certType == Installer.PrivilegeLevel.USER) {
|
||||
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "-user", "Root"});
|
||||
} else {
|
||||
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "Root"});
|
||||
}
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
if (line.contains("================")) {
|
||||
// First line is serial
|
||||
String serial = parseNextLine(in);
|
||||
if (serial != null) {
|
||||
// Second line is issuer
|
||||
String issuer = parseNextLine(in);
|
||||
if (issuer.contains("OU=" + Constants.ABOUT_COMPANY)) {
|
||||
serialList.add(serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch(Exception e) {
|
||||
log.info("Unable to find a Trusted Root Certificate matching \"OU={}\"", Constants.ABOUT_COMPANY);
|
||||
}
|
||||
return serialList;
|
||||
}
|
||||
|
||||
public boolean verify(File certFile) {
|
||||
return verifyCert(certFile);
|
||||
}
|
||||
|
||||
public static boolean verifyCert(File certFile) {
|
||||
// -user also will check the root store
|
||||
String dwErrorStatus = ShellUtilities.execute( new String[] {"certutil", "-user", "-verify", certFile.getPath() }, new String[] { "dwErrorStatus=" }, false, false);
|
||||
if(!dwErrorStatus.isEmpty()) {
|
||||
String[] parts = dwErrorStatus.split("[\r\n\\s]+");
|
||||
for(String part : parts) {
|
||||
if(part.startsWith("dwErrorStatus=")) {
|
||||
log.info("Certificate validity says {}", part);
|
||||
String[] status = part.split("=", 2);
|
||||
if (status.length == 2) {
|
||||
return status[1].trim().equals("0");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("Unable to determine certificate validity, you'll be prompted on startup");
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||
this.certType = type;
|
||||
}
|
||||
|
||||
public Installer.PrivilegeLevel getInstallType() {
|
||||
return certType;
|
||||
}
|
||||
|
||||
private static String parseNextLine(BufferedReader reader) throws IOException {
|
||||
String data = reader.readLine();
|
||||
if (data != null) {
|
||||
String[] split = data.split(":", 2);
|
||||
if (split.length == 2) {
|
||||
return split[1].trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package qz.installer.certificate.firefox;
|
||||
|
||||
class ConflictingPolicyException extends Exception {
|
||||
ConflictingPolicyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate.firefox;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import com.sun.jna.platform.win32.WinReg;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.installer.certificate.CertificateManager;
|
||||
import qz.installer.certificate.firefox.locator.AppAlias;
|
||||
import qz.installer.certificate.firefox.locator.AppInfo;
|
||||
import qz.installer.certificate.firefox.locator.AppLocator;
|
||||
import qz.utils.JsonWriter;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Installs the Firefox Policy file via Enterprise Policy, Distribution Policy file or AutoConfig, depending on OS & version
|
||||
*/
|
||||
public class FirefoxCertificateInstaller {
|
||||
protected static final Logger log = LogManager.getLogger(FirefoxCertificateInstaller.class);
|
||||
|
||||
/**
|
||||
* Versions are for Mozilla's official Firefox release.
|
||||
* 3rd-party/clones may adopt Enterprise Policy support under
|
||||
* different version numbers, adapt as needed.
|
||||
*/
|
||||
private static final Version WINDOWS_POLICY_VERSION = Version.valueOf("62.0.0");
|
||||
private static final Version MAC_POLICY_VERSION = Version.valueOf("63.0.0");
|
||||
private static final Version LINUX_POLICY_VERSION = Version.valueOf("65.0.0");
|
||||
public static final Version FIREFOX_RESTART_VERSION = Version.valueOf("60.0.0");
|
||||
|
||||
public static final String LINUX_GLOBAL_POLICY_LOCATION = "/etc/firefox/policies/policies.json";
|
||||
public static final String LINUX_SNAP_CERT_LOCATION = "/etc/firefox/policies/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION; // See https://github.com/mozilla/policy-templates/issues/936
|
||||
public static final String LINUX_GLOBAL_CERT_LOCATION = "/usr/lib/mozilla/certificates/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION;
|
||||
private static String DISTRIBUTION_ENTERPRISE_ROOT_POLICY = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }";
|
||||
private static String DISTRIBUTION_INSTALL_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION + "\", \"" + LINUX_SNAP_CERT_LOCATION + "\" ] } } }";
|
||||
private static String DISTRIBUTION_REMOVE_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"/opt/" + Constants.PROPS_FILE + "/auth/root-ca.crt\"] } } }";
|
||||
|
||||
public static final String DISTRIBUTION_POLICY_LOCATION = "distribution/policies.json";
|
||||
public static final String DISTRIBUTION_MAC_POLICY_LOCATION = "Contents/Resources/" + DISTRIBUTION_POLICY_LOCATION;
|
||||
|
||||
public static final String POLICY_AUDIT_MESSAGE = "Enterprise policy installed by " + Constants.ABOUT_TITLE + " on " + SystemUtilities.timeStamp();
|
||||
|
||||
public static void install(X509Certificate cert, String ... hostNames) {
|
||||
// Blindly install Firefox enterprise policies to the system (macOS, Windows)
|
||||
ArrayList<AppAlias.Alias> enterpriseFailed = new ArrayList<>();
|
||||
for(AppAlias.Alias alias : AppAlias.FIREFOX.getAliases()) {
|
||||
boolean success = false;
|
||||
try {
|
||||
if(alias.isEnterpriseReady() && !hasEnterprisePolicy(alias, false)) {
|
||||
log.info("Installing Firefox enterprise certificate policy for {}", alias);
|
||||
success = installEnterprisePolicy(alias, false);
|
||||
}
|
||||
} catch(ConflictingPolicyException e) {
|
||||
log.warn("Conflict found installing {} enterprise cert support. We'll fallback on the distribution policy instead", alias.getName(), e);
|
||||
}
|
||||
if(!success) {
|
||||
enterpriseFailed.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
// Search for installed instances
|
||||
ArrayList<AppInfo> foundApps = AppLocator.getInstance().locate(AppAlias.FIREFOX);
|
||||
ArrayList<Path> processPaths = null;
|
||||
|
||||
for(AppInfo appInfo : foundApps) {
|
||||
boolean success = false;
|
||||
if (honorsPolicy(appInfo)) {
|
||||
if((SystemUtilities.isWindows()|| SystemUtilities.isMac()) && !enterpriseFailed.contains(appInfo.getAlias())) {
|
||||
// Enterprise policy was already installed
|
||||
success = true;
|
||||
} else {
|
||||
log.info("Installing Firefox distribution policy for {}", appInfo);
|
||||
success = installDistributionPolicy(appInfo, cert);
|
||||
}
|
||||
} else {
|
||||
log.info("Installing Firefox auto-config script for {}", appInfo);
|
||||
try {
|
||||
String certData = Base64.getEncoder().encodeToString(cert.getEncoded());
|
||||
success = LegacyFirefoxCertificateInstaller.installAutoConfigScript(appInfo, certData, hostNames);
|
||||
}
|
||||
catch(CertificateEncodingException e) {
|
||||
log.warn("Unable to install auto-config script for {}", appInfo, e);
|
||||
}
|
||||
}
|
||||
if(success) {
|
||||
issueRestartWarning(processPaths = AppLocator.getRunningPaths(foundApps, processPaths), appInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void uninstall() {
|
||||
ArrayList<AppInfo> appList = AppLocator.getInstance().locate(AppAlias.FIREFOX);
|
||||
for(AppInfo appInfo : appList) {
|
||||
if(honorsPolicy(appInfo)) {
|
||||
if(SystemUtilities.isWindows() || SystemUtilities.isMac()) {
|
||||
log.info("Skipping uninstall of Firefox enterprise root certificate policy for {}", appInfo);
|
||||
} else {
|
||||
try {
|
||||
File policy = appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toFile();
|
||||
if(policy.exists()) {
|
||||
JsonWriter.write(appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toString(), DISTRIBUTION_INSTALL_CERT_POLICY, false, true);
|
||||
}
|
||||
} catch(IOException | JSONException e) {
|
||||
log.warn("Unable to remove Firefox policy for {}", appInfo, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("Uninstalling Firefox auto-config script for {}", appInfo);
|
||||
LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(appInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean honorsPolicy(AppInfo appInfo) {
|
||||
if (appInfo.getVersion() == null) {
|
||||
log.warn("Firefox-compatible browser found {}, but no version information is available", appInfo);
|
||||
return false;
|
||||
}
|
||||
if(SystemUtilities.isWindows()) {
|
||||
return appInfo.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION);
|
||||
} else if (SystemUtilities.isMac()) {
|
||||
return appInfo.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION);
|
||||
} else {
|
||||
return appInfo.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an alternative Firefox policy (e.g. registry, plist user or system) is installed
|
||||
*/
|
||||
private static boolean hasEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) throws ConflictingPolicyException {
|
||||
if(SystemUtilities.isWindows()) {
|
||||
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));
|
||||
Integer foundPolicy = WindowsUtilities.getRegInt(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots");
|
||||
if(foundPolicy != null) {
|
||||
return foundPolicy == 1;
|
||||
}
|
||||
} else if(SystemUtilities.isMac()) {
|
||||
String policyLocation = "/Library/Preferences/";
|
||||
if(userOnly) {
|
||||
policyLocation = System.getProperty("user.home") + policyLocation;
|
||||
}
|
||||
String policesEnabled = ShellUtilities.executeRaw(new String[] { "defaults", "read", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled"}, true);
|
||||
String foundPolicy = ShellUtilities.executeRaw(new String[] {"defaults", "read", policyLocation + alias.getBundleId(), "Certificates"}, true);
|
||||
if(!policesEnabled.isEmpty() && !foundPolicy.isEmpty()) {
|
||||
// Policies exist, decide how to proceed
|
||||
if(policesEnabled.trim().equals("1") && foundPolicy.contains("ImportEnterpriseRoots = 1;")) {
|
||||
return true;
|
||||
}
|
||||
throw new ConflictingPolicyException(String.format("%s enterprise policy conflict at %s: %s", alias.getName(), policyLocation + alias.getBundleId(), foundPolicy));
|
||||
}
|
||||
} else {
|
||||
// Linux alternate policy not yet supported
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install policy to distribution/policies.json
|
||||
*/
|
||||
public static boolean installDistributionPolicy(AppInfo app, X509Certificate cert) {
|
||||
Path jsonPath = app.getPath().resolve(SystemUtilities.isMac() ? DISTRIBUTION_MAC_POLICY_LOCATION:DISTRIBUTION_POLICY_LOCATION);
|
||||
String jsonPolicy = SystemUtilities.isWindows() || SystemUtilities.isMac() ? DISTRIBUTION_ENTERPRISE_ROOT_POLICY:DISTRIBUTION_INSTALL_CERT_POLICY;
|
||||
|
||||
// Special handling for snaps
|
||||
if(app.getPath().toString().startsWith("/snap")) {
|
||||
log.info("Snap detected, installing policy file to global location instead: {}", LINUX_GLOBAL_POLICY_LOCATION);
|
||||
jsonPath = Paths.get(LINUX_GLOBAL_POLICY_LOCATION);
|
||||
}
|
||||
|
||||
try {
|
||||
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
|
||||
// Linux lacks the concept of "enterprise roots", we'll write it to a known location instead
|
||||
writeCertFile(cert, LINUX_SNAP_CERT_LOCATION); // so that the snap can read from it
|
||||
writeCertFile(cert, LINUX_GLOBAL_CERT_LOCATION); // default location for non-snaps
|
||||
}
|
||||
|
||||
File jsonFile = jsonPath.toFile();
|
||||
|
||||
// Make sure we can traverse and read
|
||||
File distribution = jsonFile.getParentFile();
|
||||
distribution.mkdirs();
|
||||
distribution.setReadable(true, false);
|
||||
distribution.setExecutable(true, false);
|
||||
|
||||
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
|
||||
// Delete previous policy
|
||||
JsonWriter.write(jsonPath.toString(), DISTRIBUTION_REMOVE_CERT_POLICY, false, true);
|
||||
}
|
||||
|
||||
JsonWriter.write(jsonPath.toString(), jsonPolicy, false, false);
|
||||
|
||||
// Make sure ew can read
|
||||
jsonFile.setReadable(true, false);
|
||||
return true;
|
||||
} catch(JSONException | IOException e) {
|
||||
log.warn("Could not install distribution policy {} to {}", jsonPolicy, jsonPath.toString(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean installEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) {
|
||||
if(SystemUtilities.isWindows()) {
|
||||
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));;
|
||||
WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "Comment", POLICY_AUDIT_MESSAGE);
|
||||
return WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots", 1);
|
||||
} else if(SystemUtilities.isMac()) {
|
||||
String policyLocation = "/Library/Preferences/";
|
||||
if(userOnly) {
|
||||
policyLocation = System.getProperty("user.home") + policyLocation;
|
||||
}
|
||||
return ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled", "-bool", "TRUE"}, true) &&
|
||||
ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "Certificates", "-dict", "ImportEnterpriseRoots", "-bool", "TRUE",
|
||||
"Comment", "-string", POLICY_AUDIT_MESSAGE}, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean issueRestartWarning(ArrayList<Path> runningPaths, AppInfo appInfo) {
|
||||
boolean firefoxIsRunning = runningPaths.contains(appInfo.getExePath());
|
||||
|
||||
// Edge case for detecting if snap is running, since we can't compare the exact path easily
|
||||
for(Path runningPath : runningPaths) {
|
||||
if(runningPath.startsWith("/snap/")) {
|
||||
firefoxIsRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (firefoxIsRunning) {
|
||||
if (appInfo.getVersion().greaterThanOrEqualTo(FirefoxCertificateInstaller.FIREFOX_RESTART_VERSION)) {
|
||||
try {
|
||||
Installer.getInstance().spawn(appInfo.getExePath().toString(), "-private", "about:restartrequired");
|
||||
return true;
|
||||
}
|
||||
catch(Exception ignore) {}
|
||||
} else {
|
||||
log.warn("{} must be restarted manually for changes to take effect", appInfo);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void writeCertFile(X509Certificate cert, String location) throws IOException {
|
||||
File certFile = new File(location);
|
||||
|
||||
// Make sure we can traverse and read
|
||||
File certs = new File(location).getParentFile();
|
||||
certs.mkdirs();
|
||||
certs.setReadable(true, false);
|
||||
certs.setExecutable(true, false);
|
||||
File mozilla = certs.getParentFile();
|
||||
mozilla.setReadable(true, false);
|
||||
mozilla.setExecutable(true, false);
|
||||
|
||||
// Make sure we can read
|
||||
CertificateManager.writeCert(cert, certFile);
|
||||
certFile.setReadable(true, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate.firefox;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.certificate.CertificateChainBuilder;
|
||||
import qz.installer.certificate.firefox.locator.AppInfo;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Legacy Firefox Certificate installer
|
||||
*
|
||||
* For old Firefox-compatible browsers still in the wild such as Firefox 52 ESR, SeaMonkey, WaterFox, etc.
|
||||
*/
|
||||
public class LegacyFirefoxCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(CertificateChainBuilder.class);
|
||||
|
||||
private static final String CFG_TEMPLATE = "assets/firefox-autoconfig.js.in";
|
||||
private static final String CFG_FILE = Constants.PROPS_FILE + ".cfg";
|
||||
private static final String PREFS_FILE = Constants.PROPS_FILE + ".js";
|
||||
private static final String PREFS_DIR = "defaults/pref";
|
||||
private static final String MAC_PREFIX = "Contents/Resources";
|
||||
|
||||
public static boolean installAutoConfigScript(AppInfo appInfo, String certData, String ... hostNames) {
|
||||
try {
|
||||
if(appInfo.getPath().toString().equals("/usr/bin")) {
|
||||
throw new Exception("Preventing install to root location");
|
||||
}
|
||||
writePrefsFile(appInfo);
|
||||
writeParsedConfig(appInfo, certData, false, hostNames);
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
log.warn("Error installing auto-config support for {}", appInfo, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean uninstallAutoConfigScript(AppInfo appInfo) {
|
||||
try {
|
||||
writeParsedConfig(appInfo, "", true);
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
log.warn("Error uninstalling auto-config support for {}", appInfo, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static File tryWrite(AppInfo appInfo, boolean mkdirs, String ... paths) throws IOException {
|
||||
Path dir = appInfo.getPath();
|
||||
if (SystemUtilities.isMac()) {
|
||||
dir = dir.resolve(MAC_PREFIX);
|
||||
}
|
||||
for (String path : paths) {
|
||||
dir = dir.resolve(path);
|
||||
}
|
||||
File file = dir.toFile();
|
||||
|
||||
if(mkdirs) file.mkdirs();
|
||||
if(file.exists() && file.isDirectory() && file.canWrite()) {
|
||||
return file;
|
||||
}
|
||||
|
||||
throw new IOException(String.format("Directory does not exist or is not writable: %s", file));
|
||||
}
|
||||
|
||||
public static void deleteFile(File parent, String ... paths) {
|
||||
if(parent != null) {
|
||||
String toDelete = parent.getPath();
|
||||
for (String path : paths) {
|
||||
toDelete += File.separator + path;
|
||||
}
|
||||
File deleteFile = new File(toDelete);
|
||||
if (!deleteFile.exists()) {
|
||||
} else if (new File(toDelete).delete()) {
|
||||
log.info("Deleted old file: {}", toDelete);
|
||||
} else {
|
||||
log.warn("Could not delete old file: {}", toDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writePrefsFile(AppInfo app) throws Exception {
|
||||
File prefsDir = tryWrite(app, true, PREFS_DIR);
|
||||
deleteFile(prefsDir, "firefox-prefs.js"); // cleanup old version
|
||||
|
||||
// first check that there aren't other prefs files
|
||||
String pref = "general.config.filename";
|
||||
for (File file : prefsDir.listFiles()) {
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
if(line.contains(pref) && !line.contains(CFG_FILE)) {
|
||||
throw new Exception(String.format("Browser already has %s defined in %s:\n %s", pref, file, line));
|
||||
}
|
||||
}
|
||||
} catch(IOException ignore) {}
|
||||
}
|
||||
|
||||
// write out the new prefs file
|
||||
File prefsFile = new File(prefsDir, PREFS_FILE);
|
||||
BufferedWriter writer = new BufferedWriter(new FileWriter(prefsFile));
|
||||
String[] data = {
|
||||
String.format("pref('%s', '%s');", pref, CFG_FILE),
|
||||
"pref('general.config.obscure_value', 0);"
|
||||
};
|
||||
for (String line : data) {
|
||||
writer.write(line + "\n");
|
||||
}
|
||||
writer.close();
|
||||
prefsFile.setReadable(true, false);
|
||||
}
|
||||
|
||||
private static void writeParsedConfig(AppInfo appInfo, String certData, boolean uninstall, String ... hostNames) throws IOException, CertificateEncodingException{
|
||||
if (hostNames.length == 0) hostNames = CertificateChainBuilder.DEFAULT_HOSTNAMES;
|
||||
|
||||
File cfgDir = tryWrite(appInfo, false);
|
||||
deleteFile(cfgDir, "firefox-config.cfg"); // cleanup old version
|
||||
File dest = new File(cfgDir.getPath(), CFG_FILE);
|
||||
|
||||
HashMap<String, String> fieldMap = new HashMap<>();
|
||||
// Dynamic fields
|
||||
fieldMap.put("%CERT_DATA%", certData);
|
||||
fieldMap.put("%COMMON_NAME%", hostNames[0]);
|
||||
fieldMap.put("%TIMESTAMP%", uninstall ? "-1" : "" + new Date().getTime());
|
||||
fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.getAppPath() != null ? SystemUtilities.getAppPath().toString() : "" : "");
|
||||
fieldMap.put("%UNINSTALL%", "" + uninstall);
|
||||
|
||||
FileUtilities.configureAssetFile(CFG_TEMPLATE, dest, fieldMap, LegacyFirefoxCertificateInstaller.class);
|
||||
dest.setReadable(true, false);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// Firefox AutoConfig Certificate Installer for Legacy Firefox versions
|
||||
// This is part of the QZ Tray application
|
||||
//
|
||||
var serviceObserver = {
|
||||
observe: function observe(aSubject, aTopic, aData) {
|
||||
// Get NSS certdb object
|
||||
var certdb = getCertDB();
|
||||
|
||||
if (needsUninstall()) {
|
||||
deleteCertificate();
|
||||
unregisterProtocol();
|
||||
} else if (needsCert()) {
|
||||
deleteCertificate();
|
||||
installCertificate();
|
||||
registerProtocol();
|
||||
}
|
||||
|
||||
// Compares the timestamp embedded in this script against that stored in the browser's about:config
|
||||
function needsCert() {
|
||||
try {
|
||||
return getPref("%PROPS_FILE%.installer.timestamp") != "%TIMESTAMP%";
|
||||
} catch(notfound) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Installs the embedded base64 certificate into the browser
|
||||
function installCertificate() {
|
||||
certdb.addCertFromBase64(getCertData(), "C,C,C", "%COMMON_NAME% - %ABOUT_COMPANY%");
|
||||
pref("%PROPS_FILE%.installer.timestamp", "%TIMESTAMP%");
|
||||
}
|
||||
|
||||
// Deletes the certificate, if it exists
|
||||
function deleteCertificate() {
|
||||
var certs = certdb.getCerts();
|
||||
var enumerator = certs.getEnumerator();
|
||||
while (enumerator.hasMoreElements()) {
|
||||
var cert = enumerator.getNext().QueryInterface(Components.interfaces.nsIX509Cert);
|
||||
if (cert.containsEmailAddress("%ABOUT_EMAIL%")) {
|
||||
try {
|
||||
certdb.deleteCertificate(cert);
|
||||
} catch (ignore) {}
|
||||
}
|
||||
}
|
||||
pref("%PROPS_FILE%.installer.timestamp", "-1");
|
||||
}
|
||||
|
||||
// Register the specified protocol to open with the specified application
|
||||
function registerProtocol() {
|
||||
// Only register if platform needs it (e.g. macOS)
|
||||
var trayApp = "%APP_PATH%";
|
||||
if (!trayApp) { return; }
|
||||
try {
|
||||
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
|
||||
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
|
||||
|
||||
var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);
|
||||
file.initWithPath(trayApp);
|
||||
|
||||
var lhandler = Components.classes["@mozilla.org/uriloader/local-handler-app;1"].createInstance(Components.interfaces.nsILocalHandlerApp);
|
||||
lhandler.executable = file;
|
||||
lhandler.name = "%PROPS_FILE%";
|
||||
|
||||
var protocol = pservice.getProtocolHandlerInfo("%DATA_DIR%");
|
||||
protocol.preferredApplicationHandler = lhandler;
|
||||
protocol.preferredAction = 2; // useHelperApp
|
||||
protocol.alwaysAskBeforeHandling = false;
|
||||
hservice.store(protocol);
|
||||
} catch(ignore) {}
|
||||
}
|
||||
|
||||
// De-register the specified protocol from opening with the specified application
|
||||
function unregisterProtocol() {
|
||||
// Only register if platform needs it (e.g. macOS)
|
||||
var trayApp = "%APP_PATH%";
|
||||
if (!trayApp) { return; }
|
||||
try {
|
||||
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
|
||||
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
|
||||
hservice.remove(pservice.getProtocolHandlerInfo("%DATA_DIR%"));
|
||||
} catch(ignore) {}
|
||||
}
|
||||
|
||||
// Get certdb object
|
||||
function getCertDB() {
|
||||
// Import certificate using NSS certdb API (http://tinyurl.com/x509certdb)
|
||||
var id = "@mozilla.org/security/x509certdb;1";
|
||||
var db1 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB);
|
||||
var db2 = db1;
|
||||
try {
|
||||
db2 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB2);
|
||||
} catch(ignore) {}
|
||||
return db2;
|
||||
}
|
||||
|
||||
// The certificate to import (automatically generated by desktop installer)
|
||||
function getCertData() {
|
||||
return "%CERT_DATA%";
|
||||
}
|
||||
|
||||
// Whether or not an uninstall should occur, flagged by the installer/uninstaller
|
||||
function needsUninstall() {
|
||||
try {
|
||||
if (getPref("%PROPS_FILE%.installer.timestamp") == "-1") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch(notfound) {
|
||||
return false;
|
||||
}
|
||||
return %UNINSTALL%;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Services.obs.addObserver(serviceObserver, "profile-after-change", false);
|
||||
91
old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java
Executable file
91
old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java
Executable file
@@ -0,0 +1,91 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum AppAlias {
|
||||
// Tor Browser intentionally excluded; Tor's proxy blocks localhost connections
|
||||
FIREFOX(
|
||||
new Alias("Mozilla", "Mozilla Firefox", "org.mozilla.firefox", true),
|
||||
new Alias("Mozilla", "Firefox Developer Edition", "org.mozilla.firefoxdeveloperedition", true),
|
||||
new Alias("Mozilla", "Firefox Nightly", "org.mozilla.nightly", true),
|
||||
new Alias("Mozilla", "SeaMonkey", "org.mozilla.seamonkey", false),
|
||||
new Alias("Waterfox", "Waterfox", "net.waterfox.waterfoxcurrent", true),
|
||||
new Alias("Waterfox", "Waterfox Classic", "org.waterfoxproject.waterfox classic", false),
|
||||
new Alias("Mozilla", "Pale Moon", "org.mozilla.palemoon", false),
|
||||
// IceCat is technically enterprise ready, but not officially distributed for macOS, Windows
|
||||
new Alias("Mozilla", "IceCat", "org.gnu.icecat", false)
|
||||
);
|
||||
Alias[] aliases;
|
||||
AppAlias(Alias... aliases) {
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
public Alias[] getAliases() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
public static Alias findAlias(AppAlias appAlias, String appName, boolean stripVendor) {
|
||||
if (appName != null) {
|
||||
for (Alias alias : appAlias.aliases) {
|
||||
if (appName.toLowerCase(Locale.ENGLISH).matches(alias.getName(stripVendor).toLowerCase(Locale.ENGLISH))) {
|
||||
return alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class Alias {
|
||||
private String vendor;
|
||||
private String name;
|
||||
private String bundleId;
|
||||
private boolean enterpriseReady;
|
||||
private String posix;
|
||||
|
||||
public Alias(String vendor, String name, String bundleId, boolean enterpriseReady) {
|
||||
this.name = name;
|
||||
this.vendor = vendor;
|
||||
this.bundleId = bundleId;
|
||||
this.enterpriseReady = enterpriseReady;
|
||||
this.posix = getName(true).replaceAll(" ", "").toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
public String getVendor() {
|
||||
return vendor;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove vendor prefix if exists
|
||||
*/
|
||||
public String getName(boolean stripVendor) {
|
||||
if(stripVendor && "Mozilla".equals(vendor) && name.startsWith(vendor)) {
|
||||
return name.substring(vendor.length()).trim();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getBundleId() {
|
||||
return bundleId;
|
||||
}
|
||||
|
||||
public String getPosix() {
|
||||
return posix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the app is known to recognizes enterprise policies, such as GPO
|
||||
*/
|
||||
public boolean isEnterpriseReady() {
|
||||
return enterpriseReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java
Executable file
100
old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java
Executable file
@@ -0,0 +1,100 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
|
||||
|
||||
/**
|
||||
* Container class for installed app information
|
||||
*/
|
||||
public class AppInfo {
|
||||
private AppAlias.Alias alias;
|
||||
private Path path;
|
||||
private Path exePath;
|
||||
private Version version;
|
||||
|
||||
public AppInfo(Alias alias, Path exePath, String version) {
|
||||
this.alias = alias;
|
||||
this.path = exePath.getParent();
|
||||
this.exePath = exePath;
|
||||
this.version = parseVersion(version);
|
||||
}
|
||||
|
||||
public AppInfo(Alias alias, Path path, Path exePath, String version) {
|
||||
this.alias = alias;
|
||||
this.path = path;
|
||||
this.exePath = exePath;
|
||||
this.version = parseVersion(version);
|
||||
}
|
||||
|
||||
public AppInfo(Alias alias, Path exePath) {
|
||||
this.alias = alias;
|
||||
this.path = exePath.getParent();
|
||||
this.exePath = exePath;
|
||||
}
|
||||
|
||||
public Alias getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public String getName(boolean stripVendor) {
|
||||
return alias.getName(stripVendor);
|
||||
}
|
||||
|
||||
public Path getExePath() {
|
||||
return exePath;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(Path path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = parseVersion(version);
|
||||
}
|
||||
|
||||
public void setVersion(Version version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
private static Version parseVersion(String version) {
|
||||
try {
|
||||
// Ensure < 3 octets (e.g. "56.0") doesn't failing
|
||||
while(version.split("\\.").length < 3) {
|
||||
version = version + ".0";
|
||||
}
|
||||
return Version.valueOf(version);
|
||||
} catch(Exception ignore1) {
|
||||
// Catch poor formatting (e.g. "97.0a1"), try to use major version only
|
||||
if(version.split("\\.").length > 0) {
|
||||
try {
|
||||
String[] tryFix = version.split("\\.");
|
||||
return Version.valueOf(tryFix[0] + ".0.0-unknown");
|
||||
} catch(Exception ignore2) {}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof AppInfo && o != null && path != null) {
|
||||
return path.equals(((AppInfo)o).getPath());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return alias + " " + path;
|
||||
}
|
||||
}
|
||||
87
old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java
Executable file
87
old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java
Executable file
@@ -0,0 +1,87 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
public abstract class AppLocator {
|
||||
protected static final Logger log = LogManager.getLogger(AppLocator.class);
|
||||
|
||||
private static AppLocator INSTANCE = getPlatformSpecificAppLocator();
|
||||
|
||||
public abstract ArrayList<AppInfo> locate(AppAlias appAlias);
|
||||
public abstract ArrayList<Path> getPidPaths(ArrayList<String> pids);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public ArrayList<String> getPids(String ... processNames) {
|
||||
return getPids(new ArrayList<>(Arrays.asList(processNames)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Linux, Mac
|
||||
*/
|
||||
public ArrayList<String> getPids(ArrayList<String> processNames) {
|
||||
String[] response;
|
||||
ArrayList<String> pidList = new ArrayList<>();
|
||||
|
||||
if(processNames.contains("firefox") && !(SystemUtilities.isWindows() || SystemUtilities.isMac())) {
|
||||
processNames.add("MainThread"); // Workaround Firefox 79 https://github.com/qzind/tray/issues/701
|
||||
processNames.add("GeckoMain"); // Workaround Firefox 94 https://bugzilla.mozilla.org/show_bug.cgi?id=1742606
|
||||
}
|
||||
|
||||
if (processNames.size() == 0) return pidList;
|
||||
|
||||
// Quoting handled by the command processor (e.g. pgrep -x "myapp|my app" is perfectly valid)
|
||||
String data = ShellUtilities.executeRaw("pgrep", "-x", String.join("|", processNames));
|
||||
|
||||
//Splitting an empty string results in a 1 element array, this is not what we want
|
||||
if (!data.isEmpty()) {
|
||||
response = data.split("\\s*\\r?\\n");
|
||||
Collections.addAll(pidList, response);
|
||||
}
|
||||
|
||||
return pidList;
|
||||
}
|
||||
|
||||
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList) {
|
||||
return getRunningPaths(appList, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the running executables matching on <code>AppInfo.getExePath</code>
|
||||
* This is resource intensive; if a non-null <code>cache</code> is provided, it will return that instead
|
||||
*/
|
||||
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList, ArrayList<Path> cache) {
|
||||
if(cache == null) {
|
||||
ArrayList<String> appNames = new ArrayList<>();
|
||||
for(AppInfo app : appList) {
|
||||
String exeName = app.getExePath().getFileName().toString();
|
||||
if (!appNames.contains(exeName)) appNames.add(exeName);
|
||||
}
|
||||
cache = INSTANCE.getPidPaths(INSTANCE.getPids(appNames));
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
public static AppLocator getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static AppLocator getPlatformSpecificAppLocator() {
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
return new WindowsAppLocator();
|
||||
case MAC:
|
||||
return new MacAppLocator();
|
||||
default:
|
||||
return new LinuxAppLocator();
|
||||
}
|
||||
}
|
||||
}
|
||||
159
old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
Executable file
159
old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
Executable file
@@ -0,0 +1,159 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.UnixUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class LinuxAppLocator extends AppLocator {
|
||||
private static final Logger log = LogManager.getLogger(LinuxAppLocator.class);
|
||||
|
||||
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||
|
||||
// Workaround for calling "firefox --version" as sudo
|
||||
String[] env = appendPaths("HOME=/tmp");
|
||||
|
||||
// Search for matching executable in all path values
|
||||
aliasLoop:
|
||||
for(AppAlias.Alias alias : appAlias.aliases) {
|
||||
// Add non-standard app search locations (e.g. Fedora)
|
||||
for (String dirname : appendPaths(alias.getPosix(), "/usr/lib/$/bin", "/usr/lib64/$/bin", "/usr/lib/$", "/usr/lib64/$")) {
|
||||
Path path = Paths.get(dirname, alias.getPosix());
|
||||
if (Files.isRegularFile(path) && Files.isExecutable(path)) {
|
||||
log.info("Found {} {}: {}, investigating...", alias.getVendor(), alias.getName(true), path);
|
||||
try {
|
||||
File file = path.toFile().getCanonicalFile(); // fix symlinks
|
||||
if(file.getPath().endsWith("/snap")) {
|
||||
// Ubuntu 22.04+ ships Firefox as a snap
|
||||
// Snaps are read-only and are symlinks back to /usr/bin/snap
|
||||
// Reset the executable back to /snap/bin/firefox to get proper version information
|
||||
file = path.toFile();
|
||||
}
|
||||
if(file.getPath().endsWith(".sh")) {
|
||||
// Legacy Ubuntu likes to use .../firefox/firefox.sh, return .../firefox/firefox instead
|
||||
log.info("Found an '.sh' file: {}, removing file extension: {}", file, file = new File(FilenameUtils.removeExtension(file.getPath())));
|
||||
}
|
||||
String contentType = Files.probeContentType(file.toPath());
|
||||
if(contentType == null) {
|
||||
// Fallback to commandline per https://bugs.openjdk.org/browse/JDK-8188228
|
||||
contentType = ShellUtilities.executeRaw("file", "--mime-type", "--brief", file.getPath()).trim();
|
||||
}
|
||||
if(contentType != null && contentType.endsWith("/x-shellscript")) {
|
||||
if(UnixUtilities.isFedora()) {
|
||||
// Firefox's script is full of variables and not parsable, fallback to /usr/lib64/$, etc
|
||||
log.info("Found shell script at {}, but we're on Fedora, so we'll look in some known locations instead.", file.getPath());
|
||||
continue;
|
||||
}
|
||||
// Debian and Arch like to place a stub script directly in /usr/bin/
|
||||
// TODO: Split into a function; possibly recurse on search paths
|
||||
log.info("{} bin was expected but script found... Reading...", appAlias.name());
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
if(line.startsWith("exec") && line.contains(alias.getPosix())) {
|
||||
String[] parts = line.split(" ");
|
||||
// Get the app name after "exec"
|
||||
if (parts.length > 1) {
|
||||
log.info("Found a familiar line '{}', using '{}'", line, parts[1]);
|
||||
Path p = Paths.get(parts[1]);
|
||||
String exec = parts[1];
|
||||
// Handle edge-case for esr release
|
||||
if(!p.isAbsolute()) {
|
||||
// Script doesn't contain the full path, go deeper
|
||||
exec = Paths.get(dirname, exec).toFile().getCanonicalPath();
|
||||
log.info("Calculated full bin path {}", exec);
|
||||
}
|
||||
// Make sure it actually exists
|
||||
if(!(file = new File(exec)).exists()) {
|
||||
log.warn("Sorry, we couldn't detect the real path of {}. Skipping...", appAlias.name());
|
||||
continue aliasLoop;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.close();
|
||||
} else {
|
||||
log.info("Assuming {} {} is installed: {}", alias.getVendor(), alias.getName(true), file);
|
||||
}
|
||||
AppInfo appInfo = new AppInfo(alias, file.toPath());
|
||||
if(file.getPath().startsWith("/snap/")) {
|
||||
// Ubuntu 22.04+ uses snaps, fallback to a sane "path" value
|
||||
String snapPath = file.getPath(); // e.g. /snap/bin/firefox
|
||||
snapPath = snapPath.replaceFirst("/bin/", "/");
|
||||
snapPath += "/current";
|
||||
appInfo.setPath(Paths.get(snapPath));
|
||||
}
|
||||
|
||||
appList.add(appInfo);
|
||||
|
||||
// Call "--version" on executable to obtain version information
|
||||
Process p = Runtime.getRuntime().exec(new String[] {file.getPath(), "--version" }, env);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String version = reader.readLine();
|
||||
reader.close();
|
||||
if (version != null) {
|
||||
log.info("We obtained version info: {}, but we'll need to parse it", version);
|
||||
if(version.contains(" ")) {
|
||||
String[] split = version.split(" ");
|
||||
String parsed = split[split.length - 1];
|
||||
String stripped = parsed.replaceAll("[^\\d.]", "");
|
||||
appInfo.setVersion(stripped);
|
||||
if(!parsed.equals(stripped)) {
|
||||
// Add the meta data back (e.g. "esr")
|
||||
appInfo.getVersion().setBuildMetadata(parsed.replaceAll("[\\d.]", ""));
|
||||
}
|
||||
} else {
|
||||
appInfo.setVersion(version.trim());
|
||||
}
|
||||
}
|
||||
break;
|
||||
} catch(Exception e) {
|
||||
log.warn("Something went wrong getting app info for {} {}", alias.getVendor(), alias.getName(true), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||
ArrayList<Path> pathList = new ArrayList<>();
|
||||
|
||||
for(String pid : pids) {
|
||||
try {
|
||||
pathList.add(Paths.get("/proc/", pid, !SystemUtilities.isSolaris() ? "/exe" : "/path/a.out").toRealPath());
|
||||
} catch(IOException e) {
|
||||
log.warn("Process {} vanished", pid);
|
||||
}
|
||||
}
|
||||
|
||||
return pathList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a PATH value with provided paths appended, replacing "$" with POSIX app name
|
||||
* Useful for strange Firefox install locations (e.g. Fedora)
|
||||
*
|
||||
* Usage: appendPaths("firefox", "/usr/lib64");
|
||||
*
|
||||
*/
|
||||
private static String[] appendPaths(String posix, String ... prefixes) {
|
||||
String newPath = System.getenv("PATH");
|
||||
for (String prefix : prefixes) {
|
||||
newPath = newPath + File.pathSeparator + prefix.replaceAll("\\$", posix);
|
||||
}
|
||||
return newPath.split(File.pathSeparator);
|
||||
}
|
||||
}
|
||||
168
old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
Executable file
168
old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
Executable file
@@ -0,0 +1,168 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
import qz.utils.ShellUtilities;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class MacAppLocator extends AppLocator{
|
||||
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
|
||||
|
||||
private static String[] BLACKLISTED_PATHS = new String[]{"/Volumes/", "/.Trash/", "/Applications (Parallels)/" };
|
||||
|
||||
/**
|
||||
* Helper class for finding key/value siblings from the DDM
|
||||
*/
|
||||
private enum SiblingNode {
|
||||
NAME("_name"),
|
||||
PATH("path"),
|
||||
VERSION("version");
|
||||
|
||||
private String key;
|
||||
private boolean wants;
|
||||
|
||||
SiblingNode(String key) {
|
||||
this.key = key;
|
||||
this.wants = false;
|
||||
}
|
||||
|
||||
private boolean isKey(Node node) {
|
||||
if (node.getNodeName().equals("key") && node.getTextContent().equals(key)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||
Document doc;
|
||||
|
||||
try {
|
||||
// system_profile benchmarks about 30% better than lsregister
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"system_profiler", "SPApplicationsDataType", "-xml"}, ShellUtilities.envp);
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
// don't let the <!DOCTYPE> fail parsing per https://github.com/qzind/tray/issues/809
|
||||
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
doc = dbf.newDocumentBuilder().parse(p.getInputStream());
|
||||
} catch(IOException | ParserConfigurationException | SAXException e) {
|
||||
log.warn("Could not retrieve app listing for {}", appAlias.name(), e);
|
||||
return appList;
|
||||
}
|
||||
doc.normalizeDocument();
|
||||
|
||||
NodeList nodeList = doc.getElementsByTagName("dict");
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
NodeList dict = nodeList.item(i).getChildNodes();
|
||||
HashMap<SiblingNode, String> foundApp = new HashMap<>();
|
||||
for (int j = 0; j < dict.getLength(); j++) {
|
||||
Node node = dict.item(j);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
for (SiblingNode sibling : SiblingNode.values()) {
|
||||
if (sibling.wants) {
|
||||
foundApp.put(sibling, node.getTextContent());
|
||||
sibling.wants = false;
|
||||
break;
|
||||
} else if(sibling.isKey(node)) {
|
||||
sibling.wants = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppAlias.Alias alias;
|
||||
if((alias = AppAlias.findAlias(appAlias, foundApp.get(SiblingNode.NAME), true)) != null) {
|
||||
appList.add(new AppInfo(alias, Paths.get(foundApp.get(SiblingNode.PATH)),
|
||||
getExePath(foundApp.get(SiblingNode.PATH)), foundApp.get(SiblingNode.VERSION)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove blacklisted paths
|
||||
Iterator<AppInfo> appInfoIterator = appList.iterator();
|
||||
while(appInfoIterator.hasNext()) {
|
||||
AppInfo appInfo = appInfoIterator.next();
|
||||
for(String listEntry : BLACKLISTED_PATHS) {
|
||||
if (appInfo.getPath() != null && appInfo.getPath().toString().contains(listEntry)) {
|
||||
appInfoIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return appList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||
ArrayList<Path> processPaths = new ArrayList();
|
||||
for (String pid : pids) {
|
||||
Pointer buf = new Memory(SystemB.PROC_PIDPATHINFO_MAXSIZE);
|
||||
SystemB.INSTANCE.proc_pidpath(Integer.parseInt(pid), buf, SystemB.PROC_PIDPATHINFO_MAXSIZE);
|
||||
processPaths.add(Paths.get(buf.getString(0).trim()));
|
||||
}
|
||||
return processPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate executable path by parsing Contents/Info.plist
|
||||
*/
|
||||
private static Path getExePath(String appPath) {
|
||||
Path path = Paths.get(appPath).toAbsolutePath().normalize();
|
||||
Path plist = path.resolve("Contents/Info.plist");
|
||||
Document doc;
|
||||
try {
|
||||
if(!plist.toFile().exists()) {
|
||||
log.warn("Could not locate plist file for {}: {}", appPath, plist);
|
||||
return null;
|
||||
}
|
||||
// Convert potentially binary plist files to XML
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toString(), "-o", "-"}, ShellUtilities.envp);
|
||||
doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream());
|
||||
} catch(IOException | ParserConfigurationException | SAXException e) {
|
||||
log.warn("Could not parse plist file for {}: {}", appPath, appPath, e);
|
||||
return null;
|
||||
}
|
||||
doc.normalizeDocument();
|
||||
|
||||
boolean upNext = false;
|
||||
NodeList nodeList = doc.getElementsByTagName("dict");
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
NodeList dict = nodeList.item(i).getChildNodes();
|
||||
for(int j = 0; j < dict.getLength(); j++) {
|
||||
Node node = dict.item(j);
|
||||
if ("key".equals(node.getNodeName()) && node.getTextContent().equals("CFBundleExecutable")) {
|
||||
upNext = true;
|
||||
} else if (upNext && "string".equals(node.getNodeName())) {
|
||||
return path.resolve("Contents/MacOS/" + node.getTextContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private interface SystemB extends Library {
|
||||
SystemB INSTANCE = Native.load("System", SystemB.class);
|
||||
int PROC_ALL_PIDS = 1;
|
||||
int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
|
||||
int sysctlbyname(String name, Pointer oldp, IntByReference oldlenp, Pointer newp, int newlen);
|
||||
int proc_listpids(int type, int typeinfo, int[] buffer, int buffersize);
|
||||
int proc_pidpath(int pid, Pointer buffer, int buffersize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.Psapi;
|
||||
import com.sun.jna.platform.win32.Tlhelp32;
|
||||
import com.sun.jna.platform.win32.WinNT;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE;
|
||||
|
||||
public class WindowsAppLocator extends AppLocator{
|
||||
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
|
||||
|
||||
private static String REG_TEMPLATE = "Software\\%s%s\\%s%s";
|
||||
|
||||
@Override
|
||||
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||
for (Alias alias : appAlias.aliases) {
|
||||
if (alias.getVendor() != null) {
|
||||
String[] suffixes = new String[]{ "", " ESR"};
|
||||
String[] prefixes = new String[]{ "", "WOW6432Node\\"};
|
||||
for (String suffix : suffixes) {
|
||||
for (String prefix : prefixes) {
|
||||
String key = String.format(REG_TEMPLATE, prefix, alias.getVendor(), alias.getName(), suffix);
|
||||
AppInfo appInfo = getAppInfo(alias, key, suffix);
|
||||
if (appInfo != null && !appList.contains(appInfo)) {
|
||||
appList.add(appInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return appList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<String> getPids(ArrayList<String> processNames) {
|
||||
ArrayList<String> pidList = new ArrayList<>();
|
||||
|
||||
if (processNames.isEmpty()) return pidList;
|
||||
|
||||
Tlhelp32.PROCESSENTRY32 pe32 = new Tlhelp32.PROCESSENTRY32();
|
||||
pe32.dwSize = new WinNT.DWORD(pe32.size());
|
||||
|
||||
// Fetch a snapshot of all processes
|
||||
WinNT.HANDLE hSnapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinNT.DWORD(0));
|
||||
if (hSnapshot.equals(WinNT.INVALID_HANDLE_VALUE)) {
|
||||
log.warn("Process snapshot has invalid handle");
|
||||
return pidList;
|
||||
}
|
||||
|
||||
if (Kernel32.INSTANCE.Process32First(hSnapshot, pe32)) {
|
||||
do {
|
||||
String processName = Native.toString(pe32.szExeFile);
|
||||
if(processNames.contains(processName.toLowerCase(Locale.ENGLISH))) {
|
||||
pidList.add(pe32.th32ProcessID.toString());
|
||||
}
|
||||
} while (Kernel32.INSTANCE.Process32Next(hSnapshot, pe32));
|
||||
}
|
||||
|
||||
Kernel32.INSTANCE.CloseHandle(hSnapshot);
|
||||
return pidList;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||
ArrayList<Path> pathList = new ArrayList<>();
|
||||
|
||||
for(String pid : pids) {
|
||||
WinNT.HANDLE hProcess = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION | WinNT.PROCESS_VM_READ, false, Integer.parseInt(pid));
|
||||
if (hProcess == null) {
|
||||
log.warn("Handle for PID {} is missing, skipping.", pid);
|
||||
continue;
|
||||
}
|
||||
|
||||
int bufferSize = WinNT.MAX_PATH;
|
||||
Pointer buffer = new Memory(bufferSize * Native.WCHAR_SIZE);
|
||||
|
||||
if (Psapi.INSTANCE.GetModuleFileNameEx(hProcess, null, buffer, bufferSize) == 0) {
|
||||
log.warn("Full path to PID {} is empty, skipping.", pid);
|
||||
Kernel32.INSTANCE.CloseHandle(hProcess);
|
||||
continue;
|
||||
}
|
||||
|
||||
Kernel32.INSTANCE.CloseHandle(hProcess);
|
||||
pathList.add(Paths.get(Native.WCHAR_SIZE == 1 ?
|
||||
buffer.getString(0) :
|
||||
buffer.getWideString(0)));
|
||||
}
|
||||
return pathList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a proprietary Firefox-only technique for getting "PathToExe" registry value
|
||||
*/
|
||||
private static AppInfo getAppInfo(Alias alias, String key, String suffix) {
|
||||
String version = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "CurrentVersion");
|
||||
if (version != null) {
|
||||
version = version.split(" ")[0]; // chop off (x86 ...)
|
||||
if (!suffix.isEmpty()) {
|
||||
if (key.endsWith(suffix)) {
|
||||
key = key.substring(0, key.length() - suffix.length());
|
||||
}
|
||||
version = version + suffix;
|
||||
}
|
||||
String exePath = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe");
|
||||
|
||||
if (exePath != null) {
|
||||
// SemVer: Replace spaces in suffixes with dashes
|
||||
version = version.replaceAll(" ", "-");
|
||||
return new AppInfo(alias, Paths.get(exePath), version);
|
||||
} else {
|
||||
log.warn("Couldn't locate \"PathToExe\" for \"{}\" in \"{}\", skipping", alias.getName(), key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
161
old code/tray/src/qz/installer/provision/ProvisionInstaller.java
Executable file
161
old code/tray/src/qz/installer/provision/ProvisionInstaller.java
Executable file
@@ -0,0 +1,161 @@
|
||||
package qz.installer.provision;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.Phase;
|
||||
import qz.build.provision.params.types.Script;
|
||||
import qz.build.provision.params.types.Software;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.provision.invoker.*;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
import static qz.utils.FileUtilities.*;
|
||||
|
||||
public class ProvisionInstaller {
|
||||
protected static final Logger log = LogManager.getLogger(ProvisionInstaller.class);
|
||||
private ArrayList<Step> steps;
|
||||
|
||||
static {
|
||||
// Populate variables for scripting environment
|
||||
ShellUtilities.addEnvp("APP_TITLE", ABOUT_TITLE,
|
||||
"APP_VERSION", VERSION,
|
||||
"APP_ABBREV", PROPS_FILE,
|
||||
"APP_VENDOR", ABOUT_COMPANY,
|
||||
"APP_VENDOR_ABBREV", DATA_DIR,
|
||||
"APP_ARCH", SystemUtilities.getArch(),
|
||||
"APP_OS", SystemUtilities.getOs(),
|
||||
"APP_DIR", SystemUtilities.getAppPath(),
|
||||
"APP_USER_DIR", USER_DIR,
|
||||
"APP_SHARED_DIR", SHARED_DIR);
|
||||
}
|
||||
|
||||
public ProvisionInstaller(Path relativePath) throws IOException, JSONException {
|
||||
this(relativePath, relativePath.resolve(Constants.PROVISION_FILE).toFile());
|
||||
}
|
||||
|
||||
public ProvisionInstaller(Path relativePath, File jsonFile) throws IOException, JSONException {
|
||||
if(!jsonFile.exists()) {
|
||||
log.info("Provision file not found '{}', skipping", jsonFile);
|
||||
this.steps = new ArrayList<>();
|
||||
return;
|
||||
}
|
||||
this.steps = parse(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8), relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Package private for internal testing only
|
||||
* Assumes files located in ./resources/ subdirectory
|
||||
*/
|
||||
ProvisionInstaller(Class relativeClass, InputStream in) throws IOException, JSONException {
|
||||
this(relativeClass, IOUtils.toString(in, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Package private for internal testing only
|
||||
* Assumes files located in ./resources/ subdirectory
|
||||
*/
|
||||
ProvisionInstaller(Class relativeClass, String jsonData) throws JSONException {
|
||||
this.steps = parse(jsonData, relativeClass);
|
||||
}
|
||||
|
||||
public void invoke(Phase phase) {
|
||||
for(Step step : this.steps) {
|
||||
if(phase == null || step.getPhase() == phase) {
|
||||
try {
|
||||
invokeStep(step);
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("[PROVISION] Provisioning step failed '{}'", step, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void invoke() {
|
||||
invoke(null);
|
||||
}
|
||||
|
||||
private static ArrayList<Step> parse(String jsonData, Object relativeObject) throws JSONException {
|
||||
return parse(new JSONArray(jsonData), relativeObject);
|
||||
}
|
||||
|
||||
private boolean invokeStep(Step step) throws Exception {
|
||||
if(Os.matchesHost(step.getOs())) {
|
||||
log.info("[PROVISION] Invoking step '{}'", step.toString());
|
||||
} else {
|
||||
log.info("[PROVISION] Skipping step for different OS '{}'", step.toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
Invokable invoker;
|
||||
switch(step.getType()) {
|
||||
case CA:
|
||||
invoker = new CaInvoker(step, PropertyInvoker.getProperties(step));
|
||||
break;
|
||||
case CERT:
|
||||
invoker = new CertInvoker(step);
|
||||
break;
|
||||
case CONF:
|
||||
invoker = new ConfInvoker(step);
|
||||
break;
|
||||
case SCRIPT:
|
||||
invoker = new ScriptInvoker(step);
|
||||
break;
|
||||
case SOFTWARE:
|
||||
invoker = new SoftwareInvoker(step);
|
||||
break;
|
||||
case REMOVER:
|
||||
invoker = new RemoverInvoker(step);
|
||||
break;
|
||||
case RESOURCE:
|
||||
invoker = new ResourceInvoker(step);
|
||||
break;
|
||||
case PREFERENCE:
|
||||
invoker = new PropertyInvoker(step, PropertyInvoker.getPreferences(step));
|
||||
break;
|
||||
case PROPERTY:
|
||||
invoker = new PropertyInvoker(step, PropertyInvoker.getProperties(step));
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Type " + step.getType() + " is not yet supported.");
|
||||
}
|
||||
return invoker.invoke();
|
||||
}
|
||||
|
||||
public ArrayList<Step> getSteps() {
|
||||
return steps;
|
||||
}
|
||||
|
||||
private static ArrayList<Step> parse(JSONArray jsonArray, Object relativeObject) throws JSONException {
|
||||
ArrayList<Step> steps = new ArrayList<>();
|
||||
for(int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject jsonStep = jsonArray.getJSONObject(i);
|
||||
try {
|
||||
steps.add(Step.parse(jsonStep, relativeObject));
|
||||
} catch(Exception e) {
|
||||
log.warn("[PROVISION] Unable to add step '{}'", jsonStep, e);
|
||||
}
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
public static boolean shouldBeExecutable(Path path) {
|
||||
return Script.parse(path) != null || Software.parse(path) != Software.UNKNOWN;
|
||||
}
|
||||
}
|
||||
49
old code/tray/src/qz/installer/provision/invoker/CaInvoker.java
Executable file
49
old code/tray/src/qz/installer/provision/invoker/CaInvoker.java
Executable file
@@ -0,0 +1,49 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.common.PropertyHelper;
|
||||
import qz.utils.ArgValue;
|
||||
import qz.utils.FileUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Combines ResourceInvoker and PropertyInvoker to deploy a file and set a property to its deployed path
|
||||
*/
|
||||
public class CaInvoker extends InvokableResource {
|
||||
Step step;
|
||||
PropertyHelper properties;
|
||||
|
||||
public CaInvoker(Step step, PropertyHelper properties) {
|
||||
this.step = step;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws IOException {
|
||||
// First, write our cert file
|
||||
File caCert = dataToFile(step);
|
||||
if(caCert == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Next, handle our property step
|
||||
Step propsStep = step.clone();
|
||||
|
||||
// If the property already exists, snag it
|
||||
String key = ArgValue.AUTHCERT_OVERRIDE.getMatch();
|
||||
String value = caCert.getPath();
|
||||
if (properties.containsKey(key)) {
|
||||
value = properties.getProperty(key) + FileUtilities.FILE_SEPARATOR + value;
|
||||
}
|
||||
|
||||
propsStep.setData(String.format("%s=%s", key, value));
|
||||
|
||||
if (new PropertyInvoker(propsStep, properties).invoke()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
26
old code/tray/src/qz/installer/provision/invoker/CertInvoker.java
Executable file
26
old code/tray/src/qz/installer/provision/invoker/CertInvoker.java
Executable file
@@ -0,0 +1,26 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.FileUtilities;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static qz.utils.ArgParser.ExitStatus.*;
|
||||
|
||||
public class CertInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
|
||||
public CertInvoker(Step step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
File cert = dataToFile(step);
|
||||
if(cert == null) {
|
||||
return false;
|
||||
}
|
||||
return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS;
|
||||
}
|
||||
}
|
||||
46
old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java
Executable file
46
old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java
Executable file
@@ -0,0 +1,46 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.common.PropertyHelper;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
|
||||
public class ConfInvoker extends PropertyInvoker {
|
||||
public ConfInvoker(Step step) {
|
||||
super(step, new PropertyHelper(calculateConfPath(step)));
|
||||
}
|
||||
|
||||
public static String calculateConfPath(Step step) {
|
||||
String relativePath = step.getArgs().get(0);
|
||||
if(SystemUtilities.isMac()) {
|
||||
return SystemUtilities.getJarParentPath().
|
||||
resolve("../PlugIns/Java.runtime/Contents/Home/conf").
|
||||
resolve(relativePath).
|
||||
normalize()
|
||||
.toString();
|
||||
} else {
|
||||
return SystemUtilities.getJarParentPath()
|
||||
.resolve("runtime/conf")
|
||||
.resolve(relativePath)
|
||||
.normalize()
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() {
|
||||
Step step = getStep();
|
||||
// Java uses the same "|" delimiter as we do, only parse one property at a time
|
||||
AbstractMap.SimpleEntry<String, String> pair = parsePropertyPair(step, step.getData());
|
||||
if (!pair.getValue().isEmpty()) {
|
||||
properties.setProperty(pair);
|
||||
if (properties.save()) {
|
||||
log.info("Successfully provisioned '1' '{}'", step.getType());
|
||||
return true;
|
||||
}
|
||||
log.error("An error occurred saving properties '{}' to file", step.getData());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
10
old code/tray/src/qz/installer/provision/invoker/Invokable.java
Executable file
10
old code/tray/src/qz/installer/provision/invoker/Invokable.java
Executable file
@@ -0,0 +1,10 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public interface Invokable {
|
||||
Logger log = LogManager.getLogger(Invokable.class);
|
||||
|
||||
boolean invoke() throws Exception;
|
||||
}
|
||||
63
old code/tray/src/qz/installer/provision/invoker/InvokableResource.java
Executable file
63
old code/tray/src/qz/installer/provision/invoker/InvokableResource.java
Executable file
@@ -0,0 +1,63 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Type;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
public abstract class InvokableResource implements Invokable {
|
||||
static final Logger log = LogManager.getLogger(InvokableResource.class);
|
||||
|
||||
public static File dataToFile(Step step) throws IOException {
|
||||
Path resourcePath = Paths.get(step.getData());
|
||||
if(resourcePath.isAbsolute() || step.usingPath()) {
|
||||
return pathResourceToFile(step);
|
||||
}
|
||||
if(step.usingClass()) {
|
||||
return classResourceToFile(step);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the resource directly from file
|
||||
*/
|
||||
private static File pathResourceToFile(Step step) {
|
||||
String resourcePath = step.getData();
|
||||
Path dataPath = Paths.get(resourcePath);
|
||||
return dataPath.isAbsolute() ? dataPath.toFile() : step.getRelativePath().resolve(resourcePath).toFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies resource from JAR to a temp file for use in installation
|
||||
*/
|
||||
private static File classResourceToFile(Step step) throws IOException {
|
||||
// Resource may be inside the jar
|
||||
InputStream in = step.getRelativeClass().getResourceAsStream("resources/" + step.getData());
|
||||
if(in == null) {
|
||||
log.warn("Resource '{}' is missing, skipping step", step.getData());
|
||||
return null;
|
||||
}
|
||||
String suffix = "_" + Paths.get(step.getData()).getFileName().toString();
|
||||
File destination = File.createTempFile(Constants.DATA_DIR + "_provision_", suffix);
|
||||
Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
IOUtils.closeQuietly(in);
|
||||
|
||||
// Set scripts executable
|
||||
if(step.getType() == Type.SCRIPT && !SystemUtilities.isWindows()) {
|
||||
destination.setExecutable(true, false);
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
}
|
||||
99
old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java
Executable file
99
old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java
Executable file
@@ -0,0 +1,99 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.common.Constants;
|
||||
import qz.common.PropertyHelper;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PropertyInvoker implements Invokable {
|
||||
private Step step;
|
||||
PropertyHelper properties;
|
||||
|
||||
public PropertyInvoker(Step step, PropertyHelper properties) {
|
||||
this.step = step;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public boolean invoke() {
|
||||
HashMap<String, String> pairs = parsePropertyPairs(step);
|
||||
if (!pairs.isEmpty()) {
|
||||
for(Map.Entry<String, String> pair : pairs.entrySet()) {
|
||||
properties.setProperty(pair);
|
||||
}
|
||||
if (properties.save()) {
|
||||
log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType());
|
||||
return true;
|
||||
}
|
||||
log.error("An error occurred saving properties '{}' to file", step.getData());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static PropertyHelper getProperties(Step step) {
|
||||
File propertiesFile;
|
||||
if(step.getRelativePath() != null) {
|
||||
// Assume qz-tray.properties is one directory up from provision folder
|
||||
// required to prevent installing to payload
|
||||
propertiesFile = step.getRelativePath().getParent().resolve(Constants.PROPS_FILE + ".properties").toFile();
|
||||
} else {
|
||||
// If relative path isn't set, fallback to the jar's parent path
|
||||
propertiesFile = SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile();
|
||||
}
|
||||
log.info("Provisioning '{}' to properties file: '{}'", step.getData(), propertiesFile);
|
||||
return new PropertyHelper(propertiesFile);
|
||||
}
|
||||
|
||||
public static PropertyHelper getPreferences(Step step) {
|
||||
return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
|
||||
}
|
||||
|
||||
public static HashMap<String, String> parsePropertyPairs(Step step) {
|
||||
HashMap<String, String> pairs = new HashMap<>();
|
||||
if(step.getData() != null && !step.getData().trim().isEmpty()) {
|
||||
String[] props = step.getData().split("\\|");
|
||||
for(String prop : props) {
|
||||
AbstractMap.SimpleEntry<String,String> pair = parsePropertyPair(step, prop);
|
||||
if (pair != null) {
|
||||
if(pairs.get(pair.getKey()) != null) {
|
||||
log.warn("Property {} already exists, replacing [before: {}, after: {}] ",
|
||||
pair.getKey(), pairs.get(pair.getKey()), pair.getValue());
|
||||
}
|
||||
pairs.put(pair.getKey(), pair.getValue());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error("Skipping Step '{}', Data is null or empty", step.getType());
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
|
||||
public static AbstractMap.SimpleEntry<String, String> parsePropertyPair(Step step, String prop) {
|
||||
if(prop.contains("=")) {
|
||||
String[] pair = prop.split("=", 2);
|
||||
if (!pair[0].trim().isEmpty()) {
|
||||
if (!pair[1].trim().isEmpty()) {
|
||||
return new AbstractMap.SimpleEntry<>(pair[0], pair[1]);
|
||||
} else {
|
||||
log.warn("Skipping '{}' '{}', property value is malformed", step.getType(), prop);
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping '{}' '{}', property name is malformed", step.getType(), prop);
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping '{}' '{}', property is malformed", step.getType(), prop);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Step getStep() {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
100
old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java
Executable file
100
old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java
Executable file
@@ -0,0 +1,100 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.types.Remover;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class RemoverInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
private String aboutTitle; // e.g. "QZ Tray"
|
||||
private String propsFile; // e.g. "qz-tray"
|
||||
private String dataDir; // e.g. "qz"
|
||||
|
||||
|
||||
public RemoverInvoker(Step step) {
|
||||
this.step = step;
|
||||
Remover remover = Remover.parse(step.getData());
|
||||
if(remover == Remover.CUSTOM) {
|
||||
// Fields are comma delimited in the data field
|
||||
parseCustomFromData(step.getData());
|
||||
} else {
|
||||
aboutTitle = remover.getAboutTitle();
|
||||
propsFile = remover.getPropsFile();
|
||||
dataDir = remover.getDataDir();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
ArrayList<String> command = getRemoveCommand();
|
||||
if(command.size() == 0) {
|
||||
log.info("An existing installation of '{}' was not found. Skipping.", aboutTitle);
|
||||
return true;
|
||||
}
|
||||
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
|
||||
if(!success) {
|
||||
log.error("An error occurred invoking [{}]", step.getData());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public void parseCustomFromData(String data) {
|
||||
String[] parts = data.split(",");
|
||||
aboutTitle = parts[0].trim();
|
||||
propsFile = parts[1].trim();
|
||||
dataDir = parts[2].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the installer command (including the installer itself and if needed, arguments) to
|
||||
* invoke the installer file
|
||||
*/
|
||||
public ArrayList<String> getRemoveCommand() {
|
||||
ArrayList<String> removeCmd = new ArrayList<>();
|
||||
Os os = SystemUtilities.getOs();
|
||||
switch(os) {
|
||||
case WINDOWS:
|
||||
Path win = Paths.get(System.getenv("PROGRAMFILES"))
|
||||
.resolve(aboutTitle)
|
||||
.resolve("uninstall.exe");
|
||||
|
||||
if(win.toFile().exists()) {
|
||||
removeCmd.add(win.toString());
|
||||
removeCmd.add("/S");
|
||||
break;
|
||||
}
|
||||
case MAC:
|
||||
Path legacy = Paths.get("/Applications")
|
||||
.resolve(aboutTitle + ".app")
|
||||
.resolve("Contents")
|
||||
.resolve("uninstall");
|
||||
|
||||
Path mac = Paths.get("/Applications")
|
||||
.resolve(aboutTitle + ".app")
|
||||
.resolve("Contents")
|
||||
.resolve("Resources")
|
||||
.resolve("uninstall");
|
||||
|
||||
if(legacy.toFile().exists()) {
|
||||
removeCmd.add(legacy.toString());
|
||||
} else if(mac.toFile().exists()) {
|
||||
removeCmd.add(mac.toString());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Path linux = Paths.get("/opt")
|
||||
.resolve(propsFile)
|
||||
.resolve("uninstall");
|
||||
if(linux.toFile().exists()) {
|
||||
removeCmd.add(linux.toString());
|
||||
}
|
||||
}
|
||||
return removeCmd;
|
||||
}
|
||||
}
|
||||
19
old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java
Executable file
19
old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java
Executable file
@@ -0,0 +1,19 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
|
||||
/**
|
||||
* Stub class for deploying an otherwise "action-less" resource, only to be used by other tasks
|
||||
*/
|
||||
public class ResourceInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
|
||||
public ResourceInvoker(Step step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
return dataToFile(step) != null;
|
||||
}
|
||||
}
|
||||
77
old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java
Executable file
77
old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java
Executable file
@@ -0,0 +1,77 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.types.Script;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ScriptInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
|
||||
public ScriptInvoker(Step step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
File script = dataToFile(step);
|
||||
if(script == null) {
|
||||
return false;
|
||||
}
|
||||
Script engine = Script.parse(step.getData());
|
||||
ArrayList<String> command = getInterpreter(engine);
|
||||
if(command.isEmpty() && SystemUtilities.isWindows()) {
|
||||
log.warn("No interpreter found for {}, skipping", step.getData());
|
||||
return false;
|
||||
}
|
||||
command.add(script.toString());
|
||||
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
|
||||
if(!success) {
|
||||
log.error("An error occurred invoking [{}]", step.getData());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the interpreter command (and if needed, arguments) to invoke the script file
|
||||
*
|
||||
* An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3
|
||||
* which will allow the OS to select the correct interpreter for the given file
|
||||
*
|
||||
* No special attention is given to "shebang", behavior may differ between OSs
|
||||
*/
|
||||
private static ArrayList<String> getInterpreter(Script engine) {
|
||||
ArrayList<String> interpreter = new ArrayList<>();
|
||||
Os osType = SystemUtilities.getOs();
|
||||
switch(engine) {
|
||||
case PS1:
|
||||
if(osType == Os.WINDOWS) {
|
||||
interpreter.add("powershell.exe");
|
||||
} else if(osType == Os.MAC) {
|
||||
interpreter.add("/usr/local/bin/pwsh");
|
||||
} else {
|
||||
interpreter.add("pwsh");
|
||||
}
|
||||
interpreter.add("-File");
|
||||
break;
|
||||
case PY:
|
||||
interpreter.add(osType == Os.WINDOWS ? "python3.exe" : "python3");
|
||||
break;
|
||||
case BAT:
|
||||
interpreter.add(osType == Os.WINDOWS ? "cmd.exe" : "wineconsole");
|
||||
break;
|
||||
case RB:
|
||||
interpreter.add(osType == Os.WINDOWS ? "ruby.exe" : "ruby");
|
||||
break;
|
||||
case SH:
|
||||
default:
|
||||
// Allow the environment to parse it from the shebang at invocation time
|
||||
}
|
||||
return interpreter;
|
||||
}
|
||||
}
|
||||
87
old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java
Executable file
87
old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java
Executable file
@@ -0,0 +1,87 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.types.Software;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SoftwareInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
|
||||
public SoftwareInvoker(Step step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
File payload = dataToFile(step);
|
||||
if(payload == null) {
|
||||
return false;
|
||||
}
|
||||
Software installer = Software.parse(step.getData());
|
||||
ArrayList<String> command = getInstallCommand(installer, step.getArgs(), payload);
|
||||
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]), payload.getParentFile());
|
||||
if(!success) {
|
||||
log.error("An error occurred invoking [{}]", step.getData());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the installer command (including the installer itself and if needed, arguments) to
|
||||
* invoke the installer file
|
||||
*/
|
||||
public ArrayList<String> getInstallCommand(Software installer, List<String> args, File payload) {
|
||||
ArrayList<String> interpreter = new ArrayList<>();
|
||||
Os os = SystemUtilities.getOs();
|
||||
switch(installer) {
|
||||
case EXE:
|
||||
if(!SystemUtilities.isWindows()) {
|
||||
interpreter.add("wine");
|
||||
}
|
||||
// Executable on its own
|
||||
interpreter.add(payload.toString());
|
||||
interpreter.addAll(args); // Assume exe args come after payload
|
||||
break;
|
||||
case MSI:
|
||||
interpreter.add(os == Os.WINDOWS ? "msiexec.exe" : "msiexec");
|
||||
interpreter.add("/i"); // Assume standard install
|
||||
interpreter.add(payload.toString());
|
||||
interpreter.addAll(args); // Assume msiexec args come after payload
|
||||
break;
|
||||
case PKG:
|
||||
if(os == Os.MAC) {
|
||||
interpreter.add("installer");
|
||||
interpreter.addAll(args); // Assume installer args come before payload
|
||||
interpreter.add("-package");
|
||||
interpreter.add(payload.toString());
|
||||
interpreter.add("-target");
|
||||
interpreter.add("/"); // Assume we don't want this on a removable volume
|
||||
} else {
|
||||
throw new UnsupportedOperationException("PKG is not yet supported on this platform");
|
||||
}
|
||||
break;
|
||||
case DMG:
|
||||
// DMG requires "hdiutil attach", but the mount point is unknown
|
||||
throw new UnsupportedOperationException("DMG is not yet supported");
|
||||
case RUN:
|
||||
if(SystemUtilities.isWindows()) {
|
||||
interpreter.add("bash");
|
||||
interpreter.add("-c");
|
||||
}
|
||||
interpreter.add(payload.toString());
|
||||
interpreter.addAll(args); // Assume run args come after payload
|
||||
// Executable on its own
|
||||
break;
|
||||
default:
|
||||
// We'll try to parse it from the shebang just before invocation time
|
||||
}
|
||||
return interpreter;
|
||||
}
|
||||
|
||||
}
|
||||
44
old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java
Executable file
44
old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java
Executable file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.shortcut;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.installer.LinuxInstaller;
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
class LinuxShortcutCreator extends ShortcutCreator {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(LinuxShortcutCreator.class);
|
||||
private static String DESKTOP = System.getProperty("user.home") + "/Desktop/";
|
||||
|
||||
public boolean canAutoStart() {
|
||||
return Files.exists(Paths.get(LinuxInstaller.STARTUP_DIR, LinuxInstaller.SHORTCUT_NAME));
|
||||
}
|
||||
public void createDesktopShortcut() {
|
||||
copyShortcut(LinuxInstaller.APP_LAUNCHER, DESKTOP);
|
||||
}
|
||||
|
||||
private static void copyShortcut(String source, String target) {
|
||||
try {
|
||||
Files.copy(Paths.get(source), Paths.get(target));
|
||||
} catch(IOException e) {
|
||||
log.warn("Error creating shortcut {}", target, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java
Executable file
100
old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java
Executable file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package qz.installer.shortcut;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.MacUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
class MacShortcutCreator extends ShortcutCreator {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(MacShortcutCreator.class);
|
||||
private static String SHORTCUT_PATH = System.getProperty("user.home") + "/Desktop/" + Constants.ABOUT_TITLE;
|
||||
|
||||
/**
|
||||
* Verify LaunchAgents plist file exists and parse it to verify it's enabled
|
||||
*/
|
||||
@Override
|
||||
public boolean canAutoStart() {
|
||||
// plist is stored as io.qz.plist
|
||||
Path plistPath = Paths.get("/Library/LaunchAgents", MacUtilities.getBundleId() + ".plist");
|
||||
|
||||
if (Files.exists(plistPath)) {
|
||||
try {
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||
Document doc = dBuilder.parse(plistPath.toFile());
|
||||
doc.getDocumentElement().normalize();
|
||||
|
||||
NodeList dictList = doc.getElementsByTagName("dict");
|
||||
|
||||
// Loop to find "RunAtLoad" key, then the adjacent key
|
||||
boolean foundItem = false;
|
||||
if (dictList.getLength() > 0) {
|
||||
NodeList children = dictList.item(0).getChildNodes();
|
||||
for(int n = 0; n < children.getLength(); n++) {
|
||||
Node item = children.item(n);
|
||||
// Apple stores booleans as adjacent tags to their owner
|
||||
if (foundItem) {
|
||||
String nodeName = children.item(n).getNodeName();
|
||||
log.debug("Found RunAtLoad value {}", nodeName);
|
||||
return "true".equals(nodeName);
|
||||
}
|
||||
if (item.getNodeName().equals("key") && item.getTextContent().equals("RunAtLoad")) {
|
||||
log.debug("Found RunAtLoad key in {}", plistPath);
|
||||
foundItem = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("RunAtLoad was not in plist {}, autostart will not work.", plistPath);
|
||||
}
|
||||
catch(SAXException | IOException | ParserConfigurationException e) {
|
||||
log.warn("Error reading plist {}, autostart will not work.", plistPath, e);
|
||||
}
|
||||
} else {
|
||||
log.warn("No plist {} found, autostart will not work", plistPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void createDesktopShortcut() {
|
||||
try {
|
||||
new File(SHORTCUT_PATH).delete();
|
||||
if(SystemUtilities.getJarParentPath().endsWith("Contents")) {
|
||||
// We're probably running from an .app bundle
|
||||
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getAppPath());
|
||||
} else {
|
||||
// We're running from a mystery location, use the jar instead
|
||||
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getJarPath());
|
||||
}
|
||||
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not create desktop shortcut {}", SHORTCUT_PATH, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
old code/tray/src/qz/installer/shortcut/ShortcutCreator.java
Executable file
41
old code/tray/src/qz/installer/shortcut/ShortcutCreator.java
Executable file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.shortcut;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
/**
|
||||
* Utility class for creating, querying and removing startup shortcuts and
|
||||
* desktop shortcuts.
|
||||
*
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
public abstract class ShortcutCreator {
|
||||
private static ShortcutCreator instance;
|
||||
protected static final Logger log = LogManager.getLogger(ShortcutCreator.class);
|
||||
public abstract boolean canAutoStart();
|
||||
public abstract void createDesktopShortcut();
|
||||
|
||||
public static ShortcutCreator getInstance() {
|
||||
if (instance == null) {
|
||||
if (SystemUtilities.isWindows()) {
|
||||
instance = new WindowsShortcutCreator();
|
||||
} else if (SystemUtilities.isMac()) {
|
||||
instance = new MacShortcutCreator();
|
||||
} else {
|
||||
instance = new LinuxShortcutCreator();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
60
old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java
Executable file
60
old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java
Executable file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*
|
||||
*/
|
||||
|
||||
package qz.installer.shortcut;
|
||||
|
||||
import com.sun.jna.platform.win32.Win32Exception;
|
||||
import mslinks.ShellLinkException;
|
||||
import mslinks.ShellLinkHelper;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.WindowsSpecialFolders;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
public class WindowsShortcutCreator extends ShortcutCreator {
|
||||
private static String SHORTCUT_NAME = Constants.ABOUT_TITLE + ".lnk";
|
||||
|
||||
public void createDesktopShortcut() {
|
||||
createShortcut(WindowsSpecialFolders.DESKTOP.toString());
|
||||
}
|
||||
|
||||
public boolean canAutoStart() {
|
||||
try {
|
||||
return Files.exists(Paths.get(WindowsSpecialFolders.COMMON_STARTUP.toString(), SHORTCUT_NAME));
|
||||
} catch(Win32Exception e) {
|
||||
log.warn("An exception occurred locating the startup folder; autostart cannot be determined.", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void createShortcut(String folderPath) {
|
||||
try {
|
||||
ShellLinkHelper.createLink(getAppPath(), folderPath + File.separator + SHORTCUT_NAME);
|
||||
}
|
||||
catch(ShellLinkException | IOException ex) {
|
||||
log.warn("Error creating desktop shortcut", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates .exe path from .jar
|
||||
* fixme: overlaps SystemUtilities.getAppPath
|
||||
*/
|
||||
private static String getAppPath() {
|
||||
return SystemUtilities.getJarPath().toString().replaceAll(".jar$", ".exe");
|
||||
}
|
||||
}
|
||||
752
old code/tray/src/qz/printer/PrintOptions.java
Executable file
752
old code/tray/src/qz/printer/PrintOptions.java
Executable file
@@ -0,0 +1,752 @@
|
||||
package qz.printer;
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.LoggerUtilities;
|
||||
import qz.utils.PrintingUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.attribute.ResolutionSyntax;
|
||||
import javax.print.attribute.Size2DSyntax;
|
||||
import javax.print.attribute.standard.Chromaticity;
|
||||
import javax.print.attribute.standard.OrientationRequested;
|
||||
import javax.print.attribute.standard.PrinterResolution;
|
||||
import javax.print.attribute.standard.Sides;
|
||||
import java.awt.*;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PrintOptions {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintOptions.class);
|
||||
|
||||
private Pixel psOptions = new Pixel();
|
||||
private Raw rawOptions = new Raw();
|
||||
private Default defOptions = new Default();
|
||||
|
||||
|
||||
/**
|
||||
* Parses the provided JSON Object into relevant Pixel and Raw options
|
||||
*/
|
||||
public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities.Format format) {
|
||||
if (configOpts == null) { return; }
|
||||
|
||||
//check for raw options
|
||||
if (!configOpts.isNull("forceRaw")) {
|
||||
rawOptions.forceRaw = configOpts.optBoolean("forceRaw", false);
|
||||
} else if (!configOpts.isNull("altPrinting")) {
|
||||
log.warn("Raw option \"altPrinting\" is deprecated. Please use \"forceRaw\" instead.");
|
||||
rawOptions.forceRaw = configOpts.optBoolean("altPrinting", false);
|
||||
}
|
||||
if (rawOptions.forceRaw && SystemUtilities.isWindows()) {
|
||||
log.warn("Forced raw printing is not supported on Windows");
|
||||
rawOptions.forceRaw = false;
|
||||
}
|
||||
|
||||
if (!configOpts.isNull("encoding")) {
|
||||
JSONObject encodings = configOpts.optJSONObject("encoding");
|
||||
if (encodings != null) {
|
||||
rawOptions.srcEncoding = encodings.optString("from", null);
|
||||
rawOptions.destEncoding = encodings.optString("to", null);
|
||||
} else {
|
||||
rawOptions.destEncoding = configOpts.optString("encoding", null);
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("spool")) {
|
||||
JSONObject spool = configOpts.optJSONObject("spool");
|
||||
if (spool != null) {
|
||||
if (!spool.isNull("size")) {
|
||||
try { rawOptions.spoolSize = spool.getInt("size"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); }
|
||||
}
|
||||
// TODO: Implement spool.start
|
||||
if (!spool.isNull("end")) {
|
||||
rawOptions.spoolEnd = spool.optString("end");
|
||||
}
|
||||
|
||||
} else {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool"));
|
||||
}
|
||||
} else {
|
||||
// Deprecated
|
||||
if (!configOpts.isNull("perSpool")) {
|
||||
try { rawOptions.spoolSize = configOpts.getInt("perSpool"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "perSpool", configOpts.opt("perSpool")); }
|
||||
}
|
||||
if (!configOpts.isNull("endOfDoc")) {
|
||||
rawOptions.spoolEnd = configOpts.optString("endOfDoc", null);
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("copies")) {
|
||||
try { rawOptions.copies = configOpts.getInt("copies"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); }
|
||||
}
|
||||
if (!configOpts.isNull("jobName")) {
|
||||
rawOptions.jobName = configOpts.optString("jobName", null);
|
||||
}
|
||||
if (!configOpts.isNull("retainTemp")) {
|
||||
rawOptions.retainTemp = configOpts.optBoolean("retainTemp", false);
|
||||
}
|
||||
|
||||
|
||||
//check for pixel options
|
||||
if (!configOpts.isNull("units")) {
|
||||
switch(configOpts.optString("units")) {
|
||||
case "mm":
|
||||
psOptions.units = Unit.MM; break;
|
||||
case "cm":
|
||||
psOptions.units = Unit.CM; break;
|
||||
case "in":
|
||||
psOptions.units = Unit.INCH; break;
|
||||
default:
|
||||
LoggerUtilities.optionWarn(log, "valid value", "units", configOpts.opt("units")); break;
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("bounds")) {
|
||||
try {
|
||||
JSONObject bounds = configOpts.getJSONObject("bounds");
|
||||
psOptions.bounds = new Bounds(bounds.optDouble("x", 0), bounds.optDouble("y", 0), bounds.optDouble("width", 0), bounds.optDouble("height", 0));
|
||||
}
|
||||
catch(JSONException e) {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "bounds", configOpts.opt("bounds"));
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("colorType")) {
|
||||
try {
|
||||
psOptions.colorType = ColorType.valueOf(configOpts.optString("colorType").toUpperCase(Locale.ENGLISH));
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
LoggerUtilities.optionWarn(log, "valid value", "colorType", configOpts.opt("colorType"));
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("copies")) {
|
||||
try { psOptions.copies = configOpts.getInt("copies"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); }
|
||||
if (psOptions.copies < 1) {
|
||||
log.warn("Cannot have less than one copy");
|
||||
psOptions.copies = 1;
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("density")) {
|
||||
JSONObject asymmDPI = configOpts.optJSONObject("density");
|
||||
if (asymmDPI != null) {
|
||||
psOptions.density = asymmDPI.optInt("feed");
|
||||
psOptions.crossDensity = asymmDPI.optInt("cross");
|
||||
} else {
|
||||
List<PrinterResolution> rSupport = output.isSetService()?
|
||||
output.getNativePrinter().getResolutions():new ArrayList<>();
|
||||
|
||||
JSONArray possibleDPIs = configOpts.optJSONArray("density");
|
||||
if (possibleDPIs != null && possibleDPIs.length() > 0) {
|
||||
PrinterResolution usableRes = null;
|
||||
|
||||
if (!rSupport.isEmpty()) {
|
||||
for(int i = 0; i < possibleDPIs.length(); i++) {
|
||||
PrinterResolution compareRes;
|
||||
asymmDPI = possibleDPIs.optJSONObject(i);
|
||||
if (asymmDPI != null) {
|
||||
compareRes = new PrinterResolution(asymmDPI.optInt("cross"), asymmDPI.optInt("feed"), psOptions.units.resSyntax);
|
||||
} else {
|
||||
compareRes = new PrinterResolution(possibleDPIs.optInt(i), possibleDPIs.optInt(i), psOptions.units.resSyntax);
|
||||
}
|
||||
|
||||
if (rSupport.contains(compareRes)) {
|
||||
usableRes = compareRes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usableRes == null) {
|
||||
log.warn("Supported printer densities not found, using first value provided");
|
||||
asymmDPI = possibleDPIs.optJSONObject(0);
|
||||
if (asymmDPI != null) {
|
||||
psOptions.density = asymmDPI.optInt("feed");
|
||||
psOptions.crossDensity = asymmDPI.optInt("cross");
|
||||
} else {
|
||||
psOptions.density = possibleDPIs.optInt(0);
|
||||
}
|
||||
} else {
|
||||
psOptions.density = usableRes.getFeedResolution(psOptions.units.resSyntax);
|
||||
psOptions.crossDensity = usableRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||
}
|
||||
} else {
|
||||
String relDPI = configOpts.optString("density", "").toLowerCase(Locale.ENGLISH);
|
||||
if ("best".equals(relDPI)) {
|
||||
PrinterResolution bestRes = null;
|
||||
for(PrinterResolution pr : rSupport) {
|
||||
if (bestRes == null || !pr.lessThanOrEquals(bestRes)) {
|
||||
bestRes = pr;
|
||||
}
|
||||
}
|
||||
if (bestRes != null) {
|
||||
psOptions.density = bestRes.getFeedResolution(psOptions.units.resSyntax);
|
||||
psOptions.crossDensity = bestRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||
} else {
|
||||
log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI);
|
||||
}
|
||||
} else if ("draft".equals(relDPI)) {
|
||||
PrinterResolution lowestRes = null;
|
||||
for(PrinterResolution pr : rSupport) {
|
||||
if (lowestRes == null || pr.lessThanOrEquals(lowestRes)) {
|
||||
lowestRes = pr;
|
||||
}
|
||||
}
|
||||
if (lowestRes != null) {
|
||||
psOptions.density = lowestRes.getFeedResolution(psOptions.units.resSyntax);
|
||||
psOptions.crossDensity = lowestRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||
} else {
|
||||
log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI);
|
||||
}
|
||||
} else {
|
||||
try { psOptions.density = configOpts.getDouble("density"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "density", configOpts.opt("density")); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("dithering")) {
|
||||
try {
|
||||
if (configOpts.getBoolean("dithering")) {
|
||||
psOptions.dithering = RenderingHints.VALUE_DITHER_ENABLE;
|
||||
} else {
|
||||
psOptions.dithering = RenderingHints.VALUE_DITHER_DISABLE;
|
||||
}
|
||||
}
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "dithering", configOpts.opt("dithering")); }
|
||||
}
|
||||
if (!configOpts.isNull("duplex")) {
|
||||
try {
|
||||
if (configOpts.getBoolean("duplex")) {
|
||||
psOptions.duplex = Sides.DUPLEX;
|
||||
}
|
||||
}
|
||||
catch(JSONException e) {
|
||||
//not a boolean, try as a string
|
||||
try {
|
||||
String duplex = configOpts.getString("duplex").toLowerCase(Locale.ENGLISH);
|
||||
if (duplex.matches("^(duplex|(two.sided.)?long(.edge)?)$")) {
|
||||
psOptions.duplex = Sides.DUPLEX;
|
||||
} else if (duplex.matches("^(tumble|(two.sided.)?short(.edge)?)$")) {
|
||||
psOptions.duplex = Sides.TUMBLE;
|
||||
}
|
||||
//else - one sided (default)
|
||||
}
|
||||
catch(JSONException e2) { LoggerUtilities.optionWarn(log, "valid value", "duplex", configOpts.opt("duplex")); }
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("interpolation")) {
|
||||
switch(configOpts.optString("interpolation")) {
|
||||
case "bicubic":
|
||||
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; break;
|
||||
case "bilinear":
|
||||
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR; break;
|
||||
case "nearest-neighbor":
|
||||
case "nearest":
|
||||
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; break;
|
||||
default:
|
||||
LoggerUtilities.optionWarn(log, "valid value", "interpolation", configOpts.opt("interpolation")); break;
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("jobName")) {
|
||||
psOptions.jobName = configOpts.optString("jobName", null);
|
||||
}
|
||||
if (!configOpts.isNull("legacy")) {
|
||||
psOptions.legacy = configOpts.optBoolean("legacy", false);
|
||||
}
|
||||
if (!configOpts.isNull("margins")) {
|
||||
Margins m = new Margins();
|
||||
JSONObject subMargins = configOpts.optJSONObject("margins");
|
||||
if (subMargins != null) {
|
||||
//each individually
|
||||
if (!subMargins.isNull("top")) {
|
||||
try { m.top = subMargins.getDouble("top"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.top", subMargins.opt("top")); }
|
||||
}
|
||||
if (!subMargins.isNull("right")) {
|
||||
try { m.right = subMargins.getDouble("right"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.right", subMargins.opt("right")); }
|
||||
}
|
||||
if (!subMargins.isNull("bottom")) {
|
||||
try { m.bottom = subMargins.getDouble("bottom"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.bottom", subMargins.opt("bottom")); }
|
||||
}
|
||||
if (!subMargins.isNull("left")) {
|
||||
try { m.left = subMargins.getDouble("left"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.left", subMargins.opt("left")); }
|
||||
}
|
||||
} else {
|
||||
try { m.setAll(configOpts.getDouble("margins")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins", configOpts.opt("margins")); }
|
||||
}
|
||||
|
||||
psOptions.margins = m;
|
||||
}
|
||||
if (!configOpts.isNull("orientation")) {
|
||||
try {
|
||||
psOptions.orientation = Orientation.valueOf(configOpts.optString("orientation").replaceAll("-", "_").toUpperCase(Locale.ENGLISH));
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
LoggerUtilities.optionWarn(log, "valid value", "orientation", configOpts.opt("orientation"));
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("paperThickness")) {
|
||||
try { psOptions.paperThickness = configOpts.getDouble("paperThickness"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "paperThickness", configOpts.opt("paperThickness")); }
|
||||
}
|
||||
if (!configOpts.isNull("spool")) {
|
||||
JSONObject spool = configOpts.optJSONObject("spool");
|
||||
if (spool != null) {
|
||||
if (!spool.isNull("size")) {
|
||||
try { psOptions.spoolSize = spool.getInt("size"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); }
|
||||
}
|
||||
} else {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool"));
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("printerTray")) {
|
||||
psOptions.printerTray = configOpts.optString("printerTray", null);
|
||||
// Guard empty string value; will break pattern matching
|
||||
if(psOptions.printerTray != null && psOptions.printerTray.trim().equals("")) {
|
||||
psOptions.printerTray = null;
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("rasterize")) {
|
||||
try { psOptions.rasterize = configOpts.getBoolean("rasterize"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "rasterize", configOpts.opt("rasterize")); }
|
||||
}
|
||||
if (!configOpts.isNull("rotation")) {
|
||||
try { psOptions.rotation = configOpts.getDouble("rotation"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "rotation", configOpts.opt("rotation")); }
|
||||
}
|
||||
if (!configOpts.isNull("scaleContent")) {
|
||||
try { psOptions.scaleContent = configOpts.getBoolean("scaleContent"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "scaleContent", configOpts.opt("scaleContent")); }
|
||||
}
|
||||
if (!configOpts.isNull("size")) {
|
||||
Size s = new Size();
|
||||
JSONObject subSize = configOpts.optJSONObject("size");
|
||||
if (subSize != null) {
|
||||
if (!subSize.isNull("width")) {
|
||||
try { s.width = subSize.getDouble("width"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.width", subSize.opt("width")); }
|
||||
}
|
||||
if (!subSize.isNull("height")) {
|
||||
try { s.height = subSize.getDouble("height"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.height", subSize.opt("height")); }
|
||||
}
|
||||
|
||||
if (s.height <= 0 && s.width <= 0) {
|
||||
log.warn("Page size has been set without dimensions, using default");
|
||||
} else {
|
||||
psOptions.size = s;
|
||||
}
|
||||
} else {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "size", configOpts.opt("size"));
|
||||
}
|
||||
}
|
||||
|
||||
//grab any useful service defaults
|
||||
PrinterResolution defaultRes = null;
|
||||
if (output.isSetService()) {
|
||||
defaultRes = output.getNativePrinter().getResolution().value();
|
||||
|
||||
if (defaultRes == null) {
|
||||
//printer has no default resolution set, see if it is possible to pull anything
|
||||
List<PrinterResolution> rSupport = output.getNativePrinter().getResolutions();
|
||||
if (rSupport.size() > 0) {
|
||||
defaultRes = rSupport.get(0);
|
||||
log.warn("Default resolution for {} is missing, using fallback: {}", output.getNativePrinter().getName(), defaultRes);
|
||||
} else {
|
||||
log.warn("Default resolution for {} is missing, no fallback available.", output.getNativePrinter().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (defaultRes != null) {
|
||||
//convert dphi to unit-dependant density ourselves (to keep as double type)
|
||||
defOptions.density = (double)defaultRes.getFeedResolution(1) / psOptions.getUnits().getDPIUnits();
|
||||
} else {
|
||||
try { defOptions.density = configOpts.getDouble("fallbackDensity"); }
|
||||
catch(JSONException e) {
|
||||
LoggerUtilities.optionWarn(log, "double", "fallbackDensity", configOpts.opt("fallbackDensity"));
|
||||
//manually convert default dphi to a density value based on units
|
||||
defOptions.density = 60000d / psOptions.getUnits().getDPIUnits();
|
||||
}
|
||||
}
|
||||
if ((psOptions.isRasterize() || format == PrintingUtilities.Format.IMAGE) && psOptions.getDensity() <= 1) {
|
||||
psOptions.density = defOptions.density;
|
||||
psOptions.crossDensity = defOptions.density;
|
||||
}
|
||||
|
||||
if (output.isSetService()) {
|
||||
try {
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(output.getPrintService());
|
||||
PageFormat page = job.getPageFormat(null);
|
||||
defOptions.pageSize = new Size(page.getWidth(), page.getHeight());
|
||||
}
|
||||
catch(PrinterException e) {
|
||||
log.warn("Unable to find the default paper size");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Raw getRawOptions() {
|
||||
return rawOptions;
|
||||
}
|
||||
|
||||
public Pixel getPixelOptions() {
|
||||
return psOptions;
|
||||
}
|
||||
|
||||
public Default getDefaultOptions() { return defOptions; }
|
||||
|
||||
|
||||
// Option groups //
|
||||
|
||||
/** Raw printing options */
|
||||
public class Raw {
|
||||
private boolean forceRaw = false; //Alternate printing for linux systems
|
||||
private String destEncoding = null; //Text encoding / charset
|
||||
private String srcEncoding = null; //Conversion text encoding
|
||||
private String spoolEnd = null; //End of document character(s)
|
||||
private int spoolSize = 1; //Pages per spool
|
||||
private int copies = 1; //Job copies
|
||||
private String jobName = null; //Job name
|
||||
private boolean retainTemp = false; //Retain any temporary files
|
||||
|
||||
|
||||
public boolean isForceRaw() {
|
||||
return forceRaw;
|
||||
}
|
||||
|
||||
public String getDestEncoding() {
|
||||
return destEncoding;
|
||||
}
|
||||
|
||||
public String getSrcEncoding() {
|
||||
return srcEncoding;
|
||||
}
|
||||
|
||||
public String getSpoolEnd() {
|
||||
return spoolEnd;
|
||||
}
|
||||
|
||||
public int getSpoolSize() {
|
||||
return spoolSize;
|
||||
}
|
||||
|
||||
public int getCopies() {
|
||||
return copies;
|
||||
}
|
||||
|
||||
public boolean isRetainTemp() { return retainTemp; }
|
||||
|
||||
public String getJobName(String defaultVal) {
|
||||
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel printing options */
|
||||
public class Pixel {
|
||||
private Bounds bounds = null; //Bounding box rectangle
|
||||
private ColorType colorType = ColorType.COLOR; //Color / black&white
|
||||
private int copies = 1; //Job copies
|
||||
private double crossDensity = 0; //Cross feed density
|
||||
private double density = 0; //Pixel density (DPI or DPMM), feed density if crossDensity is defined
|
||||
private Object dithering = RenderingHints.VALUE_DITHER_DEFAULT; //Image dithering
|
||||
private Sides duplex = Sides.ONE_SIDED; //Multi-siding
|
||||
private Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; //Image interpolation
|
||||
private String jobName = null; //Job name
|
||||
private boolean legacy = false; //Legacy printing
|
||||
private Margins margins = new Margins(); //Page margins
|
||||
private Orientation orientation = null; //Page orientation
|
||||
private double paperThickness = -1; //Paper thickness
|
||||
private int spoolSize = 0; //Pages before sending to printer
|
||||
private String printerTray = null; //Printer tray to use
|
||||
private boolean rasterize = true; //Whether documents are rasterized before printing
|
||||
private double rotation = 0; //Image rotation
|
||||
private boolean scaleContent = true; //Adjust paper size for best image fit
|
||||
private Size size = null; //Paper size
|
||||
private Unit units = Unit.INCH; //Units for density, margins, size
|
||||
|
||||
|
||||
public Bounds getBounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public ColorType getColorType() {
|
||||
return colorType;
|
||||
}
|
||||
|
||||
public int getCopies() {
|
||||
return copies;
|
||||
}
|
||||
|
||||
public double getCrossDensity() {
|
||||
return crossDensity;
|
||||
}
|
||||
|
||||
public double getDensity() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public Object getDithering() {
|
||||
return dithering;
|
||||
}
|
||||
|
||||
public Sides getDuplex() {
|
||||
return duplex;
|
||||
}
|
||||
|
||||
public Object getInterpolation() {
|
||||
return interpolation;
|
||||
}
|
||||
|
||||
public String getJobName(String defaultVal) {
|
||||
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
|
||||
}
|
||||
|
||||
public boolean isLegacy() {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
public Margins getMargins() {
|
||||
return margins;
|
||||
}
|
||||
|
||||
public Orientation getOrientation() {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
public double getPaperThickness() {
|
||||
return paperThickness;
|
||||
}
|
||||
|
||||
public int getSpoolSize() {
|
||||
return spoolSize;
|
||||
}
|
||||
|
||||
public String getPrinterTray() {
|
||||
return printerTray;
|
||||
}
|
||||
|
||||
public boolean isRasterize() {
|
||||
return rasterize;
|
||||
}
|
||||
|
||||
public double getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public boolean isScaleContent() {
|
||||
return scaleContent;
|
||||
}
|
||||
|
||||
public Size getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public Unit getUnits() {
|
||||
return units;
|
||||
}
|
||||
}
|
||||
|
||||
/** PrintService Defaults **/
|
||||
public class Default {
|
||||
private double density;
|
||||
private Size pageSize;
|
||||
|
||||
|
||||
public double getDensity() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public Size getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Sub options //
|
||||
|
||||
/** Pixel page size options */
|
||||
public class Size {
|
||||
private double width = -1; //Page width
|
||||
private double height = -1; //Page height
|
||||
|
||||
|
||||
public Size() {}
|
||||
|
||||
public Size(double width, double height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel page margins options */
|
||||
public class Margins {
|
||||
private double top = 0; //Top page margin
|
||||
private double right = 0; //Right page margin
|
||||
private double bottom = 0; //Bottom page margin
|
||||
private double left = 0; //Left page margin
|
||||
|
||||
private void setAll(double margin) {
|
||||
top = margin;
|
||||
right = margin;
|
||||
bottom = margin;
|
||||
left = margin;
|
||||
}
|
||||
|
||||
|
||||
public double top() {
|
||||
return top;
|
||||
}
|
||||
|
||||
public double right() {
|
||||
return right;
|
||||
}
|
||||
|
||||
public double bottom() {
|
||||
return bottom;
|
||||
}
|
||||
|
||||
public double left() {
|
||||
return left;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bounding box generic rectangle */
|
||||
public class Bounds {
|
||||
private double x;
|
||||
private double y;
|
||||
private double width;
|
||||
private double height;
|
||||
|
||||
public Bounds(double x, double y, double width, double height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel dimension values */
|
||||
public enum Unit {
|
||||
INCH(ResolutionSyntax.DPI, 1.0f, 1.0f, Size2DSyntax.INCH), //1in = 1in
|
||||
CM(ResolutionSyntax.DPCM, .3937f, 2.54f, 10000), //1cm = .3937in ; 1in = 2.54cm
|
||||
MM(ResolutionSyntax.DPCM * 10, .03937f, 25.4f, Size2DSyntax.MM); //1mm = .03937in ; 1in = 25.4mm
|
||||
|
||||
private final float fromInch;
|
||||
private final float toInch; //multiplicand to convert to inches
|
||||
private final int resSyntax;
|
||||
private final int µm;
|
||||
|
||||
Unit(int resSyntax, float toIN, float fromIN, int µm) {
|
||||
toInch = toIN;
|
||||
fromInch = fromIN;
|
||||
this.resSyntax = resSyntax;
|
||||
this.µm = µm;
|
||||
}
|
||||
|
||||
public float toInches() {
|
||||
return toInch;
|
||||
}
|
||||
|
||||
public float as1Inch() {
|
||||
return fromInch;
|
||||
}
|
||||
|
||||
public int getDPIUnits() {
|
||||
return resSyntax;
|
||||
}
|
||||
|
||||
public int getMediaSizeUnits() {
|
||||
return µm;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel page orientation option */
|
||||
public enum Orientation {
|
||||
PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 0),
|
||||
REVERSE_PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 180),
|
||||
LANDSCAPE(OrientationRequested.LANDSCAPE, PageFormat.LANDSCAPE, 270),
|
||||
REVERSE_LANDSCAPE(OrientationRequested.REVERSE_LANDSCAPE, PageFormat.REVERSE_LANDSCAPE, 90);
|
||||
|
||||
private final OrientationRequested orientationRequested;
|
||||
private final int orientationFormat;
|
||||
private final int degreesRot;
|
||||
|
||||
Orientation(OrientationRequested orientationRequested, int orientationFormat, int degreesRot) {
|
||||
this.orientationRequested = orientationRequested;
|
||||
this.orientationFormat = orientationFormat;
|
||||
this.degreesRot = degreesRot;
|
||||
}
|
||||
|
||||
|
||||
public OrientationRequested getAsOrientRequested() {
|
||||
return orientationRequested;
|
||||
}
|
||||
|
||||
public int getAsOrientFormat() {
|
||||
return orientationFormat;
|
||||
}
|
||||
|
||||
public int getDegreesRot() {
|
||||
return degreesRot;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel page color option */
|
||||
public enum ColorType {
|
||||
COLOR(Chromaticity.COLOR),
|
||||
GREYSCALE(Chromaticity.MONOCHROME),
|
||||
GRAYSCALE(Chromaticity.MONOCHROME),
|
||||
BLACKWHITE(Chromaticity.MONOCHROME),
|
||||
DEFAULT(null);
|
||||
|
||||
private final Chromaticity chromatic;
|
||||
|
||||
ColorType(Chromaticity chromatic) {
|
||||
this.chromatic = chromatic;
|
||||
}
|
||||
|
||||
|
||||
public Chromaticity getAsChromaticity() {
|
||||
return chromatic;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
92
old code/tray/src/qz/printer/PrintOutput.java
Executable file
92
old code/tray/src/qz/printer/PrintOutput.java
Executable file
@@ -0,0 +1,92 @@
|
||||
package qz.printer;
|
||||
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.printer.info.NativePrinter;
|
||||
import qz.utils.FileUtilities;
|
||||
|
||||
import javax.print.PrintService;
|
||||
import javax.print.attribute.standard.Media;
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class PrintOutput {
|
||||
|
||||
private NativePrinter printer = null;
|
||||
|
||||
private File file = null;
|
||||
|
||||
private String host = null;
|
||||
private int port = -1;
|
||||
|
||||
|
||||
public PrintOutput(JSONObject configPrinter) throws JSONException, IllegalArgumentException {
|
||||
if (configPrinter == null) { return; }
|
||||
|
||||
if (configPrinter.has("name")) {
|
||||
printer = PrintServiceMatcher.matchPrinter(configPrinter.getString("name"));
|
||||
if (printer == null) {
|
||||
throw new IllegalArgumentException("Cannot find printer with name \"" + configPrinter.getString("name") + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
if (configPrinter.has("file")) {
|
||||
String filename = configPrinter.getString("file");
|
||||
if (!FileUtilities.isGoodExtension(Paths.get(filename))) {
|
||||
throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited file extension)");
|
||||
} else if (FileUtilities.isBadPath(filename)) {
|
||||
throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited directory name)");
|
||||
} else {
|
||||
file = new File(filename);
|
||||
}
|
||||
}
|
||||
|
||||
if (configPrinter.has("host")) {
|
||||
host = configPrinter.getString("host");
|
||||
port = configPrinter.optInt("port", 9100); // default to port 9100 (HP/JetDirect standard) if not provided
|
||||
}
|
||||
|
||||
//at least one method must be set for printing
|
||||
if (!isSetService() && !isSetFile() && !isSetHost()) {
|
||||
throw new IllegalArgumentException("No printer output has been specified");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean isSetService() {
|
||||
return printer != null && printer.getPrintService() != null && !printer.getPrintService().isNull();
|
||||
}
|
||||
|
||||
public PrintService getPrintService() {
|
||||
return printer.getPrintService().value();
|
||||
}
|
||||
|
||||
public NativePrinter getNativePrinter() {
|
||||
return printer;
|
||||
}
|
||||
|
||||
public boolean isSetFile() {
|
||||
return file != null;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public boolean isSetHost() {
|
||||
return host != null;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public Media[] getSupportedMedia() {
|
||||
return (Media[])getPrintService().getSupportedAttributeValues(Media.class, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
242
old code/tray/src/qz/printer/PrintServiceMatcher.java
Executable file
242
old code/tray/src/qz/printer/PrintServiceMatcher.java
Executable file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.printer;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.printer.info.CachedPrintServiceLookup;
|
||||
import qz.printer.info.NativePrinter;
|
||||
import qz.printer.info.NativePrinterMap;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.PrintService;
|
||||
import javax.print.PrintServiceLookup;
|
||||
import javax.print.attribute.ResolutionSyntax;
|
||||
import javax.print.attribute.standard.*;
|
||||
import java.util.*;
|
||||
|
||||
public class PrintServiceMatcher {
|
||||
private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class);
|
||||
|
||||
// PrintService is slow in CUPS, use a cache instead per JDK-7001133
|
||||
// TODO: Include JDK version test for caching when JDK-7001133 is fixed upstream
|
||||
private static final boolean useCache = SystemUtilities.isUnix();
|
||||
|
||||
public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) {
|
||||
NativePrinterMap printers = NativePrinterMap.getInstance();
|
||||
printers.putAll(true, lookupPrintServices());
|
||||
if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); }
|
||||
if (!silent) { log.debug("Found {} printers", printers.size()); }
|
||||
return printers;
|
||||
}
|
||||
|
||||
private static PrintService[] lookupPrintServices() {
|
||||
return useCache ? CachedPrintServiceLookup.lookupPrintServices() :
|
||||
PrintServiceLookup.lookupPrintServices(null, null);
|
||||
}
|
||||
|
||||
private static PrintService lookupDefaultPrintService() {
|
||||
return useCache ? CachedPrintServiceLookup.lookupDefaultPrintService() :
|
||||
PrintServiceLookup.lookupDefaultPrintService();
|
||||
}
|
||||
|
||||
public static NativePrinterMap getNativePrinterList(boolean silent) {
|
||||
return getNativePrinterList(silent, false);
|
||||
}
|
||||
|
||||
public static NativePrinterMap getNativePrinterList() {
|
||||
return getNativePrinterList(false);
|
||||
}
|
||||
|
||||
public static NativePrinter getDefaultPrinter() {
|
||||
PrintService defaultService = lookupDefaultPrintService();
|
||||
|
||||
if(defaultService == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
NativePrinterMap printers = NativePrinterMap.getInstance();
|
||||
if (!printers.contains(defaultService)) {
|
||||
printers.putAll(false, defaultService);
|
||||
}
|
||||
|
||||
return printers.get(defaultService);
|
||||
}
|
||||
|
||||
public static String findPrinterName(String query) throws JSONException {
|
||||
NativePrinter printer = PrintServiceMatcher.matchPrinter(query);
|
||||
|
||||
if (printer != null) {
|
||||
return printer.getPrintService().value().getName();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds {@code PrintService} by looking at any matches to {@code printerSearch}.
|
||||
*
|
||||
* @param printerSearch Search query to compare against service names.
|
||||
*/
|
||||
public static NativePrinter matchPrinter(String printerSearch, boolean silent) {
|
||||
NativePrinter exact = null;
|
||||
NativePrinter begins = null;
|
||||
NativePrinter partial = null;
|
||||
|
||||
if (!silent) { log.debug("Searching for PrintService matching {}", printerSearch); }
|
||||
|
||||
// Fix for https://github.com/qzind/tray/issues/931
|
||||
// This is more than an optimization, removal will lead to a regression
|
||||
NativePrinter defaultPrinter = getDefaultPrinter();
|
||||
if (defaultPrinter != null && printerSearch.equals(defaultPrinter.getName())) {
|
||||
if (!silent) { log.debug("Matched default printer, skipping further search"); }
|
||||
return defaultPrinter;
|
||||
}
|
||||
|
||||
printerSearch = printerSearch.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
// Search services for matches
|
||||
for(NativePrinter printer : getNativePrinterList(silent).values()) {
|
||||
if (printer.getName() == null) {
|
||||
continue;
|
||||
}
|
||||
String printerName = printer.getName().toLowerCase(Locale.ENGLISH);
|
||||
if (printerName.equals(printerSearch)) {
|
||||
exact = printer;
|
||||
break;
|
||||
}
|
||||
if (printerName.startsWith(printerSearch)) {
|
||||
begins = printer;
|
||||
continue;
|
||||
}
|
||||
if (printerName.contains(printerSearch)) {
|
||||
partial = printer;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SystemUtilities.isMac()) {
|
||||
// 1.9 compat: fallback for old style names
|
||||
PrinterName name = printer.getLegacyName();
|
||||
if (name == null || name.getValue() == null) { continue; }
|
||||
printerName = name.getValue().toLowerCase(Locale.ENGLISH);
|
||||
if (printerName.equals(printerSearch)) {
|
||||
exact = printer;
|
||||
continue;
|
||||
}
|
||||
if (printerName.startsWith(printerSearch)) {
|
||||
begins = printer;
|
||||
continue;
|
||||
}
|
||||
if (printerName.contains(printerSearch)) {
|
||||
partial = printer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return closest match
|
||||
NativePrinter use = null;
|
||||
if (exact != null) {
|
||||
use = exact;
|
||||
} else if (begins != null) {
|
||||
use = begins;
|
||||
} else if (partial != null) {
|
||||
use = partial;
|
||||
}
|
||||
|
||||
if (use != null) {
|
||||
if(!silent) log.debug("Found match: {}", use.getPrintService().value().getName());
|
||||
} else {
|
||||
log.warn("Printer not found: {}", printerSearch);
|
||||
}
|
||||
|
||||
return use;
|
||||
}
|
||||
|
||||
public static NativePrinter matchPrinter(String printerSearch) {
|
||||
return matchPrinter(printerSearch, false);
|
||||
}
|
||||
|
||||
public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException {
|
||||
JSONArray list = new JSONArray();
|
||||
|
||||
PrintService defaultService = lookupDefaultPrintService();
|
||||
|
||||
boolean mediaTrayCrawled = false;
|
||||
|
||||
for(NativePrinter printer : getNativePrinterList().values()) {
|
||||
PrintService ps = printer.getPrintService().value();
|
||||
JSONObject jsonService = new JSONObject();
|
||||
jsonService.put("name", ps.getName());
|
||||
|
||||
if (includeDetails) {
|
||||
jsonService.put("driver", printer.getDriver().value());
|
||||
jsonService.put("connection", printer.getConnection());
|
||||
jsonService.put("default", ps == defaultService);
|
||||
|
||||
if (!mediaTrayCrawled) {
|
||||
log.info("Gathering printer MediaTray information...");
|
||||
mediaTrayCrawled = true;
|
||||
}
|
||||
|
||||
HashSet<String> uniqueSizes = new HashSet<>(); // prevents duplicates
|
||||
JSONArray trays = new JSONArray();
|
||||
JSONArray sizes = new JSONArray();
|
||||
|
||||
for(Media m : (Media[])ps.getSupportedAttributeValues(Media.class, null, null)) {
|
||||
if (m instanceof MediaTray) { trays.put(m.toString()); }
|
||||
if (m instanceof MediaSizeName) {
|
||||
if(uniqueSizes.add(m.toString())) {
|
||||
MediaSize mediaSize = MediaSize.getMediaSizeForName((MediaSizeName)m);
|
||||
if(mediaSize == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject size = new JSONObject();
|
||||
size.put("name", m.toString());
|
||||
|
||||
JSONObject in = new JSONObject();
|
||||
in.put("width", mediaSize.getX(MediaPrintableArea.INCH));
|
||||
in.put("height", mediaSize.getY(MediaPrintableArea.INCH));
|
||||
size.put("in", in);
|
||||
|
||||
JSONObject mm = new JSONObject();
|
||||
mm.put("width", mediaSize.getX(MediaPrintableArea.MM));
|
||||
mm.put("height", mediaSize.getY(MediaPrintableArea.MM));
|
||||
size.put("mm", mm);
|
||||
|
||||
sizes.put(size);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if(trays.length() > 0) {
|
||||
jsonService.put("trays", trays);
|
||||
}
|
||||
if(sizes.length() > 0) {
|
||||
jsonService.put("sizes", sizes);
|
||||
}
|
||||
|
||||
PrinterResolution res = printer.getResolution().value();
|
||||
int density = -1; if (res != null) { density = res.getFeedResolution(ResolutionSyntax.DPI); }
|
||||
jsonService.put("density", density);
|
||||
}
|
||||
|
||||
list.put(jsonService);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
97
old code/tray/src/qz/printer/action/PrintDirect.java
Executable file
97
old code/tray/src/qz/printer/action/PrintDirect.java
Executable file
@@ -0,0 +1,97 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64InputStream;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.utils.PrintingUtilities;
|
||||
|
||||
import javax.print.DocFlavor;
|
||||
import javax.print.DocPrintJob;
|
||||
import javax.print.PrintException;
|
||||
import javax.print.SimpleDoc;
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.JobName;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PrintDirect extends PrintRaw {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintDirect.class);
|
||||
|
||||
private ArrayList<String> prints = new ArrayList<>();
|
||||
private ArrayList<PrintingUtilities.Flavor> flavors = new ArrayList<>();
|
||||
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.DIRECT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.optJSONObject(i);
|
||||
if (data == null) { continue; }
|
||||
|
||||
prints.add(data.getString("data"));
|
||||
flavors.add(PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrintException {
|
||||
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||
attributes.add(new JobName(options.getRawOptions().getJobName(Constants.RAW_PRINT), Locale.getDefault()));
|
||||
|
||||
for(int i = 0; i < prints.size(); i++) {
|
||||
DocPrintJob printJob = output.getPrintService().createPrintJob();
|
||||
InputStream stream = null;
|
||||
|
||||
try {
|
||||
switch(flavors.get(i)) {
|
||||
case BASE64:
|
||||
stream = new Base64InputStream(new ByteArrayInputStream(prints.get(i).getBytes("UTF-8")));
|
||||
break;
|
||||
case FILE:
|
||||
stream = new DataInputStream(new URL(prints.get(i)).openStream());
|
||||
break;
|
||||
case PLAIN:
|
||||
default:
|
||||
stream = new ByteArrayInputStream(prints.get(i).getBytes("UTF-8"));
|
||||
break;
|
||||
}
|
||||
|
||||
SimpleDoc doc = new SimpleDoc(stream, DocFlavor.INPUT_STREAM.AUTOSENSE, null);
|
||||
|
||||
waitForPrint(printJob, doc, attributes);
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new PrintException(e);
|
||||
}
|
||||
finally {
|
||||
if (stream != null) {
|
||||
try { stream.close(); } catch(Exception ignore) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
prints.clear();
|
||||
flavors.clear();
|
||||
}
|
||||
|
||||
}
|
||||
414
old code/tray/src/qz/printer/action/PrintHTML.java
Executable file
414
old code/tray/src/qz/printer/action/PrintHTML.java
Executable file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.printer.action;
|
||||
|
||||
import com.sun.javafx.print.PrintHelper;
|
||||
import com.sun.javafx.print.Units;
|
||||
import javafx.print.*;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.printer.action.html.WebApp;
|
||||
import qz.printer.action.html.WebAppModel;
|
||||
import qz.utils.PrintingUtilities;
|
||||
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.Copies;
|
||||
import javax.print.attribute.standard.CopiesSupported;
|
||||
import javax.print.attribute.standard.Sides;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class PrintHTML extends PrintImage implements PrintProcessor {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintHTML.class);
|
||||
|
||||
private List<WebAppModel> models;
|
||||
|
||||
private JLabel legacyLabel = null;
|
||||
|
||||
|
||||
public PrintHTML() {
|
||||
super();
|
||||
models = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.HTML;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
try {
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
if (!pxlOpts.isLegacy()) {
|
||||
WebApp.initialize();
|
||||
}
|
||||
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.getJSONObject(i);
|
||||
|
||||
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||
|
||||
String source;
|
||||
switch(flavor) {
|
||||
case FILE:
|
||||
case PLAIN:
|
||||
// We'll toggle between 'plain' and 'file' when we construct WebAppModel
|
||||
source = data.getString("data");
|
||||
break;
|
||||
default:
|
||||
source = new String(flavor.read(data.getString("data")), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
double pageZoom = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch()) / 72.0;
|
||||
if (pageZoom <= 1) { pageZoom = 1; }
|
||||
|
||||
double pageWidth = 0;
|
||||
double pageHeight = 0;
|
||||
double convertFactor = (72.0 / pxlOpts.getUnits().as1Inch());
|
||||
|
||||
boolean renderFromHeight = Arrays.asList(PrintOptions.Orientation.LANDSCAPE,
|
||||
PrintOptions.Orientation.REVERSE_LANDSCAPE).contains(pxlOpts.getOrientation());
|
||||
|
||||
if (pxlOpts.getSize() != null) {
|
||||
if (!renderFromHeight) {
|
||||
pageWidth = pxlOpts.getSize().getWidth() * convertFactor;
|
||||
} else {
|
||||
pageWidth = pxlOpts.getSize().getHeight() * convertFactor;
|
||||
}
|
||||
} else if (options.getDefaultOptions().getPageSize() != null) {
|
||||
if (!renderFromHeight) {
|
||||
pageWidth = options.getDefaultOptions().getPageSize().getWidth();
|
||||
} else {
|
||||
pageWidth = options.getDefaultOptions().getPageSize().getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
if (pxlOpts.getMargins() != null) {
|
||||
PrintOptions.Margins margins = pxlOpts.getMargins();
|
||||
if (!renderFromHeight || pxlOpts.isRasterize()) {
|
||||
pageWidth -= (margins.left() + margins.right()) * convertFactor;
|
||||
} else {
|
||||
pageWidth -= (margins.top() + margins.bottom()) * convertFactor; //due to vector margin matching
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.isNull("options")) {
|
||||
JSONObject dataOpt = data.getJSONObject("options");
|
||||
|
||||
if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) {
|
||||
pageWidth = dataOpt.optDouble("pageWidth") * convertFactor;
|
||||
}
|
||||
if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) {
|
||||
pageHeight = dataOpt.optDouble("pageHeight") * convertFactor;
|
||||
}
|
||||
}
|
||||
|
||||
models.add(new WebAppModel(source, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, pxlOpts.isScaleContent(), pageZoom));
|
||||
}
|
||||
|
||||
log.debug("Parsed {} html records", models.size());
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new UnsupportedOperationException("Unable to start JavaFX service", e);
|
||||
}
|
||||
catch(NoClassDefFoundError e) {
|
||||
throw new UnsupportedOperationException("JavaFX libraries not found", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||
if (options.getPixelOptions().isLegacy()) {
|
||||
printLegacy(output, options);
|
||||
} else if (options.getPixelOptions().isRasterize()) {
|
||||
//grab a snapshot of the pages for PrintImage instead of printing directly
|
||||
for(WebAppModel model : models) {
|
||||
try { images.add(WebApp.raster(model)); }
|
||||
catch(Throwable t) {
|
||||
if (model.getZoom() > 1 && t instanceof IllegalArgumentException) {
|
||||
//probably a unrecognized image loader error, try at default zoom
|
||||
try {
|
||||
log.warn("Capture failed with increased zoom, attempting with default value");
|
||||
model.setZoom(1);
|
||||
images.add(WebApp.raster(model));
|
||||
}
|
||||
catch(Throwable tt) {
|
||||
throw new PrinterException(tt.getMessage());
|
||||
}
|
||||
} else {
|
||||
throw new PrinterException(t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.print(output, options);
|
||||
} else {
|
||||
Printer fxPrinter = null;
|
||||
for(Printer p : Printer.getAllPrinters()) {
|
||||
if (p.getName().equals(output.getPrintService().getName())) {
|
||||
fxPrinter = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fxPrinter == null) {
|
||||
throw new PrinterException("Cannot find printer under the JavaFX libraries");
|
||||
}
|
||||
|
||||
PrinterJob job = PrinterJob.createPrinterJob(fxPrinter);
|
||||
|
||||
|
||||
// apply option settings
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
JobSettings settings = job.getJobSettings();
|
||||
settings.setJobName(pxlOpts.getJobName(Constants.HTML_PRINT));
|
||||
settings.setPrintQuality(PrintQuality.HIGH);
|
||||
|
||||
// If colortype is default, leave printColor blank. The system's printer settings will be used instead.
|
||||
if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) {
|
||||
settings.setPrintColor(getColor(pxlOpts));
|
||||
}
|
||||
if (pxlOpts.getDuplex() == Sides.DUPLEX || pxlOpts.getDuplex() == Sides.TWO_SIDED_LONG_EDGE) {
|
||||
settings.setPrintSides(PrintSides.DUPLEX);
|
||||
}
|
||||
if (pxlOpts.getDuplex() == Sides.TUMBLE || pxlOpts.getDuplex() == Sides.TWO_SIDED_SHORT_EDGE) {
|
||||
settings.setPrintSides(PrintSides.TUMBLE);
|
||||
}
|
||||
if (pxlOpts.getPrinterTray() != null) {
|
||||
PaperSource tray = findFXTray(fxPrinter.getPrinterAttributes().getSupportedPaperSources(), pxlOpts.getPrinterTray());
|
||||
if (tray != null) {
|
||||
settings.setPaperSource(tray);
|
||||
}
|
||||
}
|
||||
|
||||
if (pxlOpts.getDensity() > 0) {
|
||||
settings.setPrintResolution(PrintHelper.createPrintResolution((int)pxlOpts.getDensity(), (int)pxlOpts.getDensity()));
|
||||
}
|
||||
|
||||
Paper paper;
|
||||
if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) {
|
||||
double convert = 1;
|
||||
Units units = getUnits(pxlOpts);
|
||||
if (units == null) {
|
||||
convert = 10; //need to adjust from cm to mm only for DPCM sizes
|
||||
units = Units.MM;
|
||||
}
|
||||
paper = PrintHelper.createPaper("Custom", pxlOpts.getSize().getWidth() * convert, pxlOpts.getSize().getHeight() * convert, units);
|
||||
} else {
|
||||
PrintOptions.Size paperSize = options.getDefaultOptions().getPageSize();
|
||||
paper = PrintHelper.createPaper("Default", paperSize.getWidth(), paperSize.getHeight(), Units.POINT);
|
||||
}
|
||||
|
||||
PageOrientation orient = fxPrinter.getPrinterAttributes().getDefaultPageOrientation();
|
||||
if (pxlOpts.getOrientation() != null) {
|
||||
orient = getOrientation(pxlOpts);
|
||||
}
|
||||
|
||||
try {
|
||||
PageLayout layout;
|
||||
PrintOptions.Margins m = pxlOpts.getMargins();
|
||||
if (m != null) {
|
||||
//force access to the page layout constructor as the adjusted margins on small sizes are wildly inaccurate
|
||||
Constructor<PageLayout> plCon = PageLayout.class.getDeclaredConstructor(Paper.class, PageOrientation.class, double.class, double.class, double.class, double.class);
|
||||
plCon.setAccessible(true);
|
||||
|
||||
//margins defined as pnt (1/72nds)
|
||||
double asPnt = pxlOpts.getUnits().toInches() * 72;
|
||||
if (orient == PageOrientation.PORTRAIT || orient == PageOrientation.REVERSE_PORTRAIT) {
|
||||
layout = plCon.newInstance(paper, orient, m.left() * asPnt, m.right() * asPnt, m.top() * asPnt, m.bottom() * asPnt);
|
||||
} else {
|
||||
//rotate margins to match raster prints
|
||||
layout = plCon.newInstance(paper, orient, m.top() * asPnt, m.bottom() * asPnt, m.right() * asPnt, m.left() * asPnt);
|
||||
}
|
||||
} else {
|
||||
//if margins are not provided, use default paper margins
|
||||
PageLayout valid = fxPrinter.getDefaultPageLayout();
|
||||
layout = fxPrinter.createPageLayout(paper, orient, valid.getLeftMargin(), valid.getRightMargin(), valid.getTopMargin(), valid.getBottomMargin());
|
||||
}
|
||||
|
||||
//force our layout as the default to avoid default-margin exceptions on small paper sizes
|
||||
Field field = fxPrinter.getClass().getDeclaredField("defPageLayout");
|
||||
field.setAccessible(true);
|
||||
field.set(fxPrinter, layout);
|
||||
|
||||
settings.setPageLayout(layout);
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Failed to set custom layout", e);
|
||||
}
|
||||
|
||||
settings.setCopies(pxlOpts.getCopies());
|
||||
log.trace("{}", settings.toString());
|
||||
|
||||
//javaFX lies about this value, so pull from original print service
|
||||
CopiesSupported cSupport = (CopiesSupported)output.getPrintService()
|
||||
.getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], null);
|
||||
|
||||
try {
|
||||
if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) {
|
||||
for(WebAppModel model : models) {
|
||||
WebApp.print(job, model);
|
||||
}
|
||||
} else {
|
||||
settings.setCopies(1); //manually handle copies if they are not supported
|
||||
for(int i = 0; i < pxlOpts.getCopies(); i++) {
|
||||
for(WebAppModel model : models) {
|
||||
WebApp.print(job, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Throwable t) {
|
||||
job.cancelJob();
|
||||
throw new PrinterException(t.getMessage());
|
||||
}
|
||||
|
||||
//send pending prints
|
||||
job.endJob();
|
||||
}
|
||||
}
|
||||
|
||||
private void printLegacy(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
|
||||
java.awt.print.PrinterJob job = java.awt.print.PrinterJob.getPrinterJob();
|
||||
job.setPrintService(output.getPrintService());
|
||||
PageFormat page = job.getPageFormat(null);
|
||||
|
||||
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||
|
||||
//setup swing ui
|
||||
JFrame legacyFrame = new JFrame(pxlOpts.getJobName(Constants.HTML_PRINT));
|
||||
legacyFrame.setUndecorated(true);
|
||||
legacyFrame.setLayout(new FlowLayout());
|
||||
legacyFrame.setExtendedState(Frame.ICONIFIED);
|
||||
|
||||
legacyLabel = new JLabel();
|
||||
legacyLabel.setOpaque(true);
|
||||
legacyLabel.setBackground(Color.WHITE);
|
||||
legacyLabel.setBorder(null);
|
||||
legacyLabel.setDoubleBuffered(false);
|
||||
|
||||
legacyFrame.add(legacyLabel);
|
||||
|
||||
try {
|
||||
for(WebAppModel model : models) {
|
||||
if (model.isPlainText()) {
|
||||
legacyLabel.setText(cleanHtmlContent(model.getSource()));
|
||||
} else {
|
||||
try(InputStream fis = new URL(model.getSource()).openStream()) {
|
||||
String webPage = cleanHtmlContent(IOUtils.toString(fis, "UTF-8"));
|
||||
legacyLabel.setText(webPage);
|
||||
}
|
||||
}
|
||||
|
||||
legacyFrame.pack();
|
||||
legacyFrame.setVisible(true);
|
||||
|
||||
job.setPrintable(this);
|
||||
printCopies(output, pxlOpts, job, attributes);
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
throw new PrinterException(e.getMessage());
|
||||
}
|
||||
finally {
|
||||
legacyFrame.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private String cleanHtmlContent(String html) {
|
||||
return html.replaceAll("^[\\s\\S]*<(HTML|html)\\b.*?>", "<html>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
|
||||
if (legacyLabel == null) {
|
||||
return super.print(graphics, pageFormat, pageIndex);
|
||||
} else {
|
||||
if (graphics == null) { throw new PrinterException("No graphics specified"); }
|
||||
if (pageFormat == null) { throw new PrinterException("No page format specified"); }
|
||||
|
||||
if (pageIndex + 1 > models.size()) {
|
||||
return NO_SUCH_PAGE;
|
||||
}
|
||||
log.trace("Requested page {} for printing", pageIndex);
|
||||
|
||||
Graphics2D graphics2D = (Graphics2D)graphics;
|
||||
graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||
graphics2D.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
|
||||
graphics2D.scale(pageFormat.getImageableWidth() / pageFormat.getWidth(), pageFormat.getImageableHeight() / pageFormat.getHeight());
|
||||
legacyLabel.paint(graphics2D);
|
||||
|
||||
return PAGE_EXISTS;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
super.cleanup();
|
||||
|
||||
models.clear();
|
||||
legacyLabel = null;
|
||||
}
|
||||
|
||||
public static Units getUnits(PrintOptions.Pixel opts) {
|
||||
switch(opts.getUnits()) {
|
||||
case INCH:
|
||||
return Units.INCH;
|
||||
case MM:
|
||||
return Units.MM;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static PageOrientation getOrientation(PrintOptions.Pixel opts) {
|
||||
switch(opts.getOrientation()) {
|
||||
case LANDSCAPE:
|
||||
return PageOrientation.LANDSCAPE;
|
||||
case REVERSE_LANDSCAPE:
|
||||
return PageOrientation.REVERSE_LANDSCAPE;
|
||||
case REVERSE_PORTRAIT:
|
||||
return PageOrientation.REVERSE_PORTRAIT;
|
||||
default:
|
||||
return PageOrientation.PORTRAIT;
|
||||
}
|
||||
}
|
||||
|
||||
public static PrintColor getColor(PrintOptions.Pixel opts) {
|
||||
switch(opts.getColorType()) {
|
||||
case COLOR:
|
||||
return PrintColor.COLOR;
|
||||
default:
|
||||
return PrintColor.MONOCHROME;
|
||||
}
|
||||
}
|
||||
}
|
||||
341
old code/tray/src/qz/printer/action/PrintImage.java
Executable file
341
old code/tray/src/qz/printer/action/PrintImage.java
Executable file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*
|
||||
*/
|
||||
package qz.printer.action;
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.utils.ConnectionUtilities;
|
||||
import qz.utils.PrintingUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.ResolutionSyntax;
|
||||
import javax.print.attribute.standard.OrientationRequested;
|
||||
import javax.print.attribute.standard.PrinterResolution;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Printable;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro, Anton Mezerny
|
||||
*/
|
||||
public class PrintImage extends PrintPixel implements PrintProcessor, Printable {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintImage.class);
|
||||
|
||||
protected List<BufferedImage> images;
|
||||
|
||||
protected double dpiScale = 1;
|
||||
protected boolean scaleImage = false;
|
||||
protected Object dithering = RenderingHints.VALUE_DITHER_DEFAULT;
|
||||
protected Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
|
||||
protected double imageRotation = 0;
|
||||
protected boolean manualReverse = false;
|
||||
|
||||
public PrintImage() {
|
||||
images = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.IMAGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
dpiScale = (options.getPixelOptions().getDensity() * options.getPixelOptions().getUnits().as1Inch()) / 72.0;
|
||||
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.getJSONObject(i);
|
||||
|
||||
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||
|
||||
try {
|
||||
BufferedImage bi;
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
// There's really no such thing as a 'PLAIN' image, assume it's a URL
|
||||
case FILE:
|
||||
bi = ImageIO.read(ConnectionUtilities.getInputStream(data.getString("data"), true));
|
||||
break;
|
||||
default:
|
||||
bi = ImageIO.read(new ByteArrayInputStream(flavor.read(data.getString("data"))));
|
||||
}
|
||||
|
||||
images.add(bi);
|
||||
}
|
||||
catch(IIOException e) {
|
||||
if (e.getCause() != null && e.getCause() instanceof FileNotFoundException) {
|
||||
throw new UnsupportedOperationException("Image file specified could not be found.", e);
|
||||
} else {
|
||||
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as an image", flavor, data.getString("data")), e);
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as an image: %s", flavor, data.getString("data"), e.getLocalizedMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Parsed {} images for printing", images.size());
|
||||
}
|
||||
|
||||
private List<BufferedImage> breakupOverPages(BufferedImage img, PageFormat page, PrintRequestAttributeSet attributes) {
|
||||
List<BufferedImage> splits = new ArrayList<>();
|
||||
|
||||
Rectangle printBounds = new Rectangle(0, 0, (int)page.getImageableWidth(), (int)page.getImageableHeight());
|
||||
PrinterResolution res = (PrinterResolution)attributes.get(PrinterResolution.class);
|
||||
float dpi = res.getFeedResolution(1) / (float)ResolutionSyntax.DPI;
|
||||
float cdpi = res.getCrossFeedResolution(1) / (float)ResolutionSyntax.DPI;
|
||||
|
||||
//printing uses 72dpi, convert so we can check split size correctly
|
||||
int useWidth = (int)((img.getWidth() / cdpi) * 72);
|
||||
int useHeight = (int)((img.getHeight() / dpi) * 72);
|
||||
|
||||
int columnsNeed = (int)Math.ceil(useWidth / page.getImageableWidth());
|
||||
int rowsNeed = (int)Math.ceil(useHeight / page.getImageableHeight());
|
||||
|
||||
if (columnsNeed == 1 && rowsNeed == 1) {
|
||||
log.trace("Unscaled image does not need spit");
|
||||
splits.add(img);
|
||||
} else {
|
||||
log.trace("Image to be printed across {} pages", columnsNeed * rowsNeed);
|
||||
//allows us to split the image at the actual dpi instead of 72
|
||||
float upscale = dpi / 72f;
|
||||
float c_upscale = cdpi / 72f;
|
||||
|
||||
for(int row = 0; row < rowsNeed; row++) {
|
||||
for(int col = 0; col < columnsNeed; col++) {
|
||||
Rectangle clip = new Rectangle((col * (int)(printBounds.width * c_upscale)), (row * (int)(printBounds.height * upscale)),
|
||||
(int)(printBounds.width * c_upscale), (int)(printBounds.height * upscale));
|
||||
|
||||
if (clip.x + clip.width > img.getWidth()) { clip.width = img.getWidth() - clip.x; }
|
||||
if (clip.y + clip.height > img.getHeight()) { clip.height = img.getHeight() - clip.y; }
|
||||
|
||||
splits.add(img.getSubimage(clip.x, clip.y, clip.width, clip.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return splits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||
if (images.isEmpty()) {
|
||||
log.warn("Nothing to print");
|
||||
return;
|
||||
}
|
||||
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(output.getPrintService());
|
||||
PageFormat page = job.getPageFormat(null);
|
||||
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||
|
||||
scaleImage = pxlOpts.isScaleContent();
|
||||
dithering = pxlOpts.getDithering();
|
||||
interpolation = pxlOpts.getInterpolation();
|
||||
imageRotation = pxlOpts.getRotation();
|
||||
|
||||
//reverse fix for OSX
|
||||
if (SystemUtilities.isMac() && pxlOpts.getOrientation() != null
|
||||
&& pxlOpts.getOrientation().getAsOrientRequested() == OrientationRequested.REVERSE_LANDSCAPE) {
|
||||
imageRotation += 180;
|
||||
manualReverse = true;
|
||||
}
|
||||
|
||||
if (!scaleImage) {
|
||||
//breakup large images to print across pages as needed
|
||||
List<BufferedImage> split = new ArrayList<>();
|
||||
for(BufferedImage bi : images) {
|
||||
split.addAll(breakupOverPages(bi, page, attributes));
|
||||
}
|
||||
images = split;
|
||||
}
|
||||
|
||||
job.setJobName(pxlOpts.getJobName(Constants.IMAGE_PRINT));
|
||||
job.setPrintable(this, job.validatePage(page));
|
||||
|
||||
printCopies(output, pxlOpts, job, attributes);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
|
||||
if (graphics == null) { throw new PrinterException("No graphics specified"); }
|
||||
if (pageFormat == null) { throw new PrinterException("No page format specified"); }
|
||||
|
||||
if (pageIndex + 1 > images.size()) {
|
||||
return NO_SUCH_PAGE;
|
||||
}
|
||||
log.trace("Requested page {} for printing", pageIndex);
|
||||
|
||||
if ("sun.print.PeekGraphics".equals(graphics.getClass().getCanonicalName())) {
|
||||
//java uses class only to query if a page needs printed - save memory/time by short circuiting
|
||||
return PAGE_EXISTS;
|
||||
}
|
||||
|
||||
|
||||
//allows pages view to rotate in different orientations
|
||||
graphics.drawString(" ", 0, 0);
|
||||
|
||||
BufferedImage imgToPrint = fixColorModel(images.get(pageIndex));
|
||||
if (imageRotation % 360 != 0) {
|
||||
imgToPrint = rotate(imgToPrint, imageRotation);
|
||||
}
|
||||
|
||||
// apply image scaling
|
||||
double boundW = pageFormat.getImageableWidth();
|
||||
double boundH = pageFormat.getImageableHeight();
|
||||
|
||||
double imgW = imgToPrint.getWidth() / dpiScale;
|
||||
double imgH = imgToPrint.getHeight() / dpiScale;
|
||||
|
||||
if (scaleImage) {
|
||||
imgToPrint = scale(imgToPrint, pageFormat);
|
||||
|
||||
// adjust dimensions to smallest edge, keeping size ratio
|
||||
if (((float)imgToPrint.getWidth() / (float)imgToPrint.getHeight()) >= (boundW / boundH)) {
|
||||
imgW = boundW;
|
||||
imgH = (imgToPrint.getHeight() / (imgToPrint.getWidth() / boundW));
|
||||
} else {
|
||||
imgW = (imgToPrint.getWidth() / (imgToPrint.getHeight() / boundH));
|
||||
imgH = boundH;
|
||||
}
|
||||
}
|
||||
|
||||
double boundX = pageFormat.getImageableX();
|
||||
double boundY = pageFormat.getImageableY();
|
||||
|
||||
log.debug("Paper area: {},{}:{},{}", (int)boundX, (int)boundY, (int)boundW, (int)boundH);
|
||||
log.trace("Image size: {},{}", imgW, imgH);
|
||||
|
||||
// Now we perform our rendering
|
||||
Graphics2D graphics2D = (Graphics2D)graphics;
|
||||
graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||
log.trace("{}", graphics2D.getRenderingHints());
|
||||
|
||||
log.debug("Memory: {}m/{}m", (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576, Runtime.getRuntime().maxMemory() / 1048576);
|
||||
|
||||
if (!manualReverse) {
|
||||
graphics2D.drawImage(imgToPrint, (int)boundX, (int)boundY, (int)(boundX + imgW), (int)(boundY + imgH),
|
||||
0, 0, imgToPrint.getWidth(), imgToPrint.getHeight(), null);
|
||||
} else {
|
||||
graphics2D.drawImage(imgToPrint, (int)(boundW + boundX - imgW), (int)(boundH + boundY - imgH), (int)(boundW + boundX), (int)(boundH + boundY),
|
||||
0, 0, imgToPrint.getWidth(), imgToPrint.getHeight(), null);
|
||||
}
|
||||
|
||||
// Valid page
|
||||
return PAGE_EXISTS;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param image
|
||||
* @param pageFormat
|
||||
* @return
|
||||
*/
|
||||
private BufferedImage scale(BufferedImage image, PageFormat pageFormat) {
|
||||
//scale up to print density (using less of a stretch if image is already larger than page)
|
||||
double upScale = dpiScale * Math.min((pageFormat.getImageableWidth() / image.getWidth()), (pageFormat.getImageableHeight() / image.getHeight()));
|
||||
if (upScale > dpiScale) { upScale = dpiScale; } else if (upScale < 1) { upScale = 1; }
|
||||
|
||||
if (upScale > 1) {
|
||||
log.debug("Scaling image up by x{}", upScale);
|
||||
|
||||
BufferedImage scaled = new BufferedImage((int)(image.getWidth() * upScale), (int)(image.getHeight() * upScale), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = scaled.createGraphics();
|
||||
g2d.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||
g2d.drawImage(image, 0, 0, (int)(image.getWidth() * upScale), (int)(image.getHeight() * upScale), null);
|
||||
g2d.dispose();
|
||||
|
||||
return scaled;
|
||||
} else {
|
||||
log.debug("No need to upscale image");
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates {@code image} by the specified {@code angle}.
|
||||
*
|
||||
* @param image BufferedImage to rotate
|
||||
* @param angle Rotation angle in degrees
|
||||
* @return Rotated image data
|
||||
*/
|
||||
public static BufferedImage rotate(BufferedImage image, double angle, Object dithering, Object interpolation) {
|
||||
double rads = Math.toRadians(angle);
|
||||
double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads));
|
||||
|
||||
int sWidth = image.getWidth(), sHeight = image.getHeight();
|
||||
int eWidth = (int)Math.floor((sWidth * cos) + (sHeight * sin)), eHeight = (int)Math.floor((sHeight * cos) + (sWidth * sin));
|
||||
|
||||
BufferedImage result;
|
||||
if(!GraphicsEnvironment.isHeadless()) {
|
||||
GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0].getDefaultConfiguration();
|
||||
result = gc.createCompatibleImage(eWidth, eHeight, Transparency.TRANSLUCENT);
|
||||
} else {
|
||||
result = new BufferedImage(eWidth, eHeight, Transparency.TRANSLUCENT);
|
||||
}
|
||||
|
||||
Graphics2D g2d = result.createGraphics();
|
||||
g2d.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||
g2d.translate((eWidth - sWidth) / 2, (eHeight - sHeight) / 2);
|
||||
g2d.rotate(rads, sWidth / 2, sHeight / 2);
|
||||
|
||||
if (angle % 90 == 0 || interpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) {
|
||||
g2d.drawRenderedImage(image, null);
|
||||
} else {
|
||||
g2d.setPaint(new TexturePaint(image, new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight())));
|
||||
g2d.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
}
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private BufferedImage rotate(BufferedImage image, double angle) {
|
||||
return rotate(image, angle, dithering, interpolation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
images.clear();
|
||||
|
||||
dpiScale = 1.0;
|
||||
scaleImage = false;
|
||||
imageRotation = 0;
|
||||
dithering = RenderingHints.VALUE_DITHER_DEFAULT;
|
||||
interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
|
||||
manualReverse = false;
|
||||
}
|
||||
|
||||
}
|
||||
326
old code/tray/src/qz/printer/action/PrintPDF.java
Executable file
326
old code/tray/src/qz/printer/action/PrintPDF.java
Executable file
@@ -0,0 +1,326 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.pdfbox.io.IOUtils;
|
||||
import org.apache.pdfbox.multipdf.Splitter;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.printing.Scaling;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.common.Constants;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.printer.action.pdf.BookBundle;
|
||||
import qz.printer.action.pdf.PDFWrapper;
|
||||
import qz.utils.ConnectionUtilities;
|
||||
import qz.utils.PrintingUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.Media;
|
||||
import javax.print.attribute.standard.MediaPrintableArea;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Paper;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class PrintPDF extends PrintPixel implements PrintProcessor {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintPDF.class);
|
||||
|
||||
private List<PDDocument> originals;
|
||||
private List<PDDocument> printables;
|
||||
private Splitter splitter = new Splitter();
|
||||
|
||||
private double docWidth = 0;
|
||||
private double docHeight = 0;
|
||||
private boolean ignoreTransparency = false;
|
||||
private boolean altFontRendering = false;
|
||||
|
||||
|
||||
public PrintPDF() {
|
||||
originals = new ArrayList<>();
|
||||
printables = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.PDF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
double convert = 72.0 / pxlOpts.getUnits().as1Inch();
|
||||
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.getJSONObject(i);
|
||||
HashSet<Integer> pagesToPrint = new HashSet<>();
|
||||
|
||||
if (!data.isNull("options")) {
|
||||
JSONObject dataOpt = data.getJSONObject("options");
|
||||
|
||||
if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) {
|
||||
docWidth = dataOpt.optDouble("pageWidth") * convert;
|
||||
}
|
||||
if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) {
|
||||
docHeight = dataOpt.optDouble("pageHeight") * convert;
|
||||
}
|
||||
|
||||
ignoreTransparency = dataOpt.optBoolean("ignoreTransparency", false);
|
||||
altFontRendering = dataOpt.optBoolean("altFontRendering", false);
|
||||
|
||||
if (!dataOpt.isNull("pageRanges")) {
|
||||
String[] ranges = dataOpt.optString("pageRanges", "").split(",");
|
||||
for(String range : ranges) {
|
||||
range = range.trim();
|
||||
if(range.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] period = range.split("-");
|
||||
|
||||
try {
|
||||
int start = Integer.parseInt(period[0]);
|
||||
pagesToPrint.add(start);
|
||||
|
||||
if (period.length > 1) {
|
||||
int end = Integer.parseInt(period[period.length - 1]);
|
||||
pagesToPrint.addAll(IntStream.rangeClosed(start, end).boxed().collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
catch(NumberFormatException nfe) {
|
||||
log.warn("Unable to parse page range {}.", range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||
|
||||
try {
|
||||
PDDocument doc;
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
|
||||
case FILE:
|
||||
doc = PDDocument.load(ConnectionUtilities.getInputStream(data.getString("data"), true));
|
||||
break;
|
||||
default:
|
||||
doc = PDDocument.load(new ByteArrayInputStream(flavor.read(data.getString("data"))));
|
||||
}
|
||||
|
||||
if (pxlOpts.getBounds() != null) {
|
||||
PrintOptions.Bounds bnd = pxlOpts.getBounds();
|
||||
|
||||
for(PDPage page : doc.getPages()) {
|
||||
PDRectangle box = new PDRectangle(
|
||||
(float)(bnd.getX() * convert),
|
||||
page.getMediaBox().getUpperRightY() - (float)((bnd.getHeight() + bnd.getY()) * convert),
|
||||
(float)(bnd.getWidth() * convert),
|
||||
(float)(bnd.getHeight() * convert));
|
||||
page.setMediaBox(box);
|
||||
}
|
||||
}
|
||||
|
||||
if (pagesToPrint.isEmpty()) {
|
||||
pagesToPrint.addAll(IntStream.rangeClosed(1, doc.getNumberOfPages()).boxed().collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
originals.add(doc);
|
||||
|
||||
List<PDDocument> splitPages = splitter.split(doc);
|
||||
originals.addAll(splitPages); //ensures non-ranged page will still get closed
|
||||
|
||||
for(int pg = 0; pg < splitPages.size(); pg++) {
|
||||
if (pagesToPrint.contains(pg + 1)) { //ranges are 1-indexed
|
||||
printables.add(splitPages.get(pg));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(FileNotFoundException e) {
|
||||
throw new UnsupportedOperationException("PDF file specified could not be found.", e);
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as a PDF file: %s", flavor, data.getString("data"), e.getLocalizedMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Parsed {} files for printing", printables.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintRequestAttributeSet applyDefaultSettings(PrintOptions.Pixel pxlOpts, PageFormat page, Media[] supported) {
|
||||
if (pxlOpts.getOrientation() != null) {
|
||||
//page orient does not set properly on pdfs with orientation requested attribute
|
||||
page.setOrientation(pxlOpts.getOrientation().getAsOrientFormat());
|
||||
}
|
||||
|
||||
return super.applyDefaultSettings(pxlOpts, page, supported);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||
if (printables.isEmpty()) {
|
||||
log.warn("Nothing to print");
|
||||
return;
|
||||
}
|
||||
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(output.getPrintService());
|
||||
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
Scaling scale = (pxlOpts.isScaleContent()? Scaling.SCALE_TO_FIT:Scaling.ACTUAL_SIZE);
|
||||
|
||||
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, job.getPageFormat(null), (Media[])output.getPrintService().getSupportedAttributeValues(Media.class, null, null));
|
||||
|
||||
// Disable attributes per https://github.com/qzind/tray/issues/174
|
||||
if (SystemUtilities.isMac() && Constants.JAVA_VERSION.compareWithBuildsTo(Version.valueOf("1.8.0+202")) < 0) {
|
||||
log.warn("MacOS and Java < 1.8.0u202 cannot use attributes with PDF prints, disabling");
|
||||
attributes.clear();
|
||||
}
|
||||
|
||||
RenderingHints hints = new RenderingHints(buildRenderingHints(pxlOpts.getDithering(), pxlOpts.getInterpolation()));
|
||||
double useDensity = pxlOpts.getDensity();
|
||||
|
||||
if (!pxlOpts.isRasterize()) {
|
||||
if (pxlOpts.getDensity() > 0) {
|
||||
// clear density for vector prints (applied via print attributes instead)
|
||||
useDensity = 0;
|
||||
} else if (SystemUtilities.isMac() && Constants.JAVA_VERSION.compareWithBuildsTo(Version.valueOf("1.8.0+121")) < 0) {
|
||||
log.warn("OSX systems cannot print vector PDF's, forcing raster to prevent crash.");
|
||||
useDensity = options.getDefaultOptions().getDensity();
|
||||
}
|
||||
}
|
||||
|
||||
BookBundle bundle = new BookBundle();
|
||||
|
||||
for(PDDocument doc : printables) {
|
||||
PageFormat page = job.getPageFormat(null);
|
||||
applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||
|
||||
//trick pdfbox into an alternate doc size if specified
|
||||
if (docWidth > 0 || docHeight > 0) {
|
||||
Paper paper = page.getPaper();
|
||||
|
||||
if (docWidth <= 0) { docWidth = page.getImageableWidth(); }
|
||||
if (docHeight <= 0) { docHeight = page.getImageableHeight(); }
|
||||
|
||||
paper.setImageableArea(paper.getImageableX(), paper.getImageableY(), docWidth, docHeight);
|
||||
page.setPaper(paper);
|
||||
|
||||
scale = Scaling.SCALE_TO_FIT; //to get custom size we need to force scaling
|
||||
|
||||
//pdf uses imageable area from Paper, so this can be safely removed
|
||||
attributes.remove(MediaPrintableArea.class);
|
||||
}
|
||||
|
||||
for(PDPage pd : doc.getPages()) {
|
||||
if (pxlOpts.getRotation() % 360 != 0) {
|
||||
rotatePage(doc, pd, pxlOpts.getRotation());
|
||||
}
|
||||
|
||||
if (pxlOpts.getOrientation() == null) {
|
||||
PDRectangle bounds = pd.getBBox();
|
||||
if ((page.getImageableHeight() > page.getImageableWidth() && bounds.getWidth() > bounds.getHeight()) ^ (pd.getRotation() / 90) % 2 == 1) {
|
||||
log.info("Adjusting orientation to print landscape PDF source");
|
||||
page.setOrientation(PrintOptions.Orientation.LANDSCAPE.getAsOrientFormat());
|
||||
}
|
||||
} else if (pxlOpts.getOrientation() != PrintOptions.Orientation.PORTRAIT) {
|
||||
//flip imageable area dimensions when in landscape
|
||||
Paper repap = page.getPaper();
|
||||
repap.setImageableArea(repap.getImageableX(), repap.getImageableY(), repap.getImageableHeight(), repap.getImageableWidth());
|
||||
page.setPaper(repap);
|
||||
|
||||
//reverse fix for OSX
|
||||
if (SystemUtilities.isMac() && pxlOpts.getOrientation() == PrintOptions.Orientation.REVERSE_LANDSCAPE) {
|
||||
pd.setRotation(pd.getRotation() + 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PDFWrapper wrapper = new PDFWrapper(doc, scale, false, ignoreTransparency, altFontRendering,
|
||||
(float)(useDensity * pxlOpts.getUnits().as1Inch()),
|
||||
false, pxlOpts.getOrientation(), hints);
|
||||
|
||||
bundle.append(wrapper, page, doc.getNumberOfPages());
|
||||
}
|
||||
|
||||
if (pxlOpts.getSpoolSize() > 0 && bundle.getNumberOfPages() > pxlOpts.getSpoolSize()) {
|
||||
int jobNum = 1;
|
||||
int offset = 0;
|
||||
while(offset < bundle.getNumberOfPages()) {
|
||||
job.setJobName(pxlOpts.getJobName(Constants.PDF_PRINT) + "-" + jobNum++);
|
||||
job.setPageable(bundle.wrapAndPresent(offset, pxlOpts.getSpoolSize()));
|
||||
|
||||
printCopies(output, pxlOpts, job, attributes);
|
||||
|
||||
offset += pxlOpts.getSpoolSize();
|
||||
}
|
||||
} else {
|
||||
job.setJobName(pxlOpts.getJobName(Constants.PDF_PRINT));
|
||||
job.setPageable(bundle.wrapAndPresent());
|
||||
|
||||
printCopies(output, pxlOpts, job, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
private void rotatePage(PDDocument doc, PDPage page, double rotation) {
|
||||
try {
|
||||
//copy page to object for manipulation
|
||||
PDFormXObject xobject = new PDFormXObject(doc);
|
||||
InputStream src = page.getContents();
|
||||
OutputStream dest = xobject.getStream().createOutputStream();
|
||||
|
||||
try { IOUtils.copy(src, dest); }
|
||||
finally {
|
||||
IOUtils.closeQuietly(src);
|
||||
IOUtils.closeQuietly(dest);
|
||||
}
|
||||
|
||||
xobject.setResources(page.getResources());
|
||||
xobject.setBBox(page.getBBox());
|
||||
|
||||
//draw our object at a rotated angle
|
||||
AffineTransform transform = new AffineTransform();
|
||||
transform.rotate(Math.toRadians(360 - rotation), xobject.getBBox().getWidth() / 2.0, xobject.getBBox().getHeight() / 2.0);
|
||||
xobject.setMatrix(transform);
|
||||
|
||||
PDPageContentStream stream = new PDPageContentStream(doc, page);
|
||||
stream.drawForm(xobject);
|
||||
stream.close();
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Failed to rotate PDF page for printing");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
for(PDDocument doc : originals) {
|
||||
try { doc.close(); } catch(IOException ignore) {}
|
||||
}
|
||||
|
||||
originals.clear();
|
||||
printables.clear();
|
||||
|
||||
docWidth = 0;
|
||||
docHeight = 0;
|
||||
ignoreTransparency = false;
|
||||
altFontRendering = false;
|
||||
}
|
||||
}
|
||||
231
old code/tray/src/qz/printer/action/PrintPixel.java
Executable file
231
old code/tray/src/qz/printer/action/PrintPixel.java
Executable file
@@ -0,0 +1,231 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import javafx.print.PaperSource;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.IndexColorModel;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Paper;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class PrintPixel {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintPixel.class);
|
||||
|
||||
private static final List<Integer> MAC_BAD_IMAGE_TYPES = Arrays.asList(BufferedImage.TYPE_BYTE_BINARY, BufferedImage.TYPE_CUSTOM);
|
||||
|
||||
|
||||
protected PrintRequestAttributeSet applyDefaultSettings(PrintOptions.Pixel pxlOpts, PageFormat page, Media[] supported) {
|
||||
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||
|
||||
//apply general attributes
|
||||
// If colortype is default, leave printColor blank. The system's printer settings will be used instead.
|
||||
if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) {
|
||||
attributes.add(pxlOpts.getColorType().getAsChromaticity());
|
||||
}
|
||||
attributes.add(pxlOpts.getDuplex());
|
||||
if (pxlOpts.getOrientation() != null) {
|
||||
attributes.add(pxlOpts.getOrientation().getAsOrientRequested());
|
||||
}
|
||||
if (pxlOpts.getPrinterTray() != null && !pxlOpts.getPrinterTray().isEmpty()) {
|
||||
Media tray = findMediaTray(supported, pxlOpts.getPrinterTray());
|
||||
if (tray != null) {
|
||||
attributes.add(tray);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO - set paper thickness
|
||||
|
||||
|
||||
// Java prints using inches at 72dpi
|
||||
final float CONVERT = pxlOpts.getUnits().toInches() * 72f;
|
||||
|
||||
log.trace("DPI: [{}x{}]\tCNV: {}", pxlOpts.getDensity(), pxlOpts.getCrossDensity(), CONVERT);
|
||||
if (pxlOpts.getDensity() > 0) {
|
||||
double cross = pxlOpts.getCrossDensity();
|
||||
if (cross == 0) { cross = pxlOpts.getDensity(); }
|
||||
|
||||
attributes.add(new PrinterResolution((int)cross, (int)pxlOpts.getDensity(), pxlOpts.getUnits().getDPIUnits()));
|
||||
}
|
||||
|
||||
//apply sizing and margins
|
||||
Paper paper = page.getPaper();
|
||||
|
||||
float pageX = 0f;
|
||||
float pageY = 0f;
|
||||
float pageW = (float)page.getWidth() / CONVERT;
|
||||
float pageH = (float)page.getHeight() / CONVERT;
|
||||
|
||||
//page size
|
||||
if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) {
|
||||
pageW = (float)pxlOpts.getSize().getWidth();
|
||||
pageH = (float)pxlOpts.getSize().getHeight();
|
||||
|
||||
paper.setSize(pageW * CONVERT, pageH * CONVERT);
|
||||
}
|
||||
|
||||
//margins
|
||||
if (pxlOpts.getMargins() != null) {
|
||||
pageX += pxlOpts.getMargins().left();
|
||||
pageY += pxlOpts.getMargins().top();
|
||||
pageW -= (pxlOpts.getMargins().right() + pxlOpts.getMargins().left());
|
||||
pageH -= (pxlOpts.getMargins().bottom() + pxlOpts.getMargins().top());
|
||||
}
|
||||
|
||||
log.trace("Drawable area: {},{}:{},{}", pageX, pageY, pageW, pageH);
|
||||
if (pageW > 0 && pageH > 0) {
|
||||
attributes.add(new MediaPrintableArea(pageX, pageY, pageW, pageH, pxlOpts.getUnits().getMediaSizeUnits()));
|
||||
paper.setImageableArea(pageX * CONVERT, pageY * CONVERT, pageW * CONVERT, pageH * CONVERT);
|
||||
page.setPaper(paper);
|
||||
} else {
|
||||
log.warn("Could not apply custom size, using printer default");
|
||||
attributes.add(new MediaPrintableArea(0, 0, (float)page.getWidth() / 72f, (float)page.getHeight() / 72f, PrintOptions.Unit.INCH.getMediaSizeUnits()));
|
||||
}
|
||||
|
||||
log.trace("{}", Arrays.toString(attributes.toArray()));
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
|
||||
protected void printCopies(PrintOutput output, PrintOptions.Pixel pxlOpts, PrinterJob job, PrintRequestAttributeSet attributes) throws PrinterException {
|
||||
log.info("Starting printing ({} copies)", pxlOpts.getCopies());
|
||||
|
||||
PrinterResolution rUsing = (PrinterResolution)attributes.get(PrinterResolution.class);
|
||||
if (rUsing != null) {
|
||||
List<PrinterResolution> rSupport = output.getNativePrinter().getResolutions();
|
||||
if (!rSupport.isEmpty()) {
|
||||
if (!rSupport.contains(rUsing)) {
|
||||
log.warn("Not using a supported DPI for printing");
|
||||
log.debug("Available DPI: {}", ArrayUtils.toString(rSupport));
|
||||
}
|
||||
} else {
|
||||
log.warn("Supported printer densities not found");
|
||||
}
|
||||
}
|
||||
|
||||
CopiesSupported cSupport = (CopiesSupported)output.getPrintService()
|
||||
.getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], attributes);
|
||||
|
||||
if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) {
|
||||
attributes.add(new Copies(pxlOpts.getCopies()));
|
||||
job.print(attributes);
|
||||
} else {
|
||||
for(int i = 0; i < pxlOpts.getCopies(); i++) {
|
||||
job.print(attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: Temporary fix for OS X 10.10 hard crash.
|
||||
* See https://github.com/qzind/qz-print/issues/75
|
||||
*/
|
||||
protected BufferedImage fixColorModel(BufferedImage imgToPrint) {
|
||||
if (SystemUtilities.isMac()) {
|
||||
if (MAC_BAD_IMAGE_TYPES.contains(imgToPrint.getType())) {
|
||||
BufferedImage sanitizedImage;
|
||||
ColorModel cm = imgToPrint.getColorModel();
|
||||
|
||||
if (cm instanceof IndexColorModel) {
|
||||
log.info("Image converted to 256 colors for OSX 10.10 Workaround");
|
||||
sanitizedImage = new BufferedImage(imgToPrint.getWidth(), imgToPrint.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel)cm);
|
||||
} else {
|
||||
log.info("Image converted to ARGB for OSX 10.10 Workaround");
|
||||
sanitizedImage = new BufferedImage(imgToPrint.getWidth(), imgToPrint.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
}
|
||||
|
||||
sanitizedImage.createGraphics().drawImage(imgToPrint, 0, 0, null);
|
||||
imgToPrint = sanitizedImage;
|
||||
}
|
||||
}
|
||||
|
||||
return imgToPrint;
|
||||
}
|
||||
|
||||
protected static Map<RenderingHints.Key,Object> buildRenderingHints(Object dithering, Object interpolation) {
|
||||
Map<RenderingHints.Key,Object> rhMap = new HashMap<>();
|
||||
rhMap.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||
rhMap.put(RenderingHints.KEY_DITHERING, dithering);
|
||||
rhMap.put(RenderingHints.KEY_INTERPOLATION, interpolation);
|
||||
rhMap.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
rhMap.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
rhMap.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR.equals(interpolation)) {
|
||||
rhMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
|
||||
rhMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
|
||||
} else {
|
||||
rhMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
rhMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
}
|
||||
|
||||
|
||||
return rhMap;
|
||||
}
|
||||
|
||||
protected Media findMediaTray(Media[] supportedMedia, String traySelection) {
|
||||
HashMap<String,Media> mediaTrays = new HashMap<>();
|
||||
for(Media m : supportedMedia) {
|
||||
if (m instanceof MediaTray) {
|
||||
mediaTrays.put(m.toString(), m);
|
||||
}
|
||||
}
|
||||
|
||||
String tray = findTray(mediaTrays.keySet(), traySelection);
|
||||
return mediaTrays.get(tray);
|
||||
}
|
||||
|
||||
protected PaperSource findFXTray(Set<PaperSource> paperSources, String traySelection) {
|
||||
Map<String,PaperSource> fxTrays = paperSources.stream().collect(Collectors.toMap(PaperSource::getName, Function.identity()));
|
||||
|
||||
String tray = findTray(fxTrays.keySet(), traySelection);
|
||||
return fxTrays.get(tray);
|
||||
}
|
||||
|
||||
private String findTray(Set<String> trayOptions, String traySelection) {
|
||||
Pattern exactPattern = Pattern.compile("\\b" + Pattern.quote(traySelection) + "\\b", Pattern.CASE_INSENSITIVE);
|
||||
Pattern fuzzyPattern = Pattern.compile("\\b.*?[" + Pattern.quote(traySelection) + "]+.*?\\b", Pattern.CASE_INSENSITIVE);
|
||||
String bestFit = null;
|
||||
Integer fuzzyFitDelta = null;
|
||||
|
||||
for(String option : trayOptions) {
|
||||
Matcher exactly = exactPattern.matcher(option.trim());
|
||||
Matcher fuzzily = fuzzyPattern.matcher(option.trim());
|
||||
|
||||
if (exactly.find()) {
|
||||
bestFit = option;
|
||||
break;
|
||||
}
|
||||
|
||||
while(fuzzily.find()) {
|
||||
//look for as close to exact match as possible
|
||||
int delta = Math.abs(fuzzily.group().length() - traySelection.length());
|
||||
if (fuzzyFitDelta == null || delta < fuzzyFitDelta) {
|
||||
fuzzyFitDelta = delta;
|
||||
bestFit = option;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestFit;
|
||||
}
|
||||
|
||||
}
|
||||
39
old code/tray/src/qz/printer/action/PrintProcessor.java
Executable file
39
old code/tray/src/qz/printer/action/PrintProcessor.java
Executable file
@@ -0,0 +1,39 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.utils.PrintingUtilities;
|
||||
|
||||
import javax.print.PrintException;
|
||||
import java.awt.print.PrinterException;
|
||||
|
||||
public interface PrintProcessor {
|
||||
|
||||
|
||||
PrintingUtilities.Format getFormat();
|
||||
|
||||
/**
|
||||
* Used to parse information passed from the web API for printing.
|
||||
*
|
||||
* @param printData JSON Array of printer data
|
||||
* @param options Printing options to use for the print job
|
||||
*/
|
||||
void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException;
|
||||
|
||||
|
||||
/**
|
||||
* Used to setup and send documents to the specified printing {@code service}.
|
||||
*
|
||||
* @param output Destination used for printing
|
||||
* @param options Printing options to use for the print job
|
||||
*/
|
||||
void print(PrintOutput output, PrintOptions options) throws PrintException, PrinterException;
|
||||
|
||||
/**
|
||||
* Reset a processor back to it's initial state.
|
||||
*/
|
||||
void cleanup();
|
||||
|
||||
}
|
||||
530
old code/tray/src/qz/printer/action/PrintRaw.java
Executable file
530
old code/tray/src/qz/printer/action/PrintRaw.java
Executable file
@@ -0,0 +1,530 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package qz.printer.action;
|
||||
|
||||
import com.ibm.icu.text.ArabicShapingException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.ByteArrayBuilder;
|
||||
import qz.common.Constants;
|
||||
import qz.exception.NullCommandException;
|
||||
import qz.exception.NullPrintServiceException;
|
||||
import qz.printer.action.raw.ImageWrapper;
|
||||
import qz.printer.action.raw.LanguageType;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.printer.action.html.WebApp;
|
||||
import qz.printer.action.html.WebAppModel;
|
||||
import qz.printer.info.NativePrinter;
|
||||
import qz.printer.status.CupsUtils;
|
||||
import qz.utils.*;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.print.*;
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.JobName;
|
||||
import javax.print.event.PrintJobEvent;
|
||||
import javax.print.event.PrintJobListener;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Sends raw data to the printer, overriding your operating system's print
|
||||
* driver. Most useful for printers such as zebra card or barcode printers.
|
||||
*
|
||||
* @author A. Tres Finocchiaro
|
||||
*/
|
||||
public class PrintRaw implements PrintProcessor {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintRaw.class);
|
||||
|
||||
private ByteArrayBuilder commands;
|
||||
|
||||
private String destEncoding = null;
|
||||
|
||||
private enum Backend {
|
||||
CUPS_RSS,
|
||||
CUPS_LPR,
|
||||
WIN32_WMI
|
||||
}
|
||||
|
||||
public PrintRaw() {
|
||||
commands = new ByteArrayBuilder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.COMMAND;
|
||||
}
|
||||
|
||||
private byte[] getBytes(String str, String destEncoding) throws ArabicShapingException, IOException {
|
||||
switch(destEncoding.toLowerCase(Locale.ENGLISH)) {
|
||||
case "ibm864":
|
||||
case "cp864":
|
||||
case "csibm864":
|
||||
case "864":
|
||||
case "ibm-864":
|
||||
return ArabicConversionUtilities.convertToIBM864(str);
|
||||
default:
|
||||
return str.getBytes(destEncoding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.optJSONObject(i);
|
||||
if (data == null) {
|
||||
data = new JSONObject();
|
||||
data.put("data", printData.getString(i));
|
||||
}
|
||||
|
||||
String cmd = data.getString("data");
|
||||
JSONObject opt = data.optJSONObject("options");
|
||||
if (opt == null) { opt = new JSONObject(); }
|
||||
|
||||
PrintingUtilities.Format format = PrintingUtilities.Format.valueOf(data.optString("format", "COMMAND").toUpperCase(Locale.ENGLISH));
|
||||
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN);
|
||||
PrintOptions.Raw rawOpts = options.getRawOptions();
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
|
||||
destEncoding = rawOpts.getDestEncoding();
|
||||
if (destEncoding == null || destEncoding.isEmpty()) { destEncoding = Charset.defaultCharset().name(); }
|
||||
|
||||
try {
|
||||
switch(format) {
|
||||
case HTML:
|
||||
commands.append(getHtmlWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||
break;
|
||||
case IMAGE:
|
||||
commands.append(getImageWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||
break;
|
||||
case PDF:
|
||||
commands.append(getPdfWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||
break;
|
||||
case COMMAND:
|
||||
default:
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
commands.append(getBytes(cmd, destEncoding));
|
||||
break;
|
||||
default:
|
||||
commands.append(seekConversion(flavor.read(cmd, opt.optString("xmlTag", null)), rawOpts));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s into a raw %s command: %s", flavor, data.getString("data"), format, e.getLocalizedMessage()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] seekConversion(byte[] rawBytes, PrintOptions.Raw rawOpts) {
|
||||
if (rawOpts.getSrcEncoding() != null) {
|
||||
if(rawOpts.getSrcEncoding().equals(rawOpts.getDestEncoding()) || rawOpts.getDestEncoding() == null) {
|
||||
log.warn("Provided srcEncoding and destEncoding are the same, skipping");
|
||||
} else {
|
||||
try {
|
||||
String rawConvert = new String(rawBytes, rawOpts.getSrcEncoding());
|
||||
return rawConvert.getBytes(rawOpts.getDestEncoding());
|
||||
}
|
||||
catch(UnsupportedEncodingException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rawBytes;
|
||||
}
|
||||
|
||||
private ImageWrapper getImageWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||
BufferedImage bi;
|
||||
// 2.0 compat
|
||||
if (data.startsWith("data:image/") && data.contains(";base64,")) {
|
||||
String[] parts = data.split(";base64,");
|
||||
data = parts[parts.length - 1];
|
||||
flavor = PrintingUtilities.Flavor.BASE64;
|
||||
}
|
||||
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
// There's really no such thing as a 'PLAIN' image, assume it's a URL
|
||||
case FILE:
|
||||
bi = ImageIO.read(ConnectionUtilities.getInputStream(data, true));
|
||||
break;
|
||||
default:
|
||||
bi = ImageIO.read(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
|
||||
}
|
||||
|
||||
return getWrapper(bi, opt, pxlOpts);
|
||||
}
|
||||
|
||||
private ImageWrapper getPdfWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||
PDDocument doc;
|
||||
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
|
||||
case FILE:
|
||||
doc = PDDocument.load(ConnectionUtilities.getInputStream(data, true));
|
||||
break;
|
||||
default:
|
||||
doc = PDDocument.load(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
|
||||
}
|
||||
|
||||
double scale;
|
||||
PDRectangle rect = doc.getPage(0).getBBox();
|
||||
double pw = opt.optDouble("pageWidth", 0), ph = opt.optDouble("pageHeight", 0);
|
||||
if (ph <= 0 || (pw > 0 && (rect.getWidth() / rect.getHeight()) >= (pw / ph))) {
|
||||
scale = pw / rect.getWidth();
|
||||
} else {
|
||||
scale = ph / rect.getHeight();
|
||||
}
|
||||
if (scale <= 0) { scale = 1.0; }
|
||||
|
||||
BufferedImage bi = new PDFRenderer(doc).renderImage(0, (float)scale);
|
||||
return getWrapper(bi, opt, pxlOpts);
|
||||
}
|
||||
|
||||
private ImageWrapper getHtmlWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||
switch(flavor) {
|
||||
case FILE:
|
||||
case PLAIN:
|
||||
// We'll toggle between 'plain' and 'file' when we construct WebAppModel
|
||||
break;
|
||||
default:
|
||||
data = new String(seekConversion(flavor.read(data), rawOpts), destEncoding);
|
||||
}
|
||||
|
||||
double density = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch());
|
||||
if (density <= 1) {
|
||||
density = LanguageType.getType(opt.optString("language")).getDefaultDensity();
|
||||
}
|
||||
double pageZoom = density / 72.0;
|
||||
|
||||
double pageWidth = opt.optInt("pageWidth") / density * 72;
|
||||
double pageHeight = opt.optInt("pageHeight") / density * 72;
|
||||
|
||||
BufferedImage bi;
|
||||
WebAppModel model = new WebAppModel(data, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, false, pageZoom);
|
||||
|
||||
try {
|
||||
WebApp.initialize(); //starts if not already started
|
||||
bi = WebApp.raster(model);
|
||||
|
||||
// down scale back from web density
|
||||
double scaleFactor = opt.optDouble("pageWidth", 0) / bi.getWidth();
|
||||
BufferedImage scaled = new BufferedImage((int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = scaled.createGraphics();
|
||||
g2d.drawImage(bi, 0, 0, (int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), null);
|
||||
g2d.dispose();
|
||||
bi = scaled;
|
||||
}
|
||||
catch(Throwable t) {
|
||||
if (model.getZoom() > 1 && t instanceof IllegalArgumentException) {
|
||||
//probably a unrecognized image loader error, try at default zoom
|
||||
try {
|
||||
log.warn("Capture failed with increased zoom, attempting with default value");
|
||||
model.setZoom(1);
|
||||
bi = WebApp.raster(model);
|
||||
}
|
||||
catch(Throwable tt) {
|
||||
log.error("Failed to capture html raster");
|
||||
throw new IOException(tt);
|
||||
}
|
||||
} else {
|
||||
log.error("Failed to capture html raster");
|
||||
throw new IOException(t);
|
||||
}
|
||||
}
|
||||
|
||||
return getWrapper(bi, opt, pxlOpts);
|
||||
}
|
||||
|
||||
private ImageWrapper getWrapper(BufferedImage img, JSONObject opt, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||
if(img == null) {
|
||||
throw new IOException("Image provided is empty or null and cannot be converted.");
|
||||
}
|
||||
// Rotate image using orientation or rotation before sending to ImageWrapper
|
||||
if (pxlOpts.getOrientation() != null && pxlOpts.getOrientation() != PrintOptions.Orientation.PORTRAIT) {
|
||||
img = PrintImage.rotate(img, pxlOpts.getOrientation().getDegreesRot(), pxlOpts.getDithering(), pxlOpts.getInterpolation());
|
||||
} else if (pxlOpts.getRotation() % 360 != 0) {
|
||||
img = PrintImage.rotate(img, pxlOpts.getRotation(), pxlOpts.getDithering(), pxlOpts.getInterpolation());
|
||||
}
|
||||
|
||||
ImageWrapper iw = new ImageWrapper(img, LanguageType.getType(opt.optString("language")));
|
||||
iw.setCharset(Charset.forName(destEncoding));
|
||||
|
||||
//ESC/POS only
|
||||
int density = opt.optInt("dotDensity", -1);
|
||||
if (density == -1) {
|
||||
String dStr = opt.optString("dotDensity", null);
|
||||
if (dStr != null && !dStr.isEmpty()) {
|
||||
switch(dStr.toLowerCase(Locale.ENGLISH)) {
|
||||
case "single": density = 32; break;
|
||||
case "double": density = 33; break;
|
||||
case "triple": density = 39; break;
|
||||
// negative: legacy mode
|
||||
case "single-legacy": density = -32; break;
|
||||
case "double-legacy": density = -33; break;
|
||||
}
|
||||
} else {
|
||||
density = 32; //default
|
||||
}
|
||||
}
|
||||
iw.setDotDensity(density);
|
||||
|
||||
//EPL only
|
||||
iw.setxPos(opt.optInt("x", 0));
|
||||
iw.setyPos(opt.optInt("y", 0));
|
||||
|
||||
// PGL only
|
||||
iw.setLogoId(opt.optString("logoId", ""));
|
||||
iw.setIgpDots(opt.optBoolean("igpDots", false));
|
||||
|
||||
return iw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrintException {
|
||||
PrintOptions.Raw rawOpts = options.getRawOptions();
|
||||
|
||||
List<ByteArrayBuilder> pages;
|
||||
if (rawOpts.getSpoolSize() > 0 && rawOpts.getSpoolEnd() != null && !rawOpts.getSpoolEnd().isEmpty()) {
|
||||
try {
|
||||
pages = ByteUtilities.splitByteArray(commands.getByteArray(), rawOpts.getSpoolEnd().getBytes(destEncoding), rawOpts.getSpoolSize());
|
||||
}
|
||||
catch(UnsupportedEncodingException e) {
|
||||
throw new PrintException(e);
|
||||
}
|
||||
} else {
|
||||
pages = new ArrayList<>();
|
||||
pages.add(commands);
|
||||
}
|
||||
|
||||
List<File> tempFiles = null;
|
||||
for(int i = 0; i < rawOpts.getCopies(); i++) {
|
||||
for(int j = 0; j < pages.size(); j++) {
|
||||
ByteArrayBuilder bab = pages.get(j);
|
||||
try {
|
||||
if (output.isSetHost()) {
|
||||
printToHost(output.getHost(), output.getPort(), bab.getByteArray());
|
||||
} else if (output.isSetFile()) {
|
||||
printToFile(output.getFile(), bab.getByteArray(), true);
|
||||
} else {
|
||||
if (rawOpts.isForceRaw()) {
|
||||
if(tempFiles == null) {
|
||||
tempFiles = new ArrayList<>(pages.size());
|
||||
}
|
||||
File tempFile;
|
||||
if(tempFiles.size() <= j) {
|
||||
tempFile = File.createTempFile("qz_raw_", null);
|
||||
tempFiles.add(j, tempFile);
|
||||
printToFile(tempFile, bab.getByteArray(), false);
|
||||
} else {
|
||||
tempFile = tempFiles.get(j);
|
||||
}
|
||||
if(SystemUtilities.isWindows()) {
|
||||
// Placeholder only; not yet supported
|
||||
printToBackend(output.getNativePrinter(), tempFile, Backend.WIN32_WMI);
|
||||
} else {
|
||||
// Try CUPS backend first, fallback to LPR
|
||||
printToBackend(output.getNativePrinter(), tempFile, Backend.CUPS_RSS, Backend.CUPS_LPR);
|
||||
}
|
||||
} else {
|
||||
printToPrinter(output.getPrintService(), bab.getByteArray(), rawOpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
cleanupTempFiles(rawOpts.isRetainTemp(), tempFiles);
|
||||
throw new PrintException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanupTempFiles(rawOpts.isRetainTemp(), tempFiles);
|
||||
}
|
||||
|
||||
private void cleanupTempFiles(boolean retainTemp, List<File> tempFiles) {
|
||||
if(tempFiles != null) {
|
||||
if (!retainTemp) {
|
||||
for(File tempFile : tempFiles) {
|
||||
if(tempFile != null) {
|
||||
if(!tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Temp file(s) retained: {}", Arrays.toString(tempFiles.toArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A brute-force, however surprisingly elegant way to send a file to a networked printer.
|
||||
* <p/>
|
||||
* Please note that this will completely bypass the Print Spooler,
|
||||
* so the Operating System will have absolutely no printer information.
|
||||
* This is printing "blind".
|
||||
*/
|
||||
private void printToHost(String host, int port, byte[] cmds) throws IOException {
|
||||
log.debug("Printing to host {}:{}", host, port);
|
||||
|
||||
//throws any exception and auto-closes socket and stream
|
||||
try(Socket socket = new Socket(host, port); DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
|
||||
out.write(cmds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the raw commands directly to a file.
|
||||
*
|
||||
* @param file File to be written
|
||||
*/
|
||||
private void printToFile(File file, byte[] cmds, boolean locationRestricted) throws IOException {
|
||||
if(file == null) throw new IOException("No file specified");
|
||||
|
||||
if(locationRestricted && !PrefsSearch.getBoolean(ArgValue.SECURITY_PRINT_TOFILE)) {
|
||||
log.error("Printing to file '{}' is not permitted. Configure property '{}' to modify this behavior.",
|
||||
file, ArgValue.SECURITY_PRINT_TOFILE.getMatch());
|
||||
throw new IOException(String.format("Printing to file '%s' is not permitted", file));
|
||||
}
|
||||
|
||||
log.debug("Printing to file: {}", file.getName());
|
||||
|
||||
//throws any exception and auto-closes stream
|
||||
try(OutputStream out = new FileOutputStream(file)) {
|
||||
out.write(cmds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SimpleDoc} with the {@code commands} byte array.
|
||||
*/
|
||||
private void printToPrinter(PrintService service, byte[] cmds, PrintOptions.Raw rawOpts) throws PrintException {
|
||||
if (service == null) { throw new NullPrintServiceException("Service cannot be null"); }
|
||||
if (cmds == null || cmds.length == 0) { throw new NullCommandException("No commands found to send to the printer"); }
|
||||
|
||||
SimpleDoc doc = new SimpleDoc(cmds, DocFlavor.BYTE_ARRAY.AUTOSENSE, null);
|
||||
|
||||
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||
attributes.add(new JobName(rawOpts.getJobName(Constants.RAW_PRINT), Locale.getDefault()));
|
||||
|
||||
DocPrintJob printJob = service.createPrintJob();
|
||||
|
||||
waitForPrint(printJob, doc, attributes);
|
||||
}
|
||||
|
||||
protected void waitForPrint(DocPrintJob printJob, Doc doc, PrintRequestAttributeSet attributes) throws PrintException {
|
||||
final AtomicBoolean finished = new AtomicBoolean(false);
|
||||
printJob.addPrintJobListener(new PrintJobListener() {
|
||||
@Override
|
||||
public void printDataTransferCompleted(PrintJobEvent printJobEvent) {
|
||||
log.debug("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobCompleted(PrintJobEvent printJobEvent) {
|
||||
log.debug("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobFailed(PrintJobEvent printJobEvent) {
|
||||
log.error("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobCanceled(PrintJobEvent printJobEvent) {
|
||||
log.warn("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobNoMoreEvents(PrintJobEvent printJobEvent) {
|
||||
log.debug("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobRequiresAttention(PrintJobEvent printJobEvent) {
|
||||
log.info("{}", printJobEvent);
|
||||
}
|
||||
});
|
||||
|
||||
log.trace("Sending print job to printer");
|
||||
printJob.print(doc, attributes);
|
||||
|
||||
while(!finished.get()) {
|
||||
try { Thread.sleep(100); } catch(Exception ignore) {}
|
||||
}
|
||||
|
||||
log.trace("Print job received by printer");
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct/backend printing modes for forced raw printing
|
||||
*/
|
||||
public void printToBackend(NativePrinter printer, File tempFile, Backend... backends) throws IOException, PrintException {
|
||||
boolean success = false;
|
||||
|
||||
for(Backend backend : backends) {
|
||||
switch(backend) {
|
||||
case CUPS_LPR:
|
||||
// Use command line "lp" on Linux, BSD, Solaris, OSX, etc.
|
||||
String[] lpCmd = new String[] {"lp", "-d", printer.getPrinterId(), "-o", "raw", tempFile.getAbsolutePath()};
|
||||
if (!(success = ShellUtilities.execute(lpCmd))) {
|
||||
log.debug(StringUtils.join(lpCmd, ' '));
|
||||
}
|
||||
break;
|
||||
case CUPS_RSS:
|
||||
// Submit job via cupsDoRequest(...) via JNA against localhost:631\
|
||||
success = CupsUtils.sendRawFile(printer, tempFile);
|
||||
break;
|
||||
case WIN32_WMI:
|
||||
default:
|
||||
throw new UnsupportedOperationException("Raw backend \"" + backend + "\" is not yet supported.");
|
||||
}
|
||||
if(success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
throw new PrintException("Forced raw printing failed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
commands.clear();
|
||||
destEncoding = null;
|
||||
}
|
||||
|
||||
}
|
||||
44
old code/tray/src/qz/printer/action/ProcessorFactory.java
Executable file
44
old code/tray/src/qz/printer/action/ProcessorFactory.java
Executable file
@@ -0,0 +1,44 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import org.apache.commons.pool2.KeyedPooledObjectFactory;
|
||||
import org.apache.commons.pool2.PooledObject;
|
||||
import org.apache.commons.pool2.impl.DefaultPooledObject;
|
||||
import qz.utils.PrintingUtilities;
|
||||
|
||||
public class ProcessorFactory implements KeyedPooledObjectFactory<PrintingUtilities.Format,PrintProcessor> {
|
||||
|
||||
@Override
|
||||
public PooledObject<PrintProcessor> makeObject(PrintingUtilities.Format key) throws Exception {
|
||||
PrintProcessor processor;
|
||||
switch(key) {
|
||||
case HTML: processor = new PrintHTML(); break;
|
||||
case IMAGE: processor = new PrintImage(); break;
|
||||
case PDF: processor = new PrintPDF(); break;
|
||||
case DIRECT: processor = new PrintDirect(); break;
|
||||
case COMMAND: default: processor = new PrintRaw(); break;
|
||||
}
|
||||
|
||||
return new DefaultPooledObject<>(processor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) {
|
||||
return true; //no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void passivateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||
p.getObject().cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||
//no-op
|
||||
}
|
||||
|
||||
}
|
||||
512
old code/tray/src/qz/printer/action/html/WebApp.java
Executable file
512
old code/tray/src/qz/printer/action/html/WebApp.java
Executable file
@@ -0,0 +1,512 @@
|
||||
package qz.printer.action.html;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import com.sun.javafx.tk.TKPulseListener;
|
||||
import com.sun.javafx.tk.Toolkit;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.print.PageLayout;
|
||||
import javafx.print.PrinterJob;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.scene.transform.Transform;
|
||||
import javafx.scene.transform.Translate;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.w3c.dom.Attr;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.ws.PrintSocketServer;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.IntPredicate;
|
||||
|
||||
/**
|
||||
* JavaFX container for taking HTML snapshots.
|
||||
* Used by PrintHTML to generate printable images.
|
||||
* <p/>
|
||||
* Do not use constructor (used by JavaFX), instead call {@code WebApp.initialize()}
|
||||
*/
|
||||
public class WebApp extends Application {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(WebApp.class);
|
||||
|
||||
private static WebApp instance = null;
|
||||
private static Version webkitVersion = null;
|
||||
private static int CAPTURE_FRAMES = 2;
|
||||
private static int VECTOR_FRAMES = 1;
|
||||
private static Stage stage;
|
||||
private static WebView webView;
|
||||
private static double pageWidth;
|
||||
private static double pageHeight;
|
||||
private static double pageZoom;
|
||||
private static boolean raster;
|
||||
private static boolean headless;
|
||||
|
||||
private static CountDownLatch startupLatch;
|
||||
private static CountDownLatch captureLatch;
|
||||
|
||||
private static IntPredicate printAction;
|
||||
private static final AtomicReference<Throwable> thrown = new AtomicReference<>();
|
||||
|
||||
// JDK-8283686: Printing WebView may results in empty page
|
||||
private static final Version JDK_8283686_START = Version.valueOf(/* WebKit */ "609.1.0");
|
||||
private static final Version JDK_8283686_END = Version.valueOf(/* WebKit */ "612.1.0");
|
||||
private static final int JDK_8283686_VECTOR_FRAMES = 30;
|
||||
|
||||
|
||||
//listens for a Succeeded state to activate image capture
|
||||
private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
|
||||
log.trace("New state: {} > {}", oldState, newState);
|
||||
|
||||
// Cancelled should probably throw exception listener, but does not
|
||||
if (newState == Worker.State.CANCELLED) {
|
||||
// This can happen for file downloads, e.g. "response-content-disposition=attachment"
|
||||
// See https://github.com/qzind/tray/issues/1183
|
||||
unlatch(new IOException("Page load was cancelled for an unknown reason"));
|
||||
}
|
||||
if (newState == Worker.State.SUCCEEDED) {
|
||||
boolean hasBody = (boolean)webView.getEngine().executeScript("document.body != null");
|
||||
if (!hasBody) {
|
||||
log.warn("Loaded page has no body - likely a redirect, skipping state");
|
||||
return;
|
||||
}
|
||||
|
||||
//ensure html tag doesn't use scrollbars, clipping page instead
|
||||
Document doc = webView.getEngine().getDocument();
|
||||
NodeList tags = doc.getElementsByTagName("html");
|
||||
if (tags != null && tags.getLength() > 0) {
|
||||
Node base = tags.item(0);
|
||||
Attr applied = (Attr)base.getAttributes().getNamedItem("style");
|
||||
if (applied == null) {
|
||||
applied = doc.createAttribute("style");
|
||||
}
|
||||
applied.setValue(applied.getValue() + "; overflow: hidden;");
|
||||
base.getAttributes().setNamedItem(applied);
|
||||
}
|
||||
|
||||
//width was resized earlier (for responsive html), then calculate the best fit height
|
||||
// FIXME: Should only be needed when height is unknown but fixes blank vector prints
|
||||
double fittedHeight = findHeight();
|
||||
boolean heightNeeded = pageHeight <= 0;
|
||||
|
||||
if (heightNeeded) {
|
||||
pageHeight = fittedHeight;
|
||||
}
|
||||
|
||||
// find and set page zoom for increased quality
|
||||
double usableZoom = calculateSupportedZoom(pageWidth, pageHeight);
|
||||
if (usableZoom < pageZoom) {
|
||||
log.warn("Zoom level {} decreased to {} due to physical memory limitations", pageZoom, usableZoom);
|
||||
pageZoom = usableZoom;
|
||||
}
|
||||
webView.setZoom(pageZoom);
|
||||
log.trace("Zooming in by x{} for increased quality", pageZoom);
|
||||
|
||||
adjustSize(pageWidth * pageZoom, pageHeight * pageZoom);
|
||||
|
||||
//need to check for height again as resizing can cause partial results
|
||||
if (heightNeeded) {
|
||||
fittedHeight = findHeight();
|
||||
if (fittedHeight != pageHeight) {
|
||||
adjustSize(pageWidth * pageZoom, fittedHeight * pageZoom);
|
||||
}
|
||||
}
|
||||
|
||||
log.trace("Set HTML page height to {}", pageHeight);
|
||||
|
||||
autosize(webView);
|
||||
|
||||
Platform.runLater(() -> new AnimationTimer() {
|
||||
int frames = 0;
|
||||
|
||||
@Override
|
||||
public void handle(long l) {
|
||||
if (printAction.test(++frames)) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}.start());
|
||||
}
|
||||
};
|
||||
|
||||
//listens for load progress
|
||||
private static ChangeListener<Number> workDoneListener = (ov, oldWork, newWork) -> log.trace("Done: {} > {}", oldWork, newWork);
|
||||
|
||||
private static ChangeListener<String> msgListener = (ov, oldMsg, newMsg) -> log.trace("New status: {}", newMsg);
|
||||
|
||||
//listens for failures
|
||||
private static ChangeListener<Throwable> exceptListener = (obs, oldExc, newExc) -> {
|
||||
if (newExc != null) { unlatch(newExc); }
|
||||
};
|
||||
|
||||
|
||||
/** Called by JavaFX thread */
|
||||
public WebApp() {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
/** Starts JavaFX thread if not already running */
|
||||
public static synchronized void initialize() throws IOException {
|
||||
if (instance == null) {
|
||||
startupLatch = new CountDownLatch(1);
|
||||
// For JDK8 compat
|
||||
headless = false;
|
||||
|
||||
// JDK11+ depends bundled javafx
|
||||
if (Constants.JAVA_VERSION.getMajorVersion() >= 11) {
|
||||
// Monocle default for unit tests
|
||||
boolean useMonocle = true;
|
||||
if (PrintSocketServer.getTrayManager() != null) {
|
||||
// Honor user monocle override
|
||||
useMonocle = PrintSocketServer.getTrayManager().isMonoclePreferred();
|
||||
// Trust TrayManager's headless detection
|
||||
headless = PrintSocketServer.getTrayManager().isHeadless();
|
||||
} else {
|
||||
// Fallback for JDK11+
|
||||
headless = true;
|
||||
}
|
||||
if (useMonocle && SystemUtilities.hasMonocle()) {
|
||||
log.trace("Initializing monocle platform");
|
||||
System.setProperty("javafx.platform", "monocle");
|
||||
// Don't set glass.platform on Linux per https://github.com/qzind/tray/issues/702
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
case MAC:
|
||||
System.setProperty("glass.platform", "Monocle");
|
||||
break;
|
||||
default:
|
||||
// don't set "glass.platform"
|
||||
}
|
||||
|
||||
//software rendering required headless environments
|
||||
if (headless) {
|
||||
System.setProperty("prism.order", "sw");
|
||||
}
|
||||
} else {
|
||||
log.warn("Monocle platform will not be used");
|
||||
}
|
||||
}
|
||||
|
||||
new Thread(() -> Application.launch(WebApp.class)).start();
|
||||
}
|
||||
|
||||
if (startupLatch.getCount() > 0) {
|
||||
try {
|
||||
log.trace("Waiting for JavaFX..");
|
||||
if (!startupLatch.await(60, TimeUnit.SECONDS)) {
|
||||
throw new IOException("JavaFX did not start");
|
||||
} else {
|
||||
log.trace("Running a test snapshot to size the stage...");
|
||||
try {
|
||||
raster(new WebAppModel("<h1>startup</h1>", true, 0, 0, true, 2));
|
||||
log.trace("JFX initialized successfully");
|
||||
}
|
||||
catch(Throwable t) {
|
||||
throw new IOException(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(InterruptedException ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage st) throws Exception {
|
||||
startupLatch.countDown();
|
||||
log.debug("Started JavaFX");
|
||||
|
||||
webView = new WebView();
|
||||
|
||||
// JDK-8283686: Printing WebView may results in empty page
|
||||
// See also https://github.com/qzind/tray/issues/778
|
||||
if(getWebkitVersion() == null ||
|
||||
(getWebkitVersion().greaterThan(JDK_8283686_START) &&
|
||||
getWebkitVersion().lessThan(JDK_8283686_END))) {
|
||||
VECTOR_FRAMES = JDK_8283686_VECTOR_FRAMES; // Additional pulses needed for vector graphics
|
||||
}
|
||||
|
||||
st.setScene(new Scene(webView));
|
||||
stage = st;
|
||||
stage.setWidth(1);
|
||||
stage.setHeight(1);
|
||||
|
||||
Worker<Void> worker = webView.getEngine().getLoadWorker();
|
||||
worker.stateProperty().addListener(stateListener);
|
||||
worker.workDoneProperty().addListener(workDoneListener);
|
||||
worker.exceptionProperty().addListener(exceptListener);
|
||||
worker.messageProperty().addListener(msgListener);
|
||||
|
||||
//prevents JavaFX from shutting down when hiding window
|
||||
Platform.setImplicitExit(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the loaded source specified in the passed {@code model}.
|
||||
*
|
||||
* @param job A setup JavaFx {@code PrinterJob}
|
||||
* @param model The model specifying the web page parameters
|
||||
* @throws Throwable JavaFx will throw a generic {@code Throwable} class for any issues
|
||||
*/
|
||||
public static synchronized void print(final PrinterJob job, final WebAppModel model) throws Throwable {
|
||||
model.setZoom(1); //vector prints do not need to use zoom
|
||||
raster = false;
|
||||
|
||||
load(model, (int frames) -> {
|
||||
if(frames == VECTOR_FRAMES) {
|
||||
try {
|
||||
double printScale = 72d / 96d;
|
||||
webView.getTransforms().add(new Scale(printScale, printScale));
|
||||
|
||||
PageLayout layout = job.getJobSettings().getPageLayout();
|
||||
if (model.isScaled()) {
|
||||
double viewWidth = webView.getWidth() * printScale;
|
||||
double viewHeight = webView.getHeight() * printScale;
|
||||
|
||||
double scale;
|
||||
if ((viewWidth / viewHeight) >= (layout.getPrintableWidth() / layout.getPrintableHeight())) {
|
||||
scale = (layout.getPrintableWidth() / viewWidth);
|
||||
} else {
|
||||
scale = (layout.getPrintableHeight() / viewHeight);
|
||||
}
|
||||
webView.getTransforms().add(new Scale(scale, scale));
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
double useScale = 1;
|
||||
for(Transform t : webView.getTransforms()) {
|
||||
if (t instanceof Scale) { useScale *= ((Scale)t).getX(); }
|
||||
}
|
||||
|
||||
PageLayout page = job.getJobSettings().getPageLayout();
|
||||
Rectangle printBounds = new Rectangle(0, 0, page.getPrintableWidth(), page.getPrintableHeight());
|
||||
log.debug("Paper area: {},{}:{},{}", (int)page.getLeftMargin(), (int)page.getTopMargin(),
|
||||
(int)page.getPrintableWidth(), (int)page.getPrintableHeight());
|
||||
|
||||
Translate activePage = new Translate();
|
||||
webView.getTransforms().add(activePage);
|
||||
|
||||
int columnsNeed = Math.max(1, (int)Math.ceil(webView.getWidth() / printBounds.getWidth() * useScale - 0.1));
|
||||
int rowsNeed = Math.max(1, (int)Math.ceil(webView.getHeight() / printBounds.getHeight() * useScale - 0.1));
|
||||
log.debug("Document will be printed across {} pages", columnsNeed * rowsNeed);
|
||||
|
||||
try {
|
||||
for(int row = 0; row < rowsNeed; row++) {
|
||||
for(int col = 0; col < columnsNeed; col++) {
|
||||
activePage.setX((-col * printBounds.getWidth()) / useScale);
|
||||
activePage.setY((-row * printBounds.getHeight()) / useScale);
|
||||
|
||||
job.printPage(webView);
|
||||
}
|
||||
}
|
||||
|
||||
unlatch(null);
|
||||
}
|
||||
catch(Exception e) {
|
||||
unlatch(e);
|
||||
}
|
||||
finally {
|
||||
//reset state
|
||||
webView.getTransforms().clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(Exception e) { unlatch(e); }
|
||||
}
|
||||
return frames >= VECTOR_FRAMES;
|
||||
});
|
||||
|
||||
log.trace("Waiting on print..");
|
||||
captureLatch.await(); //released when unlatch is called
|
||||
|
||||
if (thrown.get() != null) { throw thrown.get(); }
|
||||
}
|
||||
|
||||
public static synchronized BufferedImage raster(final WebAppModel model) throws Throwable {
|
||||
AtomicReference<BufferedImage> capture = new AtomicReference<>();
|
||||
|
||||
//ensure JavaFX has started before we run
|
||||
if (startupLatch.getCount() > 0) {
|
||||
throw new IOException("JavaFX has not been started");
|
||||
}
|
||||
|
||||
//raster still needs to show stage for valid capture
|
||||
Platform.runLater(() -> {
|
||||
stage.show();
|
||||
stage.toBack();
|
||||
});
|
||||
|
||||
raster = true;
|
||||
|
||||
load(model, (int frames) -> {
|
||||
if (frames == CAPTURE_FRAMES) {
|
||||
log.debug("Attempting image capture");
|
||||
|
||||
Toolkit.getToolkit().addPostSceneTkPulseListener(new TKPulseListener() {
|
||||
@Override
|
||||
public void pulse() {
|
||||
try {
|
||||
// TODO: Revert to Callback once JDK-8244588/SUPQZ-5 is avail (JDK11+ only)
|
||||
capture.set(SwingFXUtils.fromFXImage(webView.snapshot(null, null), null));
|
||||
unlatch(null);
|
||||
}
|
||||
catch(Exception e) {
|
||||
unlatch(e);
|
||||
}
|
||||
finally {
|
||||
Toolkit.getToolkit().removePostSceneTkPulseListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
Toolkit.getToolkit().requestNextPulse();
|
||||
}
|
||||
|
||||
return frames >= CAPTURE_FRAMES;
|
||||
});
|
||||
|
||||
log.trace("Waiting on capture..");
|
||||
captureLatch.await(); //released when unlatch is called
|
||||
|
||||
if (thrown.get() != null) { throw thrown.get(); }
|
||||
|
||||
return capture.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the loaded source specified in the passed {@code model}.
|
||||
*
|
||||
* @param model The model specifying the web page parameters.
|
||||
* @param action EventHandler that will be ran when the WebView completes loading.
|
||||
*/
|
||||
private static synchronized void load(WebAppModel model, IntPredicate action) {
|
||||
captureLatch = new CountDownLatch(1);
|
||||
thrown.set(null);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
//zoom should only be factored on raster prints
|
||||
pageZoom = model.getZoom();
|
||||
pageWidth = model.getWebWidth();
|
||||
pageHeight = model.getWebHeight();
|
||||
|
||||
log.trace("Setting starting size {}:{}", pageWidth, pageHeight);
|
||||
adjustSize(pageWidth * pageZoom, pageHeight * pageZoom);
|
||||
|
||||
if (pageHeight == 0) {
|
||||
webView.setMinHeight(1);
|
||||
webView.setPrefHeight(1);
|
||||
webView.setMaxHeight(1);
|
||||
}
|
||||
|
||||
autosize(webView);
|
||||
|
||||
printAction = action;
|
||||
|
||||
if (model.isPlainText()) {
|
||||
webView.getEngine().loadContent(model.getSource(), "text/html");
|
||||
} else {
|
||||
webView.getEngine().load(model.getSource());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static double findHeight() {
|
||||
String heightText = webView.getEngine().executeScript("Math.max(document.body.offsetHeight, document.body.scrollHeight)").toString();
|
||||
return Double.parseDouble(heightText);
|
||||
}
|
||||
|
||||
private static void adjustSize(double toWidth, double toHeight) {
|
||||
webView.setMinSize(toWidth, toHeight);
|
||||
webView.setPrefSize(toWidth, toHeight);
|
||||
webView.setMaxSize(toWidth, toHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix blank page after autosize is called
|
||||
*/
|
||||
public static void autosize(WebView webView) {
|
||||
webView.autosize();
|
||||
|
||||
if (!raster) {
|
||||
// Call updatePeer; fixes a bug with webView resizing
|
||||
// Can be avoided by calling stage.show() but breaks headless environments
|
||||
// See: https://github.com/qzind/tray/issues/513
|
||||
String[] methods = {"impl_updatePeer" /*jfx8*/, "doUpdatePeer" /*jfx11*/};
|
||||
try {
|
||||
for(Method m : webView.getClass().getDeclaredMethods()) {
|
||||
for(String method : methods) {
|
||||
if (m.getName().equals(method)) {
|
||||
m.setAccessible(true);
|
||||
m.invoke(webView);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(SecurityException | ReflectiveOperationException e) {
|
||||
log.warn("Unable to update peer; Blank pages may occur.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double calculateSupportedZoom(double width, double height) {
|
||||
long memory = Runtime.getRuntime().maxMemory();
|
||||
int allowance = (memory / 1048576L) > 1024? 3:2;
|
||||
if (headless) { allowance--; }
|
||||
long availSpace = memory << allowance;
|
||||
|
||||
// Memory needed for print is roughly estimated as
|
||||
// (width * height) [pixels needed] * (pageZoom * 72d) [print density used] * 3 [rgb channels]
|
||||
return Math.sqrt(availSpace / ((width * height) * (pageZoom * 72d) * 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Final cleanup when no longer capturing
|
||||
*/
|
||||
public static void unlatch(Throwable t) {
|
||||
if (t != null) {
|
||||
thrown.set(t);
|
||||
}
|
||||
|
||||
captureLatch.countDown();
|
||||
stage.hide();
|
||||
}
|
||||
|
||||
public static Version getWebkitVersion() {
|
||||
if(webkitVersion == null) {
|
||||
if(webView != null) {
|
||||
String userAgent = webView.getEngine().getUserAgent();
|
||||
String[] parts = userAgent.split("WebKit/");
|
||||
if (parts.length > 1) {
|
||||
String[] split = parts[1].split(" ");
|
||||
if (split.length > 0) {
|
||||
try {
|
||||
webkitVersion = Version.valueOf(split[0]);
|
||||
log.info("WebKit version {} detected", webkitVersion);
|
||||
} catch(Exception ignore) {}
|
||||
}
|
||||
}
|
||||
if(webkitVersion == null) {
|
||||
log.warn("WebKit version couldn't be parsed from UserAgent: {}", userAgent);
|
||||
}
|
||||
} else {
|
||||
log.warn("Can't get WebKit version, JavaFX hasn't started yet.");
|
||||
}
|
||||
}
|
||||
return webkitVersion;
|
||||
}
|
||||
}
|
||||
81
old code/tray/src/qz/printer/action/html/WebAppModel.java
Executable file
81
old code/tray/src/qz/printer/action/html/WebAppModel.java
Executable file
@@ -0,0 +1,81 @@
|
||||
package qz.printer.action.html;
|
||||
|
||||
public class WebAppModel {
|
||||
|
||||
private String source;
|
||||
private boolean plainText;
|
||||
|
||||
private double width, webWidth;
|
||||
private double height, webHeight;
|
||||
private boolean isScaled;
|
||||
private double zoom;
|
||||
|
||||
public WebAppModel(String source, boolean plainText, double width, double height, boolean isScaled, double zoom) {
|
||||
this.source = source;
|
||||
this.plainText = plainText;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.webWidth = width * (96d / 72d);
|
||||
this.webHeight = height * (96d / 72d);
|
||||
this.isScaled = isScaled;
|
||||
this.zoom = zoom;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public boolean isPlainText() {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
public void setPlainText(boolean plainText) {
|
||||
this.plainText = plainText;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
this.width = width;
|
||||
this.webWidth = width * (96d / 72d);
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(double height) {
|
||||
this.height = height;
|
||||
this.webHeight = height * (96d / 72d);
|
||||
}
|
||||
|
||||
public double getWebWidth() {
|
||||
return webWidth;
|
||||
}
|
||||
|
||||
public double getWebHeight() {
|
||||
return webHeight;
|
||||
}
|
||||
|
||||
public boolean isScaled() {
|
||||
return isScaled;
|
||||
}
|
||||
|
||||
public void setScaled(boolean scaled) {
|
||||
isScaled = scaled;
|
||||
}
|
||||
|
||||
public double getZoom() {
|
||||
return zoom;
|
||||
}
|
||||
|
||||
public void setZoom(double zoom) {
|
||||
this.zoom = zoom;
|
||||
}
|
||||
}
|
||||
80
old code/tray/src/qz/printer/action/pdf/BookBundle.java
Executable file
80
old code/tray/src/qz/printer/action/pdf/BookBundle.java
Executable file
@@ -0,0 +1,80 @@
|
||||
package qz.printer.action.pdf;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.print.Book;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Printable;
|
||||
import java.awt.print.PrinterException;
|
||||
|
||||
/**
|
||||
* Wrapper of the {@code Book} class as a {@code Printable} type,
|
||||
* since PrinterJob implementations do not seem to handle the {@code Pageable} interface properly.
|
||||
*/
|
||||
public class BookBundle extends Book {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(BookBundle.class);
|
||||
|
||||
private Printable lastPrint;
|
||||
private int lastStarted;
|
||||
|
||||
public BookBundle() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper of the wrapper class so that PrinterJob implementations will handle it as proper pageable
|
||||
*/
|
||||
public Book wrapAndPresent() {
|
||||
Book cover = new Book();
|
||||
for(int i = 0; i < getNumberOfPages(); i++) {
|
||||
cover.append(new PrintingPress(), getPageFormat(i));
|
||||
}
|
||||
|
||||
return cover;
|
||||
}
|
||||
|
||||
public Book wrapAndPresent(int offset, int length) {
|
||||
Book coverSubset = new Book();
|
||||
for(int i = offset; i < offset + length && i < getNumberOfPages(); i++) {
|
||||
coverSubset.append(new PrintingPress(offset), getPageFormat(i));
|
||||
}
|
||||
|
||||
return coverSubset;
|
||||
}
|
||||
|
||||
|
||||
/** Printable wrapper to ensure proper reading of multiple documents across spooling */
|
||||
private class PrintingPress implements Printable {
|
||||
private int pageOffset;
|
||||
|
||||
public PrintingPress() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
public PrintingPress(int offset) {
|
||||
pageOffset = offset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int print(Graphics g, PageFormat format, int pageIndex) throws PrinterException {
|
||||
pageIndex += pageOffset;
|
||||
log.trace("Requested page {} for printing", pageIndex);
|
||||
|
||||
if (pageIndex < getNumberOfPages()) {
|
||||
Printable printable = getPrintable(pageIndex);
|
||||
if (printable != lastPrint) {
|
||||
lastPrint = printable;
|
||||
lastStarted = pageIndex;
|
||||
}
|
||||
|
||||
return printable.print(g, format, pageIndex - lastStarted);
|
||||
}
|
||||
|
||||
return NO_SUCH_PAGE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
90
old code/tray/src/qz/printer/action/pdf/PDFWrapper.java
Executable file
90
old code/tray/src/qz/printer/action/pdf/PDFWrapper.java
Executable file
@@ -0,0 +1,90 @@
|
||||
package qz.printer.action.pdf;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.printing.PDFPrintable;
|
||||
import org.apache.pdfbox.printing.Scaling;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.attribute.standard.OrientationRequested;
|
||||
import java.awt.*;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Printable;
|
||||
import java.awt.print.PrinterException;
|
||||
|
||||
public class PDFWrapper implements Printable {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PDFWrapper.class);
|
||||
|
||||
private PDDocument document;
|
||||
private Scaling scaling;
|
||||
private OrientationRequested orientation = OrientationRequested.PORTRAIT;
|
||||
|
||||
private PDFPrintable printable;
|
||||
|
||||
public PDFWrapper(PDDocument document, Scaling scaling, boolean showPageBorder, boolean ignoreTransparency, boolean useAlternateFontRendering, float dpi, boolean center, PrintOptions.Orientation orientation, RenderingHints hints) {
|
||||
this.document = document;
|
||||
this.scaling = scaling;
|
||||
if (orientation != null) {
|
||||
this.orientation = orientation.getAsOrientRequested();
|
||||
}
|
||||
|
||||
PDFRenderer renderer = new ParamPdfRenderer(document, useAlternateFontRendering, ignoreTransparency);
|
||||
printable = new PDFPrintable(document, scaling, showPageBorder, dpi, center, renderer);
|
||||
printable.setRenderingHints(hints);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
|
||||
log.debug("Paper area: {},{}:{},{}", (int)pageFormat.getImageableX(), (int)pageFormat.getImageableY(),
|
||||
(int)pageFormat.getImageableWidth(), (int)pageFormat.getImageableHeight());
|
||||
|
||||
graphics.drawString(" ", 0, 0);
|
||||
|
||||
//reverse fix for OSX
|
||||
if (SystemUtilities.isMac() && orientation == OrientationRequested.REVERSE_LANDSCAPE) {
|
||||
adjustPrintForOrientation(graphics, pageFormat, pageIndex);
|
||||
}
|
||||
|
||||
return printable.print(graphics, pageFormat, pageIndex);
|
||||
}
|
||||
|
||||
private void adjustPrintForOrientation(Graphics g, PageFormat format, int page) {
|
||||
PDRectangle bounds = document.getPage(page).getBBox();
|
||||
double docWidth = bounds.getWidth();
|
||||
double docHeight = bounds.getHeight();
|
||||
|
||||
//reports dimensions flipped if rotated
|
||||
if (document.getPage(page).getRotation() % 180 == 90) {
|
||||
docWidth = bounds.getHeight();
|
||||
docHeight = bounds.getWidth();
|
||||
}
|
||||
|
||||
//adjust across page to account for wrong origin corner
|
||||
double leftAdjust, topAdjust;
|
||||
|
||||
if (scaling != Scaling.ACTUAL_SIZE) {
|
||||
if ((docWidth / docHeight) >= (format.getImageableWidth() / format.getImageableHeight())) {
|
||||
leftAdjust = 0;
|
||||
topAdjust = format.getImageableHeight() - (docHeight / (docWidth / format.getImageableWidth()));
|
||||
} else {
|
||||
leftAdjust = format.getImageableWidth() - (docWidth / (docHeight / format.getImageableHeight()));
|
||||
topAdjust = 0;
|
||||
}
|
||||
} else {
|
||||
leftAdjust = format.getImageableWidth() - docWidth;
|
||||
topAdjust = format.getImageableHeight() - docHeight;
|
||||
}
|
||||
|
||||
log.info("Adjusting image by {},{} for selected orientation", leftAdjust, topAdjust);
|
||||
|
||||
//reverse landscape will have only rotated doc, this adjusts page so [0,0] appears to come from correct corner
|
||||
g.translate((int)leftAdjust, (int)topAdjust);
|
||||
}
|
||||
|
||||
}
|
||||
47
old code/tray/src/qz/printer/action/pdf/ParamPdfRenderer.java
Executable file
47
old code/tray/src/qz/printer/action/pdf/ParamPdfRenderer.java
Executable file
@@ -0,0 +1,47 @@
|
||||
package qz.printer.action.pdf;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.pdfbox.rendering.PageDrawer;
|
||||
import org.apache.pdfbox.rendering.PageDrawerParameters;
|
||||
import qz.printer.rendering.OpaqueDrawObject;
|
||||
import qz.printer.rendering.OpaqueGraphicStateParameters;
|
||||
import qz.printer.rendering.PdfFontPageDrawer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ParamPdfRenderer extends PDFRenderer {
|
||||
|
||||
private boolean useAlternateFontRendering;
|
||||
private boolean ignoreTransparency;
|
||||
|
||||
public ParamPdfRenderer(PDDocument document, boolean useAlternateFontRendering, boolean ignoreTransparency) {
|
||||
super(document);
|
||||
|
||||
this.useAlternateFontRendering = useAlternateFontRendering;
|
||||
this.ignoreTransparency = ignoreTransparency;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException {
|
||||
if (useAlternateFontRendering) {
|
||||
return new PdfFontPageDrawer(parameters, ignoreTransparency);
|
||||
} else if(ignoreTransparency) {
|
||||
return new OpaquePageDrawer(parameters);
|
||||
}
|
||||
// Fallback to default PageDrawer
|
||||
return new PageDrawer(parameters);
|
||||
}
|
||||
|
||||
// override drawer to make use of customized draw object
|
||||
private static class OpaquePageDrawer extends PageDrawer {
|
||||
public OpaquePageDrawer(PageDrawerParameters parameters) throws IOException {
|
||||
super(parameters);
|
||||
|
||||
// Note: These must match PdfFontPageDrawer's ignoreTransparency condition
|
||||
addOperator(new OpaqueDrawObject());
|
||||
addOperator(new OpaqueGraphicStateParameters());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
801
old code/tray/src/qz/printer/action/raw/ImageWrapper.java
Executable file
801
old code/tray/src/qz/printer/action/raw/ImageWrapper.java
Executable file
@@ -0,0 +1,801 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2013 Tres Finocchiaro, QZ Industries
|
||||
* Copyright (C) 2013 Antoni Ten Monro's
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package qz.printer.action.raw;
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.ByteArrayBuilder;
|
||||
import qz.exception.InvalidRawImageException;
|
||||
import qz.utils.ByteUtilities;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Abstract wrapper for images to be printed with thermal printers.
|
||||
*
|
||||
* @author Tres Finocchiaro
|
||||
* @author Antoni Ten Monro's
|
||||
* <p/>
|
||||
* Changelog:
|
||||
* <p/>
|
||||
* 20130805 (Tres Finocchiaro) Merged Antoni's changes with original keeping
|
||||
* Antoni's better instantiation, "black" pixel logic, removing class abstraction
|
||||
* (uses LanguageType Enum switching instead for smaller codebase)
|
||||
* <p/>
|
||||
* 20130710 (Antoni Ten Monro's) Refactored the original, to have the
|
||||
* actual implementation of the different ImageWrapper classes in derived
|
||||
* classes, while leaving common functionality here.
|
||||
* @author Oleg Morozov 02/21/2013 (via public domain)
|
||||
* @author Tres Finocchiaro 10/01/2013
|
||||
*/
|
||||
@SuppressWarnings("UnusedDeclaration") //Library class
|
||||
public class ImageWrapper {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(ImageWrapper.class);
|
||||
|
||||
/**
|
||||
* Represents the CHECK_BLACK quantization method, where only fully black
|
||||
* pixels are considered black when translating them to printer format.
|
||||
*/
|
||||
public static final int CHECK_BLACK = 0;
|
||||
/**
|
||||
* Represents the CHECK_LUMA quantization method, pixels are considered
|
||||
* black if their luma is less than a set threshold. Transparent pixels, and
|
||||
* pixels whose alpha channel is less than the threshold are considered not
|
||||
* black.
|
||||
*/
|
||||
public static final int CHECK_LUMA = 1;
|
||||
/**
|
||||
* Represents the CHECK_ALPHA quantization method, pixels are considered
|
||||
* black if their alpha is more than a set threshold. Color information is
|
||||
* discarded.
|
||||
*/
|
||||
public static final int CHECK_ALPHA = 2;
|
||||
|
||||
private int lumaThreshold = 127;
|
||||
private boolean[] imageAsBooleanArray; //Image representation as an array of boolean, with true values representing imageAsBooleanArray dots
|
||||
private int[] imageAsIntArray; //Image representation as an array of ints, with each bit representing a imageAsBooleanArray dot
|
||||
private ByteArrayBuilder byteBuffer = new ByteArrayBuilder();
|
||||
private int alphaThreshold = 127;
|
||||
private BufferedImage bufferedImage;
|
||||
private LanguageType languageType;
|
||||
private Charset charset = Charset.defaultCharset();
|
||||
private int imageQuantizationMethod = CHECK_LUMA;
|
||||
private int xPos = 0; // X coordinate used for EPL2, CPCL. Irrelevant for ZPLII, ESC/POS, etc
|
||||
private int yPos = 0; // Y coordinate used for EPL2, CPCL. Irrelevant for ZPLII, ESC/POS, etc
|
||||
private String logoId = ""; // PGL only, the logo ID
|
||||
private boolean igpDots = false; // PGL only, toggle IGP/PGL default resolution of 72dpi
|
||||
private int dotDensity = 32; // Generally 32 = Single (normal) 33 = Double (higher res) for ESC/POS. Irrelevant for all other languages.
|
||||
|
||||
private boolean legacyMode = false; // Use newlines for ESC/POS spacing; simulates <=2.0.11 behavior
|
||||
|
||||
/**
|
||||
* Creates a new
|
||||
* <code>ImageWrapper</code> from a
|
||||
* <code>BufferedImage.</code>
|
||||
*
|
||||
* @param bufferedImage The image to convert for thermal printing
|
||||
*/
|
||||
public ImageWrapper(BufferedImage bufferedImage, LanguageType languageType) {
|
||||
this.bufferedImage = bufferedImage;
|
||||
this.languageType = languageType;
|
||||
log.info("Loading BufferedImage");
|
||||
log.info("Dimensions: {}x{}", bufferedImage.getWidth(), bufferedImage.getHeight());
|
||||
init();
|
||||
|
||||
if (languageType.requiresImageWidthValidated()) {
|
||||
validateImageWidth();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the luma threshold used for the CHECK_LUMA quantization method.
|
||||
* Pixels that are more transparent than this, or that have a luma greater
|
||||
* than this will be considered white. The threshold goes from 0 (black) to
|
||||
* 255 (white).
|
||||
*
|
||||
* @return the current threshold
|
||||
*/
|
||||
public int getLumaThreshold() {
|
||||
return lumaThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the luma threshold used for the CHECK_LUMA quantization method.
|
||||
* Pixels that are more transparent than this, or that have a luma greater
|
||||
* than this will be considered white. The threshold goes from 0 (black) to
|
||||
* 255 (white).
|
||||
*
|
||||
* @param lumaThreshold the threshold to set
|
||||
*/
|
||||
public void setLumaThreshold(int lumaThreshold) {
|
||||
this.lumaThreshold = lumaThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method used to convert the image to monochrome. Currently
|
||||
* implemented methods are: <ul> <li><code>CHECK_BLACK</code>: Pixels are
|
||||
* considered black if and only if they are completely black and opaque
|
||||
* <li><code>CHECK_LUMA</code>: Pixels are considered black if and only if
|
||||
* their luma is under a threshold, and their opacity is over a threshold.
|
||||
* This threshold is set with
|
||||
* <code>setLumaThreshold</code> <li><code>CHECK_ALPHA</code>: Pixels are
|
||||
* considered black if and only if their opacity (alpha) is over a
|
||||
* threshold,. This threshold is set with
|
||||
* <code>setAlphaThreshold</code> </ul>
|
||||
* <p/>
|
||||
* Default quantization method is
|
||||
* <code>CHECK_BLACK</code>.
|
||||
*
|
||||
* @return the current quantization method
|
||||
*/
|
||||
public int getImageQuantizationMethod() {
|
||||
return imageQuantizationMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the method used to convert the image to monochrome. Currently
|
||||
* implemented methods are: <ul> <li><code>CHECK_BLACK</code>: Pixels are
|
||||
* considered black if and only if they are completely black and opaque
|
||||
* <li><code>CHECK_LUMA</code>: Pixels are considered black if and only if
|
||||
* their luma is under a threshold, and their opacity is over a threshold.
|
||||
* This threshold is set with
|
||||
* <code>setLumaThreshold</code> <li><code>CHECK_ALPHA</code>: Pixels are
|
||||
* considered black if and only if their opacity (alpha) is over a
|
||||
* threshold,. This threshold is set with
|
||||
* <code>setAlphaThreshold</code> </ul>
|
||||
* <p/>
|
||||
* Default (and fallback) quantization method is
|
||||
* <code>CHECK_BLACK</code>.
|
||||
*
|
||||
* @param imageQuantizationMethod the quantization method to set
|
||||
*/
|
||||
public void setImageQuantizationMethod(int imageQuantizationMethod) {
|
||||
this.imageQuantizationMethod = imageQuantizationMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the transparency (alpha) threshold used for the CHECK_ALPHA
|
||||
* quantization method. Pixels that are more transparent than this will be
|
||||
* considered white. The threshold goes from 0 (fully transparent) to 255
|
||||
* (fully opaque)
|
||||
*
|
||||
* @return the current threshold
|
||||
*/
|
||||
public int getAlphaThreshold() {
|
||||
return alphaThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the transparency (alpha) threshold used for the CHECK_ALPHA
|
||||
* quantization method. Pixels that are more transparent than this will be
|
||||
* considered white. The threshold goes from 0 (fully transparent) to 255
|
||||
* (fully opaque)
|
||||
*
|
||||
* @param alphaThreshold the Threshold to set
|
||||
*/
|
||||
public void setAlphaThreshold(int alphaThreshold) {
|
||||
this.alphaThreshold = alphaThreshold;
|
||||
}
|
||||
|
||||
public int getDotDensity() {
|
||||
return dotDensity;
|
||||
}
|
||||
|
||||
public void setDotDensity(int dotDensity) {
|
||||
this.legacyMode = dotDensity < 0;
|
||||
this.dotDensity = Math.abs(dotDensity);
|
||||
}
|
||||
|
||||
public void setLogoId(String logoId) {
|
||||
this.logoId = logoId;
|
||||
}
|
||||
|
||||
public String getLogoId() {
|
||||
return logoId;
|
||||
}
|
||||
|
||||
public void setIgpDots(boolean igpDots) {
|
||||
this.igpDots = igpDots;
|
||||
}
|
||||
|
||||
public boolean getIgpDots() {
|
||||
return igpDots;
|
||||
}
|
||||
|
||||
public int getxPos() {
|
||||
return xPos;
|
||||
}
|
||||
|
||||
public void setxPos(int xPos) {
|
||||
this.xPos = xPos;
|
||||
}
|
||||
|
||||
public int getyPos() {
|
||||
return yPos;
|
||||
}
|
||||
|
||||
public void setyPos(int yPos) {
|
||||
this.yPos = yPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a given pixel should be black. Multiple quantization algorithms
|
||||
* are available. The quantization method should be adjusted with
|
||||
* setQuantizationMethod. Should an invalid value be set as the
|
||||
* quantization method, CHECK_BLACK will be used
|
||||
*
|
||||
* @param rgbPixel the color of the pixel as defined in getRGB()
|
||||
* @return true if the pixel should be black, false otherwise
|
||||
*/
|
||||
private boolean isBlack(int rgbPixel) {
|
||||
Color color = new Color(rgbPixel, true);
|
||||
|
||||
int r = color.getRed();
|
||||
int g = color.getGreen();
|
||||
int b = color.getBlue();
|
||||
int a = color.getAlpha();
|
||||
switch(getImageQuantizationMethod()) {
|
||||
case CHECK_LUMA:
|
||||
if (a < getLumaThreshold()) {
|
||||
return false; // assume pixels that are less opaque than the luma threshold should be considered to be white
|
||||
}
|
||||
|
||||
int luma = ((r * 299) + (g * 587) + (b * 114)) / 1000; //luma formula
|
||||
return luma < getLumaThreshold(); //pixels that have less luma than the threshold are black
|
||||
case CHECK_ALPHA:
|
||||
return a > getAlphaThreshold(); //pixels that are more opaque than the threshold are black
|
||||
case CHECK_BLACK: //only fully black pixels are black
|
||||
default:
|
||||
return color.equals(Color.BLACK); //The default
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets ImageAsBooleanArray. boolean is used instead of int for memory
|
||||
* considerations.
|
||||
*/
|
||||
private boolean[] generateBlackPixels(BufferedImage bi) {
|
||||
log.info("Converting image to monochrome");
|
||||
int h = bi.getHeight();
|
||||
int w = bi.getWidth();
|
||||
int[] rgbPixels = bi.getRGB(0, 0, w, h, null, 0, w);
|
||||
|
||||
/*
|
||||
* It makes most sense to have black pixels as 1's and white pixels
|
||||
* as zero's, however some printer manufacturers had this reversed
|
||||
* and used 0's for the black pixels. EPL is a common language that
|
||||
* uses 0's for black pixels.
|
||||
* See also: https://support.zebra.com/cpws/docs/eltron/gw_command.htm
|
||||
*/
|
||||
boolean[] pixels = new boolean[rgbPixels.length];
|
||||
for(int i = 0; i < rgbPixels.length; i++) {
|
||||
pixels[i] = languageType.requiresImageOutputInverted() != isBlack(rgbPixels[i]);
|
||||
}
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the internal representation of the image into an array of bytes,
|
||||
* suitable to be sent to a raw printer.
|
||||
*
|
||||
* @return The raw bytes that compose the image
|
||||
*/
|
||||
private byte[] getBytes() {
|
||||
log.info("Generating byte array");
|
||||
int[] ints = getImageAsIntArray();
|
||||
byte[] bytes = new byte[ints.length];
|
||||
for(int i = 0; i < ints.length; i++) {
|
||||
bytes[i] = (byte)ints[i];
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private void generateIntArray() {
|
||||
log.info("Packing bits");
|
||||
imageAsIntArray = new int[imageAsBooleanArray.length / 8];
|
||||
// Convert every eight zero's to a full byte, in decimal
|
||||
for(int i = 0; i < imageAsIntArray.length; i++) {
|
||||
for(int k = 0; k < 8; k++) {
|
||||
imageAsIntArray[i] += (imageAsBooleanArray[8 * i + k]? 1:0) << 7 - k;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the EPL2 commands to print an image. One command is emitted per
|
||||
* line of the image. This avoids issues with commands being too long.
|
||||
*
|
||||
* @return The commands to print the image as an array of bytes, ready to be
|
||||
* sent to the printer
|
||||
*/
|
||||
public byte[] getImageCommand(JSONObject opt) throws InvalidRawImageException, UnsupportedEncodingException {
|
||||
getByteBuffer().clear();
|
||||
|
||||
switch(languageType) {
|
||||
case ESCP:
|
||||
appendEpsonSlices(getByteBuffer());
|
||||
break;
|
||||
case ZPL:
|
||||
String zplHexAsString = ByteUtilities.getHexString(getImageAsIntArray());
|
||||
int byteLen = zplHexAsString.length() / 2;
|
||||
int perRow = byteLen / getHeight();
|
||||
StringBuilder zpl = new StringBuilder("^GFA,")
|
||||
.append(byteLen).append(",").append(byteLen).append(",")
|
||||
.append(perRow).append(",").append(zplHexAsString);
|
||||
|
||||
getByteBuffer().append(zpl, charset);
|
||||
break;
|
||||
case EPL:
|
||||
StringBuilder epl = new StringBuilder("GW")
|
||||
.append(getxPos()).append(",")
|
||||
.append(getyPos()).append(",")
|
||||
.append(getWidth() / 8).append(",")
|
||||
.append(getHeight()).append(",");
|
||||
|
||||
getByteBuffer().append(epl, charset).append(getBytes()).append(new byte[] {10});
|
||||
break;
|
||||
case CPCL:
|
||||
String cpclHexAsString = ByteUtilities.getHexString(getImageAsIntArray());
|
||||
StringBuilder cpcl = new StringBuilder("EG ")
|
||||
.append(getWidth() / 8).append(" ")
|
||||
.append(getHeight()).append(" ")
|
||||
.append(getxPos()).append(" ")
|
||||
.append(getyPos()).append(" ")
|
||||
.append(cpclHexAsString);
|
||||
|
||||
getByteBuffer().append(cpcl, charset).append(new byte[] {13, 10});
|
||||
break;
|
||||
case EVOLIS:
|
||||
try {
|
||||
ArrayList<float[]> cymkData = convertToCYMK();
|
||||
int precision = opt.optInt("precision", 128);
|
||||
|
||||
// Y,M,C,K,O ribbon
|
||||
generateRibbonData('y', precision, cymkData.get(1));
|
||||
generateRibbonData('m', precision, cymkData.get(2));
|
||||
generateRibbonData('c', precision, cymkData.get(0));
|
||||
|
||||
//K(black) and O(overlay) are always precision 2
|
||||
generateRibbonData('k', 2, cymkData.get(3));
|
||||
|
||||
if (opt.has("overlay")) {
|
||||
try { generateRibbonData('o', 2, parseOverlay(opt.get("overlay"))); }
|
||||
catch(Exception e) {
|
||||
log.error("Failed to parse overlay data: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException ioe) {
|
||||
throw new InvalidRawImageException(ioe.getMessage(), ioe);
|
||||
}
|
||||
|
||||
break;
|
||||
case SBPL:
|
||||
String sbplHexAsString = ByteUtilities.getHexString(getImageAsIntArray());
|
||||
StringBuilder sbpl = new StringBuilder("GH")
|
||||
.append(String.format("%03d", getWidth() / 8))
|
||||
.append(String.format("%03d", getHeight() / 8))
|
||||
.append(sbplHexAsString);
|
||||
|
||||
getByteBuffer().append(new byte[] {27}).append(sbpl, charset);
|
||||
break;
|
||||
case PGL:
|
||||
if(logoId.isEmpty()) {
|
||||
throw new InvalidRawImageException("Printronix graphics require a logoId");
|
||||
}
|
||||
if(igpDots) {
|
||||
// IGP images cannot exceed 240x252
|
||||
if(getWidth() > 240 || getHeight() > 252) {
|
||||
throw new InvalidRawImageException("IGP dots is enabled; Size values HL/VL cannot exceed 240x252");
|
||||
}
|
||||
}
|
||||
|
||||
// igpDots: true: Use IGP standard 60dpi/72dpi graphics (removes ";DOTS" from raw command)
|
||||
// igpDots: false: Use the printer's native resolution (appends ";DOTS" to raw command)
|
||||
StringBuilder pgl = new StringBuilder("~LOGO;").append(logoId).append(";")
|
||||
.append(getHeight()).append(";").append(getWidth()).append(igpDots ? ";DOTS" : "").append("\n")
|
||||
.append(getImageAsPGLDots())
|
||||
.append("END").append("\n");
|
||||
|
||||
getByteBuffer().append(pgl, charset);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidRawImageException(languageType + " image conversion is not yet supported.");
|
||||
}
|
||||
|
||||
return getByteBuffer().getByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the width of the image
|
||||
*/
|
||||
public int getWidth() {
|
||||
return bufferedImage.getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the height of the image
|
||||
*/
|
||||
public int getHeight() {
|
||||
return bufferedImage.getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the image as an array of booleans
|
||||
*/
|
||||
private boolean[] getImageAsBooleanArray() {
|
||||
return imageAsBooleanArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Printronix format
|
||||
* [line];[black dots range];[more black dots range][newline]
|
||||
* e.g
|
||||
* 1;1-12;19-22;38-39
|
||||
*
|
||||
*/
|
||||
private String getImageAsPGLDots() {
|
||||
StringBuilder pglDots = new StringBuilder();
|
||||
int pixelIndex = 0;
|
||||
for(int h = 1; h <= getHeight(); h++) {
|
||||
StringBuilder line = new StringBuilder();
|
||||
|
||||
int start = -1;
|
||||
int end = -1;
|
||||
|
||||
for(int w = 1; w <= getWidth(); w++) {
|
||||
if(imageAsBooleanArray[pixelIndex]) {
|
||||
System.out.print(".");
|
||||
if(start == -1) {
|
||||
start = w;
|
||||
}
|
||||
} else {
|
||||
System.out.print(" ");
|
||||
if(start != -1) {
|
||||
end = w - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle trailing pixel
|
||||
if(w == getWidth()) {
|
||||
end = w;
|
||||
}
|
||||
|
||||
if(start != -1 && end != -1) {
|
||||
if(start == end) {
|
||||
// append a single dot
|
||||
line.append(start).append(";");
|
||||
} else {
|
||||
// append a range of dots
|
||||
line.append(start).append("-").append(end).append(";");
|
||||
}
|
||||
start = -1;
|
||||
end = -1;
|
||||
}
|
||||
pixelIndex++;
|
||||
}
|
||||
System.out.print("\n");
|
||||
if(line.length() > 0) {
|
||||
// Remove trailing ";"
|
||||
if(line.charAt(line.length() -1) == ';') {
|
||||
line.replace(line.length() - 1, line.length(), "");
|
||||
}
|
||||
// Add line number
|
||||
line.insert(0, h + ";");
|
||||
|
||||
// Add to final commands
|
||||
pglDots.append(line).append("\n");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return pglDots.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param imageAsBooleanArray the imageAsBooleanArray to set
|
||||
*/
|
||||
private void setImageAsBooleanArray(boolean[] imageAsBooleanArray) {
|
||||
this.imageAsBooleanArray = imageAsBooleanArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the imageAsIntArray
|
||||
*/
|
||||
private int[] getImageAsIntArray() {
|
||||
return imageAsIntArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param imageAsIntArray the imageAsIntArray to set
|
||||
*/
|
||||
private void setImageAsIntArray(int[] imageAsIntArray) {
|
||||
this.imageAsIntArray = imageAsIntArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the ImageWrapper. This populates the internal structures with
|
||||
* the data created from the original image. It is normally called by the
|
||||
* constructor, but if for any reason you change the image contents (for
|
||||
* example, if you resize the image), it must be initialized again prior to
|
||||
* calling getImageCommand()
|
||||
*/
|
||||
private void init() {
|
||||
log.info("Initializing Image Fields");
|
||||
setImageAsBooleanArray(generateBlackPixels(bufferedImage));
|
||||
generateIntArray();
|
||||
}
|
||||
|
||||
public Charset getCharset() {
|
||||
return charset;
|
||||
}
|
||||
|
||||
public void setCharset(Charset charset) {
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the byteBuffer
|
||||
*/
|
||||
private ByteArrayBuilder getByteBuffer() {
|
||||
return byteBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the buffer
|
||||
*/
|
||||
private BufferedImage getBufferedImage() {
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param buffer the buffer to set
|
||||
*/
|
||||
private void setBufferedImage(BufferedImage buffer) {
|
||||
bufferedImage = buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* http://android-essential-devtopics.blogspot.com/2013/02/sending-bit-image-to-epson-printer.html
|
||||
* <p>
|
||||
* Images are read as one long array of black or white pixels, as scanned top to bottom and left to right.
|
||||
* Printer format needs this sent in height chunks in bytes (normally 3, for 24 pixels at a time) for each x position along a segment,
|
||||
* and repeated for each segment of height over the byte limit.
|
||||
*
|
||||
* @param builder the ByteArrayBuilder to use
|
||||
*/
|
||||
private void appendEpsonSlices(ByteArrayBuilder builder) {
|
||||
// set line height to the size of each chunk we will be sending
|
||||
int segmentHeight = dotDensity > 1 ? 24 : (dotDensity == 1 ? 8 : 16); // height will be handled explicitly below if striping
|
||||
// Impact printers (U220, etc) benefit from double-pass striping (odd/even) for higher quality (dotDensity = 1)
|
||||
boolean stripe = dotDensity == 1;
|
||||
int bytesNeeded = (dotDensity <= 1 || stripe)? 1:3;
|
||||
|
||||
if(legacyMode) {
|
||||
// Temporarily set line spacing to 24 dots
|
||||
builder.append(new byte[] { 0x1B, 0x33, 24});
|
||||
}
|
||||
|
||||
int offset = 0; // keep track of chunk offset currently being written
|
||||
boolean zeroPass = true; // track if this segment get rewritten with 1 pixel offset, always true if not striping
|
||||
|
||||
while(offset < getHeight()) {
|
||||
// compute 2 byte value of the image width (documentation states width is 'nL' + ('nH' * 256))
|
||||
byte nL = (byte)((getWidth() % 256));
|
||||
byte nH = (byte)((getWidth() / 256));
|
||||
builder.append(new byte[] {0x1B, 0x2A, (byte)dotDensity, nL, nH});
|
||||
|
||||
for(int x = 0; x < getWidth(); x++) {
|
||||
for(int bite = 0; bite < bytesNeeded; bite++) {
|
||||
byte slice = 0;
|
||||
|
||||
//iterate bit for the byte - striping spans 2 bytes (taking every other bit) to be compacted down into one
|
||||
for(int bit = (zeroPass? 0:1); bit < 8 * (stripe? 2:1); bit += (stripe? 2:1)) {
|
||||
// get the y position of the current pixel being found
|
||||
int y = offset + ((bite * 8) + bit);
|
||||
|
||||
// calculate the location of the pixel we want in the bit array and update the slice if it is supposed to be black
|
||||
int i = (y * getWidth()) + x;
|
||||
if (i < imageAsBooleanArray.length && imageAsBooleanArray[i]) {
|
||||
// append desired bit to current byte being built, remembering that bits go right to left
|
||||
slice |= (byte)(1 << (7 - (bit - (zeroPass? 0:1)) / (stripe? 2:1)));
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(new byte[] {slice});
|
||||
}
|
||||
}
|
||||
|
||||
// move print head down to next segment (or offset by one if striping)
|
||||
if (stripe) {
|
||||
if (zeroPass) {
|
||||
builder.append(new byte[] {0x1B, 0x4A, 0x01}); // only shift down 1 pixel for the next pass
|
||||
} else {
|
||||
builder.append(new byte[] {0x1B, 0x4A, (byte)(segmentHeight - 1)}); // shift down remaining pixels
|
||||
offset += 8 * bytesNeeded; // only shift offset on every other pass (along with segments)
|
||||
}
|
||||
|
||||
zeroPass = !zeroPass;
|
||||
} else {
|
||||
if(legacyMode) {
|
||||
// render a newline to bump the print head down
|
||||
builder.append(new byte[] {10});
|
||||
} else {
|
||||
//shift down for next segment
|
||||
builder.append(new byte[] {0x1B, 0x4A, (byte)segmentHeight});
|
||||
}
|
||||
offset += 8 * bytesNeeded;
|
||||
}
|
||||
}
|
||||
|
||||
if(legacyMode) {
|
||||
// Restore line spacing to 30 dots
|
||||
builder.append(new byte[] { 0x1B, 0x33, 30});
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<float[]> convertToCYMK() throws IOException {
|
||||
int[] pixels = bufferedImage.getRGB(0, 0, getWidth(), getHeight(), null, 0, getWidth());
|
||||
|
||||
float[] cyan = new float[pixels.length];
|
||||
float[] yellow = new float[pixels.length];
|
||||
float[] magenta = new float[pixels.length];
|
||||
float[] black = new float[pixels.length];
|
||||
|
||||
for(int i = 0; i < pixels.length; i++) {
|
||||
float rgb[] = new Color(pixels[i]).getRGBColorComponents(null);
|
||||
if (rgb[0] == 0.0f && rgb[1] == 0.0f && rgb[2] == 0.0f) {
|
||||
black[i] = 1.0f;
|
||||
} else {
|
||||
cyan[i] = 1.0f - rgb[0];
|
||||
magenta[i] = 1.0f - rgb[1];
|
||||
yellow[i] = 1.0f - rgb[2];
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<float[]> colorData = new ArrayList<>();
|
||||
colorData.add(cyan);
|
||||
colorData.add(yellow);
|
||||
colorData.add(magenta);
|
||||
colorData.add(black);
|
||||
|
||||
return colorData;
|
||||
}
|
||||
|
||||
private float[] parseOverlay(Object overlay) throws IOException, JSONException {
|
||||
float[] overlayData = new float[getWidth() * getHeight()];
|
||||
|
||||
if (overlay instanceof JSONArray) {
|
||||
//array of rectangles
|
||||
JSONArray masterBlock = (JSONArray)overlay;
|
||||
for(int i = 0; i < masterBlock.length(); i++) {
|
||||
JSONArray block = masterBlock.getJSONArray(i);
|
||||
if (block != null && block.length() == 4) {
|
||||
for(int y = block.getInt(1) - 1; y < block.getInt(3); y++) {
|
||||
int off = (y * getWidth());
|
||||
for(int x = block.getInt(0) - 1; x < block.getInt(2); x++) {
|
||||
if ((off + x) >= 0 && (off + x) < overlayData.length) {
|
||||
overlayData[off + x] = 1.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (overlay instanceof String) {
|
||||
//image mask
|
||||
boolean[] mask = generateBlackPixels(ImageIO.read(new URL((String)overlay)));
|
||||
for(int i = 0; i < overlayData.length; i++) {
|
||||
overlayData[i] = (mask[i]? 1.0f:0.0f);
|
||||
}
|
||||
} else if (overlay instanceof Boolean && (boolean)overlay) {
|
||||
//boolean coat
|
||||
for(int i = 0; i < overlayData.length; i++) {
|
||||
overlayData[i] = 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
return overlayData;
|
||||
}
|
||||
|
||||
private void generateRibbonData(char ribbon, int precision, float[] colorData) throws UnsupportedEncodingException {
|
||||
log.debug("Building ribbon 'Db;{};{};..'", ribbon, precision);
|
||||
|
||||
getByteBuffer().append("\u001BDb;" + ribbon + ";" + precision + ";", charset);
|
||||
getByteBuffer().append(compactBits(precision, colorData));
|
||||
getByteBuffer().append(new byte[] {0x0D});
|
||||
}
|
||||
|
||||
private ArrayList<Byte> compactBits(int precision, float[] colorData) {
|
||||
ArrayList<Byte> bytes = new ArrayList<>();
|
||||
|
||||
int bits = precisionBits(precision);
|
||||
int empty = 8 - bits;
|
||||
|
||||
for(int i = 0; i < colorData.length; i++) {
|
||||
byte b = 0;
|
||||
int captured = 0;
|
||||
|
||||
b |= byteValue(colorData[i], precision) << empty;
|
||||
captured += 8 - empty;
|
||||
|
||||
while(captured < 8 && (i + 1) < colorData.length) {
|
||||
int excess = bits - empty;
|
||||
|
||||
if (excess > 0) { //because negative shifts don't go backwards
|
||||
b |= byteValue(colorData[i + 1], precision) >> excess;
|
||||
} else {
|
||||
b |= byteValue(colorData[i + 1], precision) << Math.abs(excess);
|
||||
}
|
||||
captured += bits - Math.max(0, excess);
|
||||
if (captured < 8 && excess <= 0) { i++; } //if we've eaten an entire color point but haven't filled the byte, increase index looking at
|
||||
|
||||
empty = 8 - excess;
|
||||
if (empty > 8) { empty -= 8; } //wrap around so we never shift over a byte length
|
||||
}
|
||||
|
||||
bytes.add(b);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private int precisionBits(int precision) {
|
||||
precision--; // "128" is actually 0-127, subtract one
|
||||
int ones = 0;
|
||||
while(precision > 0) {
|
||||
if (precision % 2 != 0) { ones++; }
|
||||
precision /= 2;
|
||||
}
|
||||
|
||||
return ones;
|
||||
}
|
||||
|
||||
private byte byteValue(float value, int precision) {
|
||||
return (byte)(value * (precision - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the image width is a multiple of 8, and if it's not,
|
||||
* pads the image on the right side with blank pixels. <br />
|
||||
* Due to limitations on the EPL2 language, image widths must be a multiple
|
||||
* of 8.
|
||||
*/
|
||||
private void validateImageWidth() {
|
||||
BufferedImage oldBufferedImage = bufferedImage;
|
||||
int height = oldBufferedImage.getHeight();
|
||||
int width = oldBufferedImage.getWidth();
|
||||
if (width % 8 != 0) {
|
||||
int newWidth = (width / 8 + 1) * 8;
|
||||
BufferedImage newBufferedImage = new BufferedImage(newWidth, height,
|
||||
BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
Graphics2D g = newBufferedImage.createGraphics();
|
||||
g.drawImage(oldBufferedImage, 0, 0, null);
|
||||
g.dispose();
|
||||
setBufferedImage(newBufferedImage);
|
||||
init();
|
||||
}
|
||||
}
|
||||
}
|
||||
84
old code/tray/src/qz/printer/action/raw/LanguageType.java
Executable file
84
old code/tray/src/qz/printer/action/raw/LanguageType.java
Executable file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package qz.printer.action.raw;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Enum for print languages, such as ZPL, EPL, etc.
|
||||
*
|
||||
* @author tfino
|
||||
*/
|
||||
public enum LanguageType {
|
||||
|
||||
ZPL(false, true, 203, "ZPL", "ZPL2", "ZPLII", "ZEBRA"),
|
||||
EPL(true, true, 203, "EPL", "EPL2", "EPLII"),
|
||||
CPCL(false, true, 203),
|
||||
ESCP(false, false, 180, "ESCP", "ESCP2", "ESCPOS", "ESC", "ESC/P", "ESC/P2", "ESCP/P2", "ESC/POS", "ESC\\P", "EPSON"),
|
||||
EVOLIS(false, false, 300),
|
||||
SBPL(false, true, 203, "SATO"),
|
||||
PGL(false, false, 203, "IGP/PGL", "PRINTRONIX"),
|
||||
UNKNOWN(false, false, 72);
|
||||
|
||||
|
||||
private boolean imgOutputInvert = false;
|
||||
private boolean imgWidthValidated = false;
|
||||
private double defaultDensity = 72;
|
||||
private List<String> altNames;
|
||||
|
||||
LanguageType(boolean imgOutputInvert, boolean imgWidthValidated, double defaultDensity, String... altNames) {
|
||||
this.imgOutputInvert = imgOutputInvert;
|
||||
this.imgWidthValidated = imgWidthValidated;
|
||||
this.defaultDensity = defaultDensity;
|
||||
|
||||
this.altNames = new ArrayList<>();
|
||||
Collections.addAll(this.altNames, altNames);
|
||||
}
|
||||
|
||||
public static LanguageType getType(String type) {
|
||||
for(LanguageType lang : LanguageType.values()) {
|
||||
if (lang.name().equalsIgnoreCase(type) || lang.altNames.contains(type)) {
|
||||
return lang;
|
||||
}
|
||||
}
|
||||
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether or not this {@code LanguageType}
|
||||
* inverts the black and white pixels before sending to the printer.
|
||||
*
|
||||
* @return {@code true} if language type flips black and white pixels
|
||||
*/
|
||||
public boolean requiresImageOutputInverted() {
|
||||
return imgOutputInvert;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the specified {@code LanguageType} requires
|
||||
* the image width to be validated prior to processing output. This
|
||||
* is required for image formats that normally require the image width to
|
||||
* be a multiple of 8
|
||||
*
|
||||
* @return {@code true} if the printer requires image width validation
|
||||
*/
|
||||
public boolean requiresImageWidthValidated() {
|
||||
return imgWidthValidated;
|
||||
}
|
||||
|
||||
public double getDefaultDensity() {
|
||||
return defaultDensity;
|
||||
}
|
||||
|
||||
}
|
||||
133
old code/tray/src/qz/printer/info/CachedPrintService.java
Executable file
133
old code/tray/src/qz/printer/info/CachedPrintService.java
Executable file
@@ -0,0 +1,133 @@
|
||||
package qz.printer.info;
|
||||
|
||||
import qz.common.CachedObject;
|
||||
|
||||
import javax.print.*;
|
||||
import javax.print.attribute.Attribute;
|
||||
import javax.print.attribute.AttributeSet;
|
||||
import javax.print.attribute.PrintServiceAttribute;
|
||||
import javax.print.attribute.PrintServiceAttributeSet;
|
||||
import javax.print.event.PrintServiceAttributeListener;
|
||||
import java.util.HashMap;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* <code>PrintService.getName()</code> is slow, and gets increasingly slower the more times it's called.
|
||||
*
|
||||
* By overriding and caching the <code>PrintService</code> attributes, we're able to help suppress/delay the
|
||||
* performance loss of this bug.
|
||||
*
|
||||
* See also JDK-7001133
|
||||
*/
|
||||
public class CachedPrintService implements PrintService {
|
||||
private final PrintService printService;
|
||||
private final long lifespan;
|
||||
private final CachedObject<String> cachedName;
|
||||
private final CachedObject<PrintServiceAttributeSet> cachedAttributeSet;
|
||||
private final HashMap<Class<?>, CachedObject<?>> cachedAttributes = new HashMap<>();
|
||||
|
||||
public CachedPrintService(PrintService printService, long lifespan) {
|
||||
this.printService = printService;
|
||||
this.lifespan = lifespan;
|
||||
cachedName = new CachedObject<>(this.printService::getName, lifespan);
|
||||
cachedAttributeSet = new CachedObject<>(this.printService::getAttributes, lifespan);
|
||||
}
|
||||
|
||||
public CachedPrintService(PrintService printService) {
|
||||
this(printService, CachedObject.DEFAULT_LIFESPAN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return cachedName.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocPrintJob createPrintJob() {
|
||||
return printService.createPrintJob();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPrintServiceAttributeListener(PrintServiceAttributeListener listener) {
|
||||
printService.addPrintServiceAttributeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePrintServiceAttributeListener(PrintServiceAttributeListener listener) {
|
||||
printService.removePrintServiceAttributeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintServiceAttributeSet getAttributes() {
|
||||
return cachedAttributeSet.get();
|
||||
}
|
||||
|
||||
public PrintService getJavaxPrintService() {
|
||||
return printService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends PrintServiceAttribute> T getAttribute(Class<T> category) {
|
||||
if (!cachedAttributes.containsKey(category)) {
|
||||
Supplier<T> supplier = () -> printService.getAttribute(category);
|
||||
CachedObject<T> cachedObject = new CachedObject<>(supplier, lifespan);
|
||||
cachedAttributes.put(category, cachedObject);
|
||||
}
|
||||
return category.cast(cachedAttributes.get(category).get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public DocFlavor[] getSupportedDocFlavors() {
|
||||
return printService.getSupportedDocFlavors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDocFlavorSupported(DocFlavor flavor) {
|
||||
return printService.isDocFlavorSupported(flavor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSupportedAttributeCategories() {
|
||||
return printService.getSupportedAttributeCategories();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAttributeCategorySupported(Class<? extends Attribute> category) {
|
||||
return printService.isAttributeCategorySupported(category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDefaultAttributeValue(Class<? extends Attribute> category) {
|
||||
return printService.getDefaultAttributeValue(category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getSupportedAttributeValues(Class<? extends Attribute> category, DocFlavor flavor, AttributeSet attributes) {
|
||||
return printService.getSupportedAttributeValues(category, flavor, attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAttributeValueSupported(Attribute attrval, DocFlavor flavor, AttributeSet attributes) {
|
||||
return printService.isAttributeValueSupported(attrval, flavor, attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeSet getUnsupportedAttributes(DocFlavor flavor, AttributeSet attributes) {
|
||||
return printService.getUnsupportedAttributes(flavor, attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceUIFactory getServiceUIFactory() {
|
||||
return printService.getServiceUIFactory();
|
||||
}
|
||||
|
||||
public boolean equals(Object obj) {
|
||||
return (obj == this ||
|
||||
(obj instanceof PrintService &&
|
||||
((PrintService)obj).getName().equals(getName())));
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return this.getClass().hashCode()+getName().hashCode();
|
||||
}
|
||||
}
|
||||
81
old code/tray/src/qz/printer/info/CachedPrintServiceLookup.java
Executable file
81
old code/tray/src/qz/printer/info/CachedPrintServiceLookup.java
Executable file
@@ -0,0 +1,81 @@
|
||||
package qz.printer.info;
|
||||
|
||||
import qz.common.CachedObject;
|
||||
|
||||
import javax.print.PrintService;
|
||||
import javax.print.PrintServiceLookup;
|
||||
|
||||
/**
|
||||
* PrintService[] cache to workaround JDK-7001133
|
||||
*
|
||||
* See also <code>CachedPrintService</code>
|
||||
*/
|
||||
public class CachedPrintServiceLookup {
|
||||
private static final CachedObject<CachedPrintService> cachedDefault = new CachedObject<>(CachedPrintServiceLookup::wrapDefaultPrintService);
|
||||
private static final CachedObject<CachedPrintService[]> cachedPrintServices = new CachedObject<>(CachedPrintServiceLookup::wrapPrintServices);
|
||||
|
||||
// Keep CachedPrintService object references between calls to supplier
|
||||
private static CachedPrintService[] cachedPrintServicesCopy = {};
|
||||
|
||||
static {
|
||||
setLifespan(CachedObject.DEFAULT_LIFESPAN);
|
||||
}
|
||||
|
||||
public static PrintService lookupDefaultPrintService() {
|
||||
return cachedDefault.get();
|
||||
}
|
||||
|
||||
public static void setLifespan(long lifespan) {
|
||||
cachedDefault.setLifespan(lifespan);
|
||||
cachedPrintServices.setLifespan(lifespan);
|
||||
}
|
||||
|
||||
public static PrintService[] lookupPrintServices() {
|
||||
return cachedPrintServices.get();
|
||||
}
|
||||
|
||||
private static CachedPrintService wrapDefaultPrintService() {
|
||||
PrintService javaxPrintService = PrintServiceLookup.lookupDefaultPrintService();
|
||||
// CachedObject's supplier returns null
|
||||
if(javaxPrintService == null) {
|
||||
return null;
|
||||
}
|
||||
// If this CachedPrintService already exists, reuse it rather than wrapping a new one
|
||||
CachedPrintService cachedPrintService = getMatch(cachedPrintServicesCopy, javaxPrintService);
|
||||
if (cachedPrintService == null) {
|
||||
// Wrap a new one
|
||||
cachedPrintService = new CachedPrintService(javaxPrintService);
|
||||
}
|
||||
return cachedPrintService;
|
||||
}
|
||||
|
||||
private static CachedPrintService[] wrapPrintServices() {
|
||||
PrintService[] javaxPrintServices = PrintServiceLookup.lookupPrintServices(null, null);
|
||||
CachedPrintService[] cachedPrintServices = new CachedPrintService[javaxPrintServices.length];
|
||||
for (int i = 0; i < javaxPrintServices.length; i++) {
|
||||
// If this CachedPrintService already exists, reuse it rather than wrapping a new one
|
||||
cachedPrintServices[i] = getMatch(cachedPrintServicesCopy, javaxPrintServices[i]);
|
||||
if (cachedPrintServices[i] == null) {
|
||||
cachedPrintServices[i] = new CachedPrintService(javaxPrintServices[i]);
|
||||
}
|
||||
}
|
||||
cachedPrintServicesCopy = cachedPrintServices;
|
||||
// Immediately invalidate the defaultPrinter cache
|
||||
cachedDefault.get(true);
|
||||
|
||||
return cachedPrintServices;
|
||||
}
|
||||
|
||||
private static CachedPrintService getMatch(CachedPrintService[] array, PrintService javaxPrintService) {
|
||||
if(array != null) {
|
||||
for(CachedPrintService cps : array) {
|
||||
// Note: Order of operations can cause the defaultService pointer to differ if lookupDefaultPrintService()
|
||||
// is called before lookupPrintServices(...) because the provider will invalidate on refreshServices() if
|
||||
// "sun.java2d.print.polling" is set to "false". We're OK with this because worst case, we just
|
||||
// call "lpstat" a second time.
|
||||
if (cps.getJavaxPrintService() == javaxPrintService) return cps;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
192
old code/tray/src/qz/printer/info/CupsPrinterMap.java
Executable file
192
old code/tray/src/qz/printer/info/CupsPrinterMap.java
Executable file
@@ -0,0 +1,192 @@
|
||||
package qz.printer.info;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.PrintService;
|
||||
import javax.print.attribute.standard.PrinterResolution;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class CupsPrinterMap extends NativePrinterMap {
|
||||
private static final String DEFAULT_CUPS_DRIVER = "TEXTONLY.ppd";
|
||||
private static final String AIRPRINT_DRIVER = "AirPrint";
|
||||
private static final Logger log = LogManager.getLogger(CupsPrinterMap.class);
|
||||
private Map<NativePrinter, List<PrinterResolution>> resolutionMap = new HashMap<>();
|
||||
|
||||
public synchronized NativePrinterMap putAll(boolean exhaustive, PrintService... services) {
|
||||
ArrayList<PrintService> missing = findMissing(exhaustive, services);
|
||||
if (missing.isEmpty()) { return this; }
|
||||
|
||||
String output = "\n" + ShellUtilities.executeRaw(new String[] {"lpstat", "-l", "-p"});
|
||||
String[] devices = output.split("[\\r\\n]printer ");
|
||||
|
||||
for (String device : devices) {
|
||||
if (device.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
NativePrinter printer = null;
|
||||
String[] lines = device.split("\\r?\\n");
|
||||
for(String line : lines) {
|
||||
line = line.trim();
|
||||
if (printer == null) {
|
||||
printer = new NativePrinter(line.split("\\s+")[0]);
|
||||
printer.getDescription().set();
|
||||
printer.getDriverFile().set();
|
||||
} else {
|
||||
String match = "Description:";
|
||||
if (printer.getDescription().isNull() && line.startsWith(match)) {
|
||||
printer.setDescription(line.substring(line.indexOf(match) + match.length()).trim());
|
||||
}
|
||||
match = "Interface:";
|
||||
if (printer.getDriverFile().isNull() && line.startsWith(match)) {
|
||||
printer.setDriverFile(line.substring(line.indexOf(match) + match.length()).trim());
|
||||
}
|
||||
if (!printer.getDescription().isNull() && !printer.getDriverFile().isNull()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (PrintService service : missing) {
|
||||
if ((SystemUtilities.isMac() && printer.getDescription().equals(service.getName()))
|
||||
|| (SystemUtilities.isLinux() && printer.getPrinterId().equals(service.getName()))) {
|
||||
printer.setPrintService(service);
|
||||
missing.remove(service);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!printer.getPrintService().isNull()) {
|
||||
put(printer.getPrinterId(), printer);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
synchronized void addResolution(NativePrinter printer, PrinterResolution resolution) {
|
||||
List<PrinterResolution> resolutions = resolutionMap.get(printer);
|
||||
if(resolutions == null) {
|
||||
resolutions = new ArrayList<>();
|
||||
resolutionMap.put(printer, resolutions);
|
||||
}
|
||||
if(!resolutions.contains(resolution)) {
|
||||
resolutions.add(resolution);
|
||||
}
|
||||
}
|
||||
|
||||
synchronized List<PrinterResolution> getResolutions(NativePrinter printer) {
|
||||
if(resolutionMap.get(printer) == null) {
|
||||
fillAttributes(printer);
|
||||
}
|
||||
return resolutionMap.get(printer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object key, Object value) {
|
||||
if(value instanceof NativePrinter) {
|
||||
resolutionMap.remove(value);
|
||||
}
|
||||
return super.remove(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "*DefaultResolution" line from CUPS .ppd file
|
||||
* @param line
|
||||
* @return
|
||||
*/
|
||||
public static PrinterResolution parseDefaultResolution(String line) {
|
||||
try {
|
||||
String[] parts = line.split("x");
|
||||
int cross = Integer.parseInt(parts[0].replaceAll("\\D+", ""));
|
||||
int feed = parts.length > 1? Integer.parseInt(parts[1].replaceAll("\\D+", "")):cross;
|
||||
int type = line.toLowerCase(Locale.ENGLISH).contains("dpi")? PrinterResolution.DPI:PrinterResolution.DPCM;
|
||||
return new PrinterResolution(cross, feed, type);
|
||||
} catch(NumberFormatException nfe) {
|
||||
log.warn("Could not parse density from \"{}\"", line);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse "/HWResolution[" line from CUPS .ppd file
|
||||
* @param line
|
||||
* @return
|
||||
*/
|
||||
public static PrinterResolution parseAdditionalResolution(String line) {
|
||||
try {
|
||||
String[] parts = line.split("/HWResolution\\[")[1].split("\\D"); // split on non-digits
|
||||
int cross = Integer.parseInt(parts[0]);
|
||||
int feed = parts.length > 1? Integer.parseInt(parts[1]) : cross;
|
||||
return new PrinterResolution(cross, feed, PrinterResolution.DPI); // always dpi per https://www.cups.org/doc/spec-ppd.html
|
||||
} catch(NumberFormatException nfe) {
|
||||
log.warn("Could not parse density from \"{}\"", line, nfe);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
synchronized void fillAttributes(NativePrinter printer) {
|
||||
String options = ShellUtilities.executeRaw("lpoptions", "-p", printer.getPrinterId());
|
||||
String connection = null;
|
||||
int start;
|
||||
int end;
|
||||
String section;
|
||||
if((start = options.indexOf("device-uri=")) != -1) {
|
||||
section = options.substring(start);
|
||||
if((end = section.indexOf(' ')) > 0) {
|
||||
connection = section.substring(section.indexOf("=") + 1, end);
|
||||
} else {
|
||||
connection = section.substring(section.indexOf("=") + 1);
|
||||
}
|
||||
}
|
||||
printer.setConnection(connection);
|
||||
|
||||
if (!printer.getDriverFile().isNull()) {
|
||||
File ppdFile = new File(printer.getDriverFile().value());
|
||||
try {
|
||||
BufferedReader buffer = new BufferedReader(new FileReader(ppdFile));
|
||||
String line;
|
||||
|
||||
while((line = buffer.readLine()) != null) {
|
||||
if (line.contains("*DefaultResolution:")) {
|
||||
// Parse default printer resolution
|
||||
PrinterResolution defaultRes = parseDefaultResolution(line);
|
||||
if(defaultRes != null) {
|
||||
printer.setResolution(defaultRes);
|
||||
addResolution(printer, defaultRes);
|
||||
}
|
||||
} else if(line.contains("/HWResolution[")) {
|
||||
PrinterResolution additionalRes = parseAdditionalResolution(line);
|
||||
if(additionalRes != null) {
|
||||
addResolution(printer, additionalRes);
|
||||
}
|
||||
} else if(line.contains("*APAirPrint:")) {
|
||||
// Detect AirPrint driver
|
||||
String[] split = line.split("\\*APAirPrint:");
|
||||
String value = split[split.length - 1].replace("\"", "").trim();
|
||||
if(Boolean.parseBoolean(value)) {
|
||||
printer.setDriver(AIRPRINT_DRIVER);
|
||||
}
|
||||
} else if(printer.getDriver().isNull() && line.contains("*PCFileName:")) {
|
||||
// Parse driver name if not found through other means
|
||||
String[] split = line.split("\\*PCFileName:");
|
||||
printer.setDriver(split[split.length - 1].replace("\"", "").trim());
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Something went wrong while reading " + printer.getDriverFile());
|
||||
}
|
||||
}
|
||||
if (printer.getDriver().isNull()) {
|
||||
printer.setDriver(DEFAULT_CUPS_DRIVER);
|
||||
}
|
||||
if(resolutionMap.get(printer) == null) {
|
||||
addResolution(printer, null); // create empty list
|
||||
}
|
||||
}
|
||||
}
|
||||
209
old code/tray/src/qz/printer/info/NativePrinter.java
Executable file
209
old code/tray/src/qz/printer/info/NativePrinter.java
Executable file
@@ -0,0 +1,209 @@
|
||||
package qz.printer.info;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.PrintService;
|
||||
import javax.print.attribute.standard.Media;
|
||||
import javax.print.attribute.standard.PrinterName;
|
||||
import javax.print.attribute.standard.PrinterResolution;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class NativePrinter {
|
||||
private static final Logger log = LogManager.getLogger(NativePrinter.class);
|
||||
/**
|
||||
* Simple object wrapper allowing lazy fetching of values
|
||||
* @param <T>
|
||||
*/
|
||||
public class PrinterProperty<T> {
|
||||
T value;
|
||||
boolean set;
|
||||
|
||||
public PrinterProperty() {
|
||||
this.set = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (value == null) {
|
||||
return null;
|
||||
} else if (value instanceof String) {
|
||||
return (String)value;
|
||||
} return value.toString();
|
||||
}
|
||||
|
||||
public void set() {
|
||||
this.set = true;
|
||||
}
|
||||
|
||||
public void set(T content) {
|
||||
this.value = content;
|
||||
this.set = true;
|
||||
}
|
||||
|
||||
public T value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public boolean isSet() {
|
||||
return set;
|
||||
}
|
||||
|
||||
public boolean isNull() {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (value != null) {
|
||||
return value.equals(o);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private final String printerId;
|
||||
|
||||
private boolean outdated;
|
||||
private PrinterProperty<String> description;
|
||||
private PrinterProperty<PrintService> printService;
|
||||
private PrinterProperty<String> driver;
|
||||
private PrinterProperty<String> connection;
|
||||
private PrinterProperty<PrinterResolution> resolution;
|
||||
private PrinterProperty<String> driverFile;
|
||||
|
||||
public NativePrinter(String printerId) {
|
||||
this.printerId = printerId;
|
||||
this.description = new PrinterProperty<>();
|
||||
this.printService = new PrinterProperty<>();
|
||||
this.driverFile = new PrinterProperty<>();
|
||||
this.driver = new PrinterProperty<>();
|
||||
this.connection = new PrinterProperty<>();
|
||||
this.resolution = new PrinterProperty<>();
|
||||
this.outdated = false;
|
||||
}
|
||||
|
||||
public PrinterProperty<String> getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description.set(description);
|
||||
}
|
||||
|
||||
public PrinterProperty<String> getDriverFile() {
|
||||
return driverFile;
|
||||
}
|
||||
|
||||
public void setDriverFile(String driverFile) {
|
||||
this.driverFile.set(driverFile);
|
||||
}
|
||||
|
||||
public PrinterProperty<String> getDriver() {
|
||||
if (!driver.isSet()) {
|
||||
getDriverAttributes(this);
|
||||
}
|
||||
return driver;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
if (printService != null && printService.value() != null) {
|
||||
return printService.value().getName();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public PrinterName getLegacyName() {
|
||||
if (printService != null && printService.value() != null) {
|
||||
return printService.value().getAttribute(PrinterName.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setDriver(String driver) {
|
||||
this.driver.set(driver);
|
||||
}
|
||||
|
||||
public void setConnection(String connection) {
|
||||
this.connection.set(connection);
|
||||
}
|
||||
|
||||
public String getConnection() {
|
||||
if (!connection.isSet()) {
|
||||
getDriverAttributes(this);
|
||||
}
|
||||
return connection.value();
|
||||
}
|
||||
|
||||
public PrinterProperty<PrintService> getPrintService() {
|
||||
return printService;
|
||||
}
|
||||
|
||||
public void setPrintService(PrintService printService) {
|
||||
this.printService.set(printService);
|
||||
}
|
||||
|
||||
public String getPrinterId() {
|
||||
return printerId;
|
||||
}
|
||||
|
||||
public void setResolution(PrinterResolution resolution) {
|
||||
this.resolution.set(resolution);
|
||||
}
|
||||
|
||||
public PrinterProperty<PrinterResolution> getResolution() {
|
||||
if (!resolution.isSet()) {
|
||||
// Fetch resolution, if available
|
||||
try {
|
||||
Object resolution = printService.value().getDefaultAttributeValue(PrinterResolution.class);
|
||||
if (resolution != null) {
|
||||
this.resolution.set((PrinterResolution)resolution);
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
log.warn("Unable to obtain PrinterResolution from {}", printService.value().getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return resolution;
|
||||
}
|
||||
|
||||
public List<PrinterResolution> getResolutions() {
|
||||
PrintService ps = getPrintService().value();
|
||||
PrinterResolution[] resSupport = (PrinterResolution[])ps.getSupportedAttributeValues(PrinterResolution.class, ps.getSupportedDocFlavors()[0], null);
|
||||
if (resSupport == null || resSupport.length == 0) {
|
||||
NativePrinterMap printerMap = NativePrinterMap.getInstance();
|
||||
// CUPS doesn't report resolutions properly, instead return the values scraped from console
|
||||
if(printerMap instanceof CupsPrinterMap) {
|
||||
return ((CupsPrinterMap)printerMap).getResolutions(this);
|
||||
}
|
||||
resSupport = new PrinterResolution[]{ getResolution().value() };
|
||||
}
|
||||
|
||||
return Arrays.asList(resSupport);
|
||||
}
|
||||
|
||||
public static void getDriverAttributes(NativePrinter printer) {
|
||||
// First, perform slow JDK operations, see issues #940, #932
|
||||
printer.getPrintService().value().getSupportedAttributeValues(Media.class, null, null); // cached by JDK
|
||||
printer.getResolution();
|
||||
|
||||
// Mark properties as "found" so we don't attempt to gather them again
|
||||
printer.driver.set();
|
||||
printer.resolution.set();
|
||||
printer.connection.set();
|
||||
|
||||
// Gather properties not exposed by the JDK
|
||||
NativePrinterMap.getInstance().fillAttributes(printer);
|
||||
}
|
||||
|
||||
public boolean isOutdated() {
|
||||
return outdated;
|
||||
}
|
||||
|
||||
public void setOutdated(boolean outdated) {
|
||||
this.outdated = outdated;
|
||||
}
|
||||
}
|
||||
96
old code/tray/src/qz/printer/info/NativePrinterMap.java
Executable file
96
old code/tray/src/qz/printer/info/NativePrinterMap.java
Executable file
@@ -0,0 +1,96 @@
|
||||
package qz.printer.info;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.PrintService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public abstract class NativePrinterMap extends ConcurrentHashMap<String, NativePrinter> {
|
||||
private static final Logger log = LogManager.getLogger(NativePrinterMap.class);
|
||||
|
||||
private static NativePrinterMap instance;
|
||||
|
||||
public abstract NativePrinterMap putAll(boolean exhaustive, PrintService... services);
|
||||
|
||||
abstract void fillAttributes(NativePrinter printer);
|
||||
|
||||
public static NativePrinterMap getInstance() {
|
||||
if (instance == null) {
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
instance = new WindowsPrinterMap();
|
||||
break;
|
||||
default:
|
||||
instance = new CupsPrinterMap();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public String lookupPrinterId(String description) {
|
||||
for(Map.Entry<String,NativePrinter> entry : entrySet()) {
|
||||
NativePrinter info = entry.getValue();
|
||||
if (description.equals(info.getPrintService().value().getName())) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
log.warn("Could not find printerId for " + description);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: Despite the function's name, if <code>exhaustive</code> is true, it will treat the listing as exhaustive and remove
|
||||
* any PrintServices that are not part of this HashMap.
|
||||
*/
|
||||
public ArrayList<PrintService> findMissing(boolean exhaustive, PrintService[] services) {
|
||||
ArrayList<PrintService> serviceList = new ArrayList<>(Arrays.asList(services)); // shrinking list drastically improves performance
|
||||
|
||||
for(NativePrinter printer : values()) {
|
||||
int index = serviceList.indexOf(printer.getPrintService());
|
||||
if (index >= 0) {
|
||||
// Java's `PrintService.equals(o)` method uses getName().equals(). This causes issues if a stale PrintService has been replaced
|
||||
// by a new PrintService of the same name. For that reason, we always refresh the PrintService reference in NativePrinter.
|
||||
// See: https://github.com/qzind/tray/issues/1259
|
||||
printer.setPrintService(serviceList.get(index));
|
||||
serviceList.remove(printer.getPrintService()); // existing match
|
||||
} else {
|
||||
if(exhaustive) {
|
||||
printer.setOutdated(true); // no matches, mark to be removed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove outdated
|
||||
for (Map.Entry<String, NativePrinter> entry : entrySet()) {
|
||||
if(entry.getValue().isOutdated()) {
|
||||
remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// any remaining services are new/missing
|
||||
return serviceList;
|
||||
}
|
||||
|
||||
public boolean contains(PrintService service) {
|
||||
for (NativePrinter printer : values()) {
|
||||
if (printer.getPrintService().equals(service)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public NativePrinter get(PrintService service) {
|
||||
for (NativePrinter printer : values()) {
|
||||
if (printer.getPrintService().equals(service)) {
|
||||
return printer;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
49
old code/tray/src/qz/printer/info/WindowsPrinterMap.java
Executable file
49
old code/tray/src/qz/printer/info/WindowsPrinterMap.java
Executable file
@@ -0,0 +1,49 @@
|
||||
package qz.printer.info;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
import javax.print.PrintService;
|
||||
|
||||
import static com.sun.jna.platform.win32.WinReg.*;
|
||||
|
||||
public class WindowsPrinterMap extends NativePrinterMap {
|
||||
private static final Logger log = LogManager.getLogger(WindowsPrinterMap.class);
|
||||
|
||||
public synchronized NativePrinterMap putAll(boolean exhaustive, PrintService... services) {
|
||||
for(PrintService service : findMissing(exhaustive, services)) {
|
||||
String name = service.getName();
|
||||
if (name.equals("PageManager PDF Writer")) {
|
||||
log.warn("Printer \"{}\" is blacklisted, removing", name); // Per https://github.com/qzind/tray/issues/599
|
||||
continue;
|
||||
}
|
||||
NativePrinter printer = new NativePrinter(name);
|
||||
printer.setDescription(name);
|
||||
printer.setPrintService(service);
|
||||
put(printer.getPrinterId(), printer);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
synchronized void fillAttributes(NativePrinter printer) {
|
||||
String keyName = printer.getPrinterId().replaceAll("\\\\", ",");
|
||||
String key = "SYSTEM\\CurrentControlSet\\Control\\Print\\Printers\\" + keyName;
|
||||
String driver = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "Printer Driver");
|
||||
String port = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "Port");
|
||||
if (driver == null) {
|
||||
key = "Printers\\Connections\\" + keyName;
|
||||
String guid = WindowsUtilities.getRegString(HKEY_CURRENT_USER, key, "GuidPrinter");
|
||||
if (guid != null) {
|
||||
String serverName = keyName.replaceAll(",,(.+),.+", "$1");
|
||||
key = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Print\\Providers\\Client Side Rendering Print Provider\\Servers\\" + serverName + "\\Printers\\" + guid;
|
||||
driver = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "Printer Driver");
|
||||
// In testing, "Port" simply pointed back to the guid; assume "DsSpooler\portName" instead
|
||||
port = StringUtils.join(WindowsUtilities.getRegMultiString(HKEY_LOCAL_MACHINE, key + "\\DsSpooler", "portName"));
|
||||
}
|
||||
}
|
||||
printer.setDriver(driver);
|
||||
printer.setConnection(port);
|
||||
}
|
||||
}
|
||||
248
old code/tray/src/qz/printer/rendering/FontManager.java
Executable file
248
old code/tray/src/qz/printer/rendering/FontManager.java
Executable file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package qz.printer.rendering;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* FontManager class pulled from PDFBOX 1.8
|
||||
* with the help of Alexander Scherbatiy
|
||||
*/
|
||||
|
||||
public class FontManager {
|
||||
// HashMap with all known fonts
|
||||
private static HashMap<String,java.awt.Font> envFonts = new HashMap<>();
|
||||
private static Properties fontMapping = new Properties();
|
||||
|
||||
static {
|
||||
loadFonts();
|
||||
loadBasefontMapping();
|
||||
loadFontMapping();
|
||||
}
|
||||
|
||||
private FontManager() {}
|
||||
|
||||
/**
|
||||
* Get the font for the given fontname.
|
||||
*
|
||||
* @param font The name of the font.
|
||||
* @return The font we are looking for or a similar font or null if nothing is found.
|
||||
*/
|
||||
public static java.awt.Font getAwtFont(String font) {
|
||||
String fontname = normalizeFontname(font);
|
||||
if (envFonts.containsKey(fontname)) {
|
||||
return envFonts.get(fontname);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all available fonts from the environment.
|
||||
*/
|
||||
private static void loadFonts() {
|
||||
for(Font font : GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts()) {
|
||||
String family = normalizeFontname(font.getFamily());
|
||||
String psname = normalizeFontname(font.getPSName());
|
||||
|
||||
if (isBoldItalic(font)) {
|
||||
envFonts.put(family + "bolditalic", font);
|
||||
} else if (isBold(font)) {
|
||||
envFonts.put(family + "bold", font);
|
||||
} else if (isItalic(font)) {
|
||||
envFonts.put(family + "italic", font);
|
||||
} else {
|
||||
envFonts.put(family, font);
|
||||
}
|
||||
|
||||
if (!family.equals(psname)) {
|
||||
envFonts.put(normalizeFontname(font.getPSName()), font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the fontname.
|
||||
*
|
||||
* @param fontname The name of the font.
|
||||
* @return The normalized name of the font.
|
||||
*/
|
||||
private static String normalizeFontname(String fontname) {
|
||||
// Terminate all whitespaces, commas and hyphens
|
||||
String normalizedFontname = fontname.toLowerCase().replaceAll(" ", "").replaceAll(",", "").replaceAll("-", "");
|
||||
// Terminate trailing characters up to the "+".
|
||||
// As far as I know, these characters are used in names of embedded fonts
|
||||
// If the embedded font can't be read, we'll try to find it here
|
||||
if (normalizedFontname.contains("+")) {
|
||||
normalizedFontname = normalizedFontname.substring(normalizedFontname.indexOf("+") + 1);
|
||||
}
|
||||
// normalize all kinds of fonttypes. There are several possible version which have to be normalized
|
||||
// e.g. Arial,Bold Arial-BoldMT Helevtica-oblique ...
|
||||
boolean isBold = normalizedFontname.contains("bold");
|
||||
boolean isItalic = normalizedFontname.contains("italic") || normalizedFontname.contains("oblique");
|
||||
normalizedFontname = normalizedFontname.toLowerCase().replaceAll("bold", "")
|
||||
.replaceAll("italic", "").replaceAll("oblique", "");
|
||||
if (isBold) {
|
||||
normalizedFontname += "bold";
|
||||
}
|
||||
if (isItalic) {
|
||||
normalizedFontname += "italic";
|
||||
}
|
||||
|
||||
return normalizedFontname;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a font-mapping.
|
||||
*
|
||||
* @param font The name of the font.
|
||||
* @param mappedName The name of the mapped font.
|
||||
*/
|
||||
private static boolean addFontMapping(String font, String mappedName) {
|
||||
String fontname = normalizeFontname(font);
|
||||
// is there already a font mapping ?
|
||||
if (envFonts.containsKey(fontname)) {
|
||||
return false;
|
||||
}
|
||||
String mappedFontname = normalizeFontname(mappedName);
|
||||
// is the mapped font available ?
|
||||
if (!envFonts.containsKey(mappedFontname)) {
|
||||
return false;
|
||||
}
|
||||
envFonts.put(fontname, envFonts.get(mappedFontname));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the mapping for the well knwon font-substitutions.
|
||||
*/
|
||||
private static void loadFontMapping() {
|
||||
boolean addedMapping = true;
|
||||
// There could be some recursive mappings in the fontmapping, so that we have to
|
||||
// read the list until no more additional mapping is added to it
|
||||
while(addedMapping) {
|
||||
int counter = 0;
|
||||
Enumeration<Object> keys = fontMapping.keys();
|
||||
while(keys.hasMoreElements()) {
|
||||
String key = (String)keys.nextElement();
|
||||
if (addFontMapping(key, (String)fontMapping.get(key))) {
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
if (counter == 0) {
|
||||
addedMapping = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping for the basefonts.
|
||||
*/
|
||||
private static void loadBasefontMapping() {
|
||||
// use well known substitutions if the environments doesn't provide native fonts for the 14 standard fonts
|
||||
// Times-Roman -> Serif
|
||||
if (!addFontMapping("Times-Roman", "TimesNewRoman")) {
|
||||
addFontMapping("Times-Roman", "Serif");
|
||||
}
|
||||
if (!addFontMapping("Times-Bold", "TimesNewRoman,Bold")) {
|
||||
addFontMapping("Times-Bold", "Serif.bold");
|
||||
}
|
||||
if (!addFontMapping("Times-Italic", "TimesNewRoman,Italic")) {
|
||||
addFontMapping("Times-Italic", "Serif.italic");
|
||||
}
|
||||
if (!addFontMapping("Times-BoldItalic", "TimesNewRoman,Bold,Italic")) {
|
||||
addFontMapping("Times-BoldItalic", "Serif.bolditalic");
|
||||
}
|
||||
// Helvetica -> SansSerif
|
||||
if (!addFontMapping("Helvetica", "Helvetica")) {
|
||||
addFontMapping("Helvetica", "SansSerif");
|
||||
}
|
||||
if (!addFontMapping("Helvetica-Bold", "Helvetica,Bold")) {
|
||||
addFontMapping("Helvetica-Bold", "SansSerif.bold");
|
||||
}
|
||||
if (!addFontMapping("Helvetica-Oblique", "Helvetica,Italic")) {
|
||||
addFontMapping("Helvetica-Oblique", "SansSerif.italic");
|
||||
}
|
||||
if (!addFontMapping("Helvetica-BoldOblique", "Helvetica,Bold,Italic")) {
|
||||
addFontMapping("Helvetica-BoldOblique", "SansSerif.bolditalic");
|
||||
}
|
||||
// Courier -> Monospaced
|
||||
if (!addFontMapping("Courier", "Courier")) {
|
||||
addFontMapping("Courier", "Monospaced");
|
||||
}
|
||||
if (!addFontMapping("Courier-Bold", "Courier,Bold")) {
|
||||
addFontMapping("Courier-Bold", "Monospaced.bold");
|
||||
}
|
||||
if (!addFontMapping("Courier-Oblique", "Courier,Italic")) {
|
||||
addFontMapping("Courier-Oblique", "Monospaced.italic");
|
||||
}
|
||||
if (!addFontMapping("Courier-BoldOblique", "Courier,Bold,Italic")) {
|
||||
addFontMapping("Courier-BoldOblique", "Monospaced.bolditalic");
|
||||
}
|
||||
// some well known (??) substitutions found on fedora linux
|
||||
addFontMapping("Symbol", "StandardSymbolsL");
|
||||
addFontMapping("ZapfDingbats", "Dingbats");
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to determine if the font has both a BOLD and an ITALIC-type.
|
||||
*
|
||||
* @param font The font.
|
||||
* @return font has BOLD and ITALIC-type or not
|
||||
*/
|
||||
private static boolean isBoldItalic(java.awt.Font font) {
|
||||
return isBold(font) && isItalic(font);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to determine if the font has a BOLD-type.
|
||||
*
|
||||
* @param font The font.
|
||||
* @return font has BOLD-type or not
|
||||
*/
|
||||
private static boolean isBold(java.awt.Font font) {
|
||||
String name = font.getName().toLowerCase();
|
||||
if (name.contains("bold")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String psname = font.getPSName().toLowerCase();
|
||||
return psname.contains("bold");
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to determine if the font has an ITALIC-type.
|
||||
*
|
||||
* @param font The font.
|
||||
* @return font has ITALIC-type or not
|
||||
*/
|
||||
private static boolean isItalic(java.awt.Font font) {
|
||||
String name = font.getName().toLowerCase();
|
||||
// oblique is the same as italic
|
||||
if (name.contains("italic") || name.contains("oblique")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String psname = font.getPSName().toLowerCase();
|
||||
return psname.contains("italic") || psname.contains("oblique");
|
||||
}
|
||||
|
||||
}
|
||||
60
old code/tray/src/qz/printer/rendering/OpaqueDrawObject.java
Executable file
60
old code/tray/src/qz/printer/rendering/OpaqueDrawObject.java
Executable file
@@ -0,0 +1,60 @@
|
||||
package qz.printer.rendering;
|
||||
|
||||
import org.apache.pdfbox.contentstream.operator.MissingOperandException;
|
||||
import org.apache.pdfbox.contentstream.operator.Operator;
|
||||
import org.apache.pdfbox.contentstream.operator.graphics.GraphicsOperatorProcessor;
|
||||
import org.apache.pdfbox.cos.COSBase;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.MissingResourceException;
|
||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
// override draw object to remove any calls to show transparency
|
||||
public class OpaqueDrawObject extends GraphicsOperatorProcessor {
|
||||
|
||||
public OpaqueDrawObject() { }
|
||||
|
||||
public void process(Operator operator, List<COSBase> operands) throws IOException {
|
||||
if (operands.isEmpty()) {
|
||||
throw new MissingOperandException(operator, operands);
|
||||
} else {
|
||||
COSBase base0 = operands.get(0);
|
||||
if (base0 instanceof COSName) {
|
||||
COSName objectName = (COSName)base0;
|
||||
PDXObject xobject = context.getResources().getXObject(objectName);
|
||||
|
||||
if (xobject == null) {
|
||||
throw new MissingResourceException("Missing XObject: " + objectName.getName());
|
||||
} else {
|
||||
if (xobject instanceof PDImageXObject) {
|
||||
PDImageXObject image = (PDImageXObject)xobject;
|
||||
context.drawImage(image);
|
||||
} else if (xobject instanceof PDFormXObject) {
|
||||
try {
|
||||
context.increaseLevel();
|
||||
if (context.getLevel() <= 25) {
|
||||
PDFormXObject form = (PDFormXObject)xobject;
|
||||
context.showForm(form);
|
||||
}
|
||||
|
||||
//LOG.error("recursion is too deep, skipping form XObject");
|
||||
}
|
||||
finally {
|
||||
context.decreaseLevel();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "Do";
|
||||
}
|
||||
|
||||
}
|
||||
77
old code/tray/src/qz/printer/rendering/OpaqueGraphicStateParameters.java
Executable file
77
old code/tray/src/qz/printer/rendering/OpaqueGraphicStateParameters.java
Executable file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package qz.printer.rendering;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.pdfbox.contentstream.operator.OperatorProcessor;
|
||||
import org.apache.pdfbox.cos.COSBase;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
|
||||
import org.apache.pdfbox.contentstream.operator.Operator;
|
||||
import org.apache.pdfbox.contentstream.operator.OperatorName;
|
||||
import org.apache.pdfbox.contentstream.PDFStreamEngine;
|
||||
import org.apache.pdfbox.contentstream.operator.MissingOperandException;
|
||||
|
||||
/**
|
||||
* gs: Set parameters from graphics state parameter dictionary.
|
||||
*
|
||||
* @author Ben Litchfield
|
||||
*/
|
||||
public class OpaqueGraphicStateParameters extends OperatorProcessor
|
||||
{
|
||||
private static final Log LOG = LogFactory.getLog(OpaqueGraphicStateParameters.class);
|
||||
|
||||
@Override
|
||||
public void process(Operator operator, List<COSBase> arguments) throws IOException
|
||||
{
|
||||
if (arguments.isEmpty())
|
||||
{
|
||||
throw new MissingOperandException(operator, arguments);
|
||||
}
|
||||
COSBase base0 = arguments.get(0);
|
||||
if (!(base0 instanceof COSName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// set parameters from graphics state parameter dictionary
|
||||
COSName graphicsName = (COSName) base0;
|
||||
PDFStreamEngine context = getContext();
|
||||
PDExtendedGraphicsState gs = context.getResources().getExtGState(graphicsName);
|
||||
if (gs == null)
|
||||
{
|
||||
LOG.error("name for 'gs' operator not found in resources: /" + graphicsName.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
// PDFBOX-5605: Disable alpha for lines, etc
|
||||
gs.setNonStrokingAlphaConstant(1f);
|
||||
gs.setStrokingAlphaConstant(1f);
|
||||
|
||||
gs.copyIntoGraphicsState( context.getGraphicsState() );
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return OperatorName.SET_GRAPHICS_STATE_PARAMS;
|
||||
}
|
||||
}
|
||||
198
old code/tray/src/qz/printer/rendering/PdfFontPageDrawer.java
Executable file
198
old code/tray/src/qz/printer/rendering/PdfFontPageDrawer.java
Executable file
@@ -0,0 +1,198 @@
|
||||
package qz.printer.rendering;
|
||||
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||
import org.apache.pdfbox.pdmodel.font.*;
|
||||
import org.apache.pdfbox.rendering.PageDrawer;
|
||||
import org.apache.pdfbox.rendering.PageDrawerParameters;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.apache.pdfbox.util.Vector;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* PageDrawer overrides derived from PDFBOX 1.8
|
||||
* with the help of Alexander Scherbatiy
|
||||
*/
|
||||
|
||||
public class PdfFontPageDrawer extends PageDrawer {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PdfFontPageDrawer.class);
|
||||
|
||||
private String fallbackFont = "helvetica"; //todo - definable parameter?
|
||||
private final Map<PDFont,Font> fonts = new HashMap<>();
|
||||
|
||||
public PdfFontPageDrawer(PageDrawerParameters parameters, boolean ignoresTransparency) throws IOException {
|
||||
super(parameters);
|
||||
|
||||
if (ignoresTransparency) {
|
||||
// Note: These must match ParamPdfRenderer's OpaquePageDrawer
|
||||
addOperator(new OpaqueDrawObject());
|
||||
addOperator(new OpaqueGraphicStateParameters());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement) throws IOException {
|
||||
// fall-back to draw Glyph when awt font has not been found
|
||||
|
||||
AffineTransform at = textRenderingMatrix.createAffineTransform();
|
||||
at.concatenate(font.getFontMatrix().createAffineTransform());
|
||||
|
||||
Graphics2D graphics = getGraphics();
|
||||
setClip();
|
||||
|
||||
AffineTransform prevTx = graphics.getTransform();
|
||||
stretchNonEmbeddedFont(at, font, code, displacement);
|
||||
// Probably relates to DEFAULT_FONT_MATRIX transform from PDFont
|
||||
at.scale(100, -100);
|
||||
graphics.transform(at);
|
||||
|
||||
graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite());
|
||||
graphics.setPaint(getNonStrokingPaint());
|
||||
|
||||
Font prevFont = graphics.getFont();
|
||||
Font awtFont = getAwtFont(font);
|
||||
graphics.setFont(awtFont);
|
||||
|
||||
graphics.drawString(font.toUnicode(code), 0, 0);
|
||||
|
||||
graphics.setFont(prevFont);
|
||||
graphics.setTransform(prevTx);
|
||||
}
|
||||
|
||||
private void stretchNonEmbeddedFont(AffineTransform at, PDFont font, int code, Vector displacement) throws IOException {
|
||||
// Stretch non-embedded glyph if it does not match the height/width contained in the PDF.
|
||||
// Vertical fonts have zero X displacement, so the following code scales to 0 if we don't skip it.
|
||||
if (!font.isEmbedded() && !font.isVertical() && !font.isStandard14() && font.hasExplicitWidth(code)) {
|
||||
float fontWidth = font.getWidthFromFont(code);
|
||||
if (fontWidth > 0 && Math.abs(fontWidth - displacement.getX() * 1000) > 0.0001) {
|
||||
float pdfWidth = displacement.getX() * 1000;
|
||||
at.scale(pdfWidth / fontWidth, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Font cacheFont(PDFont font, Font awtFont) {
|
||||
fonts.put(font, awtFont);
|
||||
return awtFont;
|
||||
}
|
||||
|
||||
private Font getAwtFont(PDFont font) throws IOException {
|
||||
Font awtFont = fonts.get(font);
|
||||
|
||||
if (awtFont != null) {
|
||||
return awtFont;
|
||||
}
|
||||
|
||||
if (font instanceof PDType0Font) {
|
||||
return cacheFont(font, getPDType0AwtFont((PDType0Font)font));
|
||||
}
|
||||
|
||||
if (font instanceof PDType1Font) {
|
||||
return cacheFont(font, getPDType1AwtFont((PDType1Font)font));
|
||||
}
|
||||
|
||||
String msg = String.format("Not yet implemented: %s", font.getClass().getName());
|
||||
throw new UnsupportedOperationException(msg);
|
||||
}
|
||||
|
||||
public Font getPDType0AwtFont(PDType0Font font) throws IOException {
|
||||
Font awtFont = null;
|
||||
PDCIDFont descendantFont = font.getDescendantFont();
|
||||
|
||||
if (descendantFont != null) {
|
||||
|
||||
if (descendantFont instanceof PDCIDFontType2) {
|
||||
awtFont = getPDCIDAwtFontType2((PDCIDFontType2)descendantFont);
|
||||
}
|
||||
if (awtFont != null) {
|
||||
/*
|
||||
* Fix Oracle JVM Crashes.
|
||||
* Tested with Oracle JRE 6.0_45-b06 and 7.0_21-b11
|
||||
*/
|
||||
awtFont.canDisplay(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (awtFont == null) {
|
||||
awtFont = FontManager.getAwtFont(fallbackFont);
|
||||
log.debug("Using font {} instead of {}", awtFont.getName(), descendantFont.getFontDescriptor().getFontName());
|
||||
}
|
||||
|
||||
return awtFont.deriveFont(10f);
|
||||
}
|
||||
|
||||
private Font getPDType1AwtFont(PDType1Font font) throws IOException {
|
||||
Font awtFont = null;
|
||||
String baseFont = font.getBaseFont();
|
||||
PDFontDescriptor fd = font.getFontDescriptor();
|
||||
|
||||
if (fd != null) {
|
||||
if (fd.getFontFile() != null) {
|
||||
try {
|
||||
// create a type1 font with the embedded data
|
||||
awtFont = Font.createFont(Font.TYPE1_FONT, fd.getFontFile().createInputStream());
|
||||
}
|
||||
catch(java.awt.FontFormatException e) {
|
||||
log.debug("Can't read the embedded type1 font {}", fd.getFontName());
|
||||
}
|
||||
}
|
||||
if (awtFont == null) {
|
||||
// check if the font is part of our environment
|
||||
if (fd.getFontName() != null) {
|
||||
awtFont = FontManager.getAwtFont(fd.getFontName());
|
||||
}
|
||||
if (awtFont == null) {
|
||||
log.debug("Can't find the specified font {}", fd.getFontName());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// check if the font is part of our environment
|
||||
awtFont = FontManager.getAwtFont(baseFont);
|
||||
if (awtFont == null) {
|
||||
log.debug("Can't find the specified basefont {}", baseFont);
|
||||
}
|
||||
}
|
||||
|
||||
if (awtFont == null) {
|
||||
// we can't find anything, so we have to use the standard font
|
||||
awtFont = FontManager.getAwtFont(fallbackFont);
|
||||
log.debug("Using font {} instead", awtFont.getName());
|
||||
}
|
||||
|
||||
return awtFont.deriveFont(20f);
|
||||
}
|
||||
|
||||
public Font getPDCIDAwtFontType2(PDCIDFontType2 font) throws IOException {
|
||||
Font awtFont = null;
|
||||
PDFontDescriptor fd = font.getFontDescriptor();
|
||||
PDStream ff2Stream = fd.getFontFile2();
|
||||
|
||||
if (ff2Stream != null) {
|
||||
try {
|
||||
// create a font with the embedded data
|
||||
awtFont = Font.createFont(Font.TRUETYPE_FONT, ff2Stream.createInputStream());
|
||||
}
|
||||
catch(java.awt.FontFormatException f) {
|
||||
log.debug("Can't read the embedded font {}", fd.getFontName());
|
||||
}
|
||||
if (awtFont == null) {
|
||||
if (fd.getFontName() != null) {
|
||||
awtFont = FontManager.getAwtFont(fd.getFontName());
|
||||
}
|
||||
if (awtFont != null) {
|
||||
log.debug("Using font {} instead", awtFont.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return awtFont;
|
||||
}
|
||||
}
|
||||
78
old code/tray/src/qz/printer/status/Cups.java
Executable file
78
old code/tray/src/qz/printer/status/Cups.java
Executable file
@@ -0,0 +1,78 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import com.sun.jna.*;
|
||||
|
||||
/**
|
||||
* Created by kyle on 3/14/17.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface Cups extends Library {
|
||||
|
||||
Cups INSTANCE = Native.load("cups", Cups.class);
|
||||
|
||||
/**
|
||||
* Static class to facilitate readability of values
|
||||
*/
|
||||
class IPP {
|
||||
public static int PORT = INSTANCE.ippPort();
|
||||
public static int TAG_OPERATION = INSTANCE.ippTagValue("Operation");
|
||||
public static int TAG_URI = INSTANCE.ippTagValue("uri");
|
||||
public static int TAG_NAME = INSTANCE.ippTagValue("Name");
|
||||
public static int TAG_TEXT = INSTANCE.ippTagValue("Text");
|
||||
public static int TAG_INTEGER = INSTANCE.ippTagValue("Integer");
|
||||
public static int TAG_KEYWORD = INSTANCE.ippTagValue("keyword");
|
||||
public static int TAG_ENUM = INSTANCE.ippTagValue("enum");
|
||||
public static int TAG_SUBSCRIPTION = INSTANCE.ippTagValue("Subscription");
|
||||
public static int TAG_MIMETYPE = INSTANCE.ippTagValue("mimetype");
|
||||
public static int GET_PRINTERS = INSTANCE.ippOpValue("CUPS-Get-Printers");
|
||||
public static int GET_PRINTER_ATTRIBUTES = INSTANCE.ippOpValue("Get-Printer-Attributes");
|
||||
public static int GET_JOB_ATTRIBUTES = INSTANCE.ippOpValue("Get-Job-Attributes");
|
||||
public static int GET_SUBSCRIPTIONS = INSTANCE.ippOpValue("Get-Subscriptions");
|
||||
public static int GET_NOTIFICATIONS = INSTANCE.ippOpValue("Get-Notifications");
|
||||
public static int CREATE_PRINTER_SUBSCRIPTION = INSTANCE.ippOpValue("Create-Printer-Subscription");
|
||||
public static int CREATE_JOB_SUBSCRIPTION = INSTANCE.ippOpValue("Create-Job-Subscription");
|
||||
public static int CANCEL_SUBSCRIPTION = INSTANCE.ippOpValue("Cancel-Subscription");
|
||||
public static int GET_JOBS = INSTANCE.ippOpValue("Get-Jobs");
|
||||
public static int CANCEL_JOB = INSTANCE.ippOpValue("Cancel-Job");
|
||||
|
||||
public static final int OP_PRINT_JOB = 0x02;
|
||||
public static final int INT_ERROR = 0;
|
||||
public static final int INT_UNDEFINED = -1;
|
||||
|
||||
public static final String CUPS_FORMAT_TEXT = "application/vnd.cups-raw";
|
||||
}
|
||||
|
||||
//See https://www.cups.org/doc/api-cups.html and https://www.cups.org/doc/api-httpipp.html for usage
|
||||
|
||||
Pointer cupsEncryption();
|
||||
Pointer httpConnectEncrypt(String host, int port, Pointer encryption);
|
||||
Pointer cupsDoFileRequest(Pointer http, Pointer request, String resource, String filename);
|
||||
Pointer cupsDoRequest(Pointer http, Pointer request, String resource);
|
||||
Pointer ippNewRequest(int op);
|
||||
Pointer ippGetString(Pointer attr, int element, Pointer dataLen);
|
||||
Pointer ippFirstAttribute(Pointer ipp);
|
||||
Pointer ippNextAttribute(Pointer ipp);
|
||||
Pointer ippFindAttribute(Pointer ipp, String name, int type);
|
||||
Pointer ippFindNextAttribute(Pointer ipp, String name, int type);
|
||||
|
||||
String cupsServer();
|
||||
String ippTagString(int tag);
|
||||
String ippGetName(Pointer attr);
|
||||
String ippGetString(Pointer attr, int element, String language);
|
||||
String ippEnumString (String attrname, int enumvalue);
|
||||
|
||||
int ippPort();
|
||||
int httpAssembleURI(int encoding, Memory uri, int urilen, String sceme, String username, String host, int port, String resourcef);
|
||||
int ippTagValue(String name);
|
||||
int ippEnumValue(String attrname, String enumstring);
|
||||
int ippOpValue(String name);
|
||||
int ippAddString(Pointer ipp, int group, int tag, String name, String charset, String value);
|
||||
int ippAddStrings(Pointer ipp, int group, int tag, String name, int num_values, String language, StringArray values);
|
||||
int ippAddInteger (Pointer ipp, int group, int tag, String name, int value);
|
||||
int ippGetCount(Pointer attr);
|
||||
int ippGetValueTag(Pointer ipp);
|
||||
int ippGetInteger(Pointer attr, int element);
|
||||
|
||||
void ippDelete(Pointer ipp);
|
||||
void httpClose(Pointer http);
|
||||
}
|
||||
114
old code/tray/src/qz/printer/status/CupsStatusHandler.java
Executable file
114
old code/tray/src/qz/printer/status/CupsStatusHandler.java
Executable file
@@ -0,0 +1,114 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.printer.status.job.NativeJobStatus;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Created by kyle on 4/27/17.
|
||||
*/
|
||||
public class CupsStatusHandler extends AbstractHandler {
|
||||
private static final Logger log = LogManager.getLogger(CupsStatusHandler.class);
|
||||
|
||||
private static Cups cups = Cups.INSTANCE;
|
||||
private int lastEventNumber = 0;
|
||||
private HashMap<String, ArrayList<Status>> lastPrinterStatusMap = new HashMap<>();
|
||||
private HashMap<String, ArrayList<Status>> lastJobStatusMap = new HashMap<>();
|
||||
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
|
||||
baseRequest.setHandled(true);
|
||||
if (request.getReader().readLine() != null) {
|
||||
getNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void getNotifications() {
|
||||
Pointer response = CupsUtils.getStatuses(lastEventNumber + 1);
|
||||
|
||||
Pointer eventNumberAttr = cups.ippFindAttribute(response, "notify-sequence-number", Cups.IPP.TAG_INTEGER);
|
||||
Pointer eventTypeAttr = cups.ippFindAttribute(response, "notify-subscribed-event", Cups.IPP.TAG_KEYWORD);
|
||||
ArrayList<Status> statuses = new ArrayList<>();
|
||||
|
||||
while (eventNumberAttr != Pointer.NULL) {
|
||||
lastEventNumber = cups.ippGetInteger(eventNumberAttr, 0);
|
||||
Pointer printerNameAttr = cups.ippFindNextAttribute(response, "printer-name", Cups.IPP.TAG_NAME);
|
||||
|
||||
String printer = cups.ippGetString(printerNameAttr, 0, "");
|
||||
String eventType = cups.ippGetString(eventTypeAttr, 0, "");
|
||||
if (eventType.startsWith("job")) {
|
||||
Pointer JobIdAttr = cups.ippFindNextAttribute(response, "notify-job-id", Cups.IPP.TAG_INTEGER);
|
||||
Pointer jobStateAttr = cups.ippFindNextAttribute(response, "job-state", Cups.IPP.TAG_ENUM);
|
||||
Pointer jobNameAttr = cups.ippFindNextAttribute(response, "job-name", Cups.IPP.TAG_NAME);
|
||||
Pointer jobStateReasonsAttr = cups.ippFindNextAttribute(response, "job-state-reasons", Cups.IPP.TAG_KEYWORD);
|
||||
int jobId = cups.ippGetInteger(JobIdAttr, 0);
|
||||
String jobState = Cups.INSTANCE.ippEnumString("job-state", Cups.INSTANCE.ippGetInteger(jobStateAttr, 0));
|
||||
String jobName = cups.ippGetString(jobNameAttr, 0, "");
|
||||
// Statuses come in blocks eg. {printing, toner_low} We only want to display a status if it didn't exist in the last block
|
||||
// Get the list of statuses from the last block associated with this printer
|
||||
// '/' Is a documented invalid character for CUPS printer names. We will use that as a separator
|
||||
String jobKey = printer + "/" + jobId;
|
||||
ArrayList<Status> oldStatuses = lastJobStatusMap.getOrDefault(jobKey, new ArrayList<>());
|
||||
ArrayList<Status> newStatuses = new ArrayList<>();
|
||||
|
||||
boolean completed = false;
|
||||
int attrCount = cups.ippGetCount(jobStateReasonsAttr);
|
||||
for (int i = 0; i < attrCount; i++) {
|
||||
String reason = cups.ippGetString(jobStateReasonsAttr, i, "");
|
||||
Status pending = NativeStatus.fromCupsJobStatus(reason, jobState, printer, jobId, jobName);
|
||||
// If this status was one we didn't see last block, send it
|
||||
if (!oldStatuses.contains(pending)) statuses.add(pending);
|
||||
// If the job is complete, we need to remove it from our map
|
||||
if ((pending.getCode() == NativeJobStatus.COMPLETE) ||
|
||||
(pending.getCode() == NativeJobStatus.CANCELED)) {
|
||||
completed = true;
|
||||
}
|
||||
// regardless, remember the status for the next block
|
||||
newStatuses.add(pending);
|
||||
}
|
||||
if (completed) {
|
||||
lastJobStatusMap.remove(jobKey);
|
||||
} else {
|
||||
// Replace the old list with the new one
|
||||
lastJobStatusMap.put(jobKey, newStatuses);
|
||||
}
|
||||
} else if (eventType.startsWith("printer")) {
|
||||
Pointer printerStateAttr = cups.ippFindNextAttribute(response, "printer-state", Cups.IPP.TAG_ENUM);
|
||||
Pointer printerStateReasonsAttr = cups.ippFindNextAttribute(response, "printer-state-reasons", Cups.IPP.TAG_KEYWORD);
|
||||
String state = Cups.INSTANCE.ippEnumString("printer-state", Cups.INSTANCE.ippGetInteger(printerStateAttr, 0));
|
||||
// Statuses come in blocks eg. {printing, toner_low} We only want to display a status if it didn't exist in the last block
|
||||
// Get the list of statuses from the last block associated with this printer
|
||||
ArrayList<Status> oldStatuses = lastPrinterStatusMap.getOrDefault(printer, new ArrayList<>());
|
||||
ArrayList<Status> newStatuses = new ArrayList<>();
|
||||
|
||||
int attrCount = cups.ippGetCount(printerStateReasonsAttr);
|
||||
for (int i = 0; i < attrCount; i++) {
|
||||
String reason = cups.ippGetString(printerStateReasonsAttr, i, "");
|
||||
Status pending = NativeStatus.fromCupsPrinterStatus(reason, state, printer);
|
||||
// If this status was one we didn't see last block, send it
|
||||
if (!oldStatuses.contains(pending)) statuses.add(pending);
|
||||
// regardless, remember the status for the next block
|
||||
newStatuses.add(pending);
|
||||
}
|
||||
// Replace the old list with the new one
|
||||
lastPrinterStatusMap.put(printer, newStatuses);
|
||||
} else {
|
||||
log.debug("Unknown CUPS event type {}.", eventType);
|
||||
}
|
||||
eventNumberAttr = cups.ippFindNextAttribute(response, "notify-sequence-number", Cups.IPP.TAG_INTEGER);
|
||||
eventTypeAttr = cups.ippFindNextAttribute(response, "notify-subscribed-event", Cups.IPP.TAG_KEYWORD);
|
||||
}
|
||||
|
||||
cups.ippDelete(response);
|
||||
StatusMonitor.statusChanged(statuses.toArray(new Status[statuses.size()]));
|
||||
}
|
||||
}
|
||||
69
old code/tray/src/qz/printer/status/CupsStatusServer.java
Executable file
69
old code/tray/src/qz/printer/status/CupsStatusServer.java
Executable file
@@ -0,0 +1,69 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* Created by kyle on 4/27/17.
|
||||
*/
|
||||
public class CupsStatusServer {
|
||||
private static final Logger log = LogManager.getLogger(CupsStatusServer.class);
|
||||
|
||||
public static final List<Integer> CUPS_RSS_PORTS = Collections.unmodifiableList(Arrays.asList(Constants.CUPS_RSS_PORTS));
|
||||
|
||||
public static int cupsRSSPort = -1;
|
||||
private static Server server;
|
||||
|
||||
public static synchronized void runServer() {
|
||||
CupsUtils.clearSubscriptions();
|
||||
boolean started = false;
|
||||
for(int p = 0; p < CUPS_RSS_PORTS.size(); p++) {
|
||||
server = new Server(CUPS_RSS_PORTS.get(p));
|
||||
server.setHandler(new CupsStatusHandler());
|
||||
|
||||
try {
|
||||
server.start();
|
||||
cupsRSSPort = CUPS_RSS_PORTS.get(p);
|
||||
CupsUtils.startSubscription(cupsRSSPort);
|
||||
started = true;
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.warn("Could not start CUPS status server on port {}, using fallback port.", p);
|
||||
}
|
||||
if (started) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!started) {
|
||||
log.warn("Could not start CUPS status server. No printer status changes will be reported.");
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized boolean isRunning() {
|
||||
return server != null && server.isRunning();
|
||||
}
|
||||
|
||||
public static synchronized void stopServer() {
|
||||
if (server != null) {
|
||||
CupsUtils.freeIppObjs();
|
||||
server.setStopTimeout(10000);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
log.warn("Stopping CUPS status server");
|
||||
server.stop();
|
||||
}
|
||||
catch(Exception ex) {
|
||||
log.warn("Failed to stop CUPS status server.");
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
295
old code/tray/src/qz/printer/status/CupsUtils.java
Executable file
295
old code/tray/src/qz/printer/status/CupsUtils.java
Executable file
@@ -0,0 +1,295 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.StringArray;
|
||||
import org.eclipse.jetty.util.URIUtil;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.printer.info.NativePrinter;
|
||||
import qz.printer.status.Cups.IPP;
|
||||
|
||||
import javax.print.PrintException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Created by kyle on 5/17/17.
|
||||
*/
|
||||
public class CupsUtils {
|
||||
private static final Logger log = LogManager.getLogger(CupsUtils.class);
|
||||
|
||||
public static String USER = System.getProperty("user.name");
|
||||
public static String CHARSET = "";
|
||||
|
||||
private static Cups cups = Cups.INSTANCE;
|
||||
|
||||
private static Pointer http;
|
||||
private static int subscriptionID = IPP.INT_UNDEFINED;
|
||||
|
||||
static Pointer getCupsHttp() {
|
||||
if (http == null) http = cups.httpConnectEncrypt(cups.cupsServer(), IPP.PORT, cups.cupsEncryption());
|
||||
return http;
|
||||
}
|
||||
|
||||
static synchronized Pointer doRequest(Pointer request, String resource) {
|
||||
return cups.cupsDoRequest(getCupsHttp(), request, resource);
|
||||
}
|
||||
|
||||
static synchronized Pointer doFileRequest(Pointer request, String resource, String fileName) {
|
||||
return cups.cupsDoFileRequest(getCupsHttp(), request, resource, fileName);
|
||||
}
|
||||
|
||||
static Pointer listSubscriptions() {
|
||||
Pointer request = cups.ippNewRequest(IPP.GET_SUBSCRIPTIONS);
|
||||
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET,
|
||||
URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers/"));
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER);
|
||||
|
||||
return doRequest(request, "/");
|
||||
}
|
||||
|
||||
public static boolean sendRawFile(NativePrinter nativePrinter, File file) throws PrintException, IOException {
|
||||
Pointer fileResponse = null;
|
||||
try {
|
||||
String printer = nativePrinter == null? null:nativePrinter.getPrinterId();
|
||||
if (printer == null || printer.trim().isEmpty()) {
|
||||
throw new UnsupportedOperationException("Printer name is blank or invalid");
|
||||
}
|
||||
|
||||
Pointer request = cups.ippNewRequest(IPP.OP_PRINT_JOB);
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers/" + printer));
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER);
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_MIMETYPE, "document-format", null, IPP.CUPS_FORMAT_TEXT);
|
||||
// request is automatically closed
|
||||
fileResponse = doFileRequest(request, "/ipp/print", file.getCanonicalPath());
|
||||
|
||||
// For debugging:
|
||||
// parseResponse(fileResponse);
|
||||
if (cups.ippFindAttribute(fileResponse, "job-id", IPP.TAG_INTEGER) == Pointer.NULL) {
|
||||
Pointer statusMessage = cups.ippFindAttribute(fileResponse, "status-message", IPP.TAG_TEXT);
|
||||
if (statusMessage != Pointer.NULL) {
|
||||
String exception = Cups.INSTANCE.ippGetString(statusMessage, 0, "");
|
||||
if (exception != null && !exception.trim().isEmpty()) {
|
||||
throw new PrintException(exception);
|
||||
}
|
||||
}
|
||||
throw new PrintException("An unknown printer exception has occurred");
|
||||
}
|
||||
}
|
||||
finally{
|
||||
if (fileResponse != null) {
|
||||
cups.ippDelete(fileResponse);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all statuses relating to our subscriptionId with a sequence number greater than eventNumber
|
||||
*/
|
||||
public static Pointer getStatuses(int eventNumber) {
|
||||
Pointer request = cups.ippNewRequest(IPP.GET_NOTIFICATIONS);
|
||||
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET,
|
||||
URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/"));
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER);
|
||||
cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "notify-subscription-ids", subscriptionID);
|
||||
cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "notify-sequence-numbers", eventNumber);
|
||||
|
||||
return doRequest(request, "/");
|
||||
}
|
||||
|
||||
public static ArrayList<Status> getAllStatuses() {
|
||||
ArrayList<Status> statuses = new ArrayList<>();
|
||||
Pointer request = cups.ippNewRequest(IPP.GET_PRINTERS);
|
||||
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER);
|
||||
|
||||
Pointer response = doRequest(request, "/");
|
||||
Pointer stateAttr = cups.ippFindAttribute(response, "printer-state", IPP.TAG_ENUM);
|
||||
Pointer reasonAttr = cups.ippFindAttribute(response, "printer-state-reasons", IPP.TAG_KEYWORD);
|
||||
Pointer nameAttr = cups.ippFindAttribute(response, "printer-name", IPP.TAG_NAME);
|
||||
|
||||
while(stateAttr != Pointer.NULL) {
|
||||
|
||||
//save reasons until we have name, we need to go through the attrs in order
|
||||
String[] reasons = new String[cups.ippGetCount(reasonAttr)];
|
||||
for(int i = 0; i < reasons.length; i++) {
|
||||
reasons[i] = cups.ippGetString(reasonAttr, i, "");
|
||||
}
|
||||
String state = Cups.INSTANCE.ippEnumString("printer-state", Cups.INSTANCE.ippGetInteger(stateAttr, 0));
|
||||
String printer = cups.ippGetString(nameAttr, 0, "");
|
||||
|
||||
for(String reason : reasons) {
|
||||
statuses.add(NativeStatus.fromCupsPrinterStatus(reason, state, printer));
|
||||
}
|
||||
|
||||
//for next loop iteration
|
||||
stateAttr = cups.ippFindNextAttribute(response, "printer-state", IPP.TAG_ENUM);
|
||||
reasonAttr = cups.ippFindNextAttribute(response, "printer-state-reasons", IPP.TAG_KEYWORD);
|
||||
nameAttr = cups.ippFindNextAttribute(response, "printer-name", IPP.TAG_NAME);
|
||||
}
|
||||
|
||||
cups.ippDelete(response);
|
||||
return statuses;
|
||||
}
|
||||
|
||||
public static boolean clearSubscriptions() {
|
||||
Pointer response = listSubscriptions();
|
||||
Pointer attr = cups.ippFindAttribute(response, "notify-recipient-uri", IPP.TAG_URI);
|
||||
|
||||
while(true) {
|
||||
if (attr == Pointer.NULL) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
String data = cups.ippGetString(attr, 0, "");
|
||||
|
||||
int port = new URI(data).getPort();
|
||||
if (CupsStatusServer.CUPS_RSS_PORTS.contains(port)) {
|
||||
Pointer idAttr = cups.ippFindNextAttribute(response, "notify-subscription-id", IPP.TAG_INTEGER);
|
||||
|
||||
int id = cups.ippGetInteger(idAttr, 0);
|
||||
log.warn("Ending CUPS subscription #{}", id);
|
||||
endSubscription(id);
|
||||
}
|
||||
}
|
||||
catch(Exception ignore) { }
|
||||
|
||||
attr = cups.ippFindNextAttribute(response, "notify-recipient-uri", IPP.TAG_URI);
|
||||
}
|
||||
|
||||
cups.ippDelete(response);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void startSubscription(int rssPort) {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(CupsUtils::freeIppObjs));
|
||||
|
||||
String[] subscriptions = {"job-state-changed", "printer-state-changed"};
|
||||
Pointer request = cups.ippNewRequest(IPP.CREATE_JOB_SUBSCRIPTION);
|
||||
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET,
|
||||
URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers"));
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER);
|
||||
cups.ippAddString(request, IPP.TAG_SUBSCRIPTION, IPP.TAG_URI, "notify-recipient-uri", CHARSET,
|
||||
URIUtil.encodePath("rss://localhost:" + rssPort));
|
||||
cups.ippAddStrings(request, IPP.TAG_SUBSCRIPTION, IPP.TAG_KEYWORD, "notify-events", subscriptions.length, CHARSET,
|
||||
new StringArray(subscriptions));
|
||||
cups.ippAddInteger(request, IPP.TAG_SUBSCRIPTION, IPP.TAG_INTEGER, "notify-lease-duration", 0);
|
||||
|
||||
Pointer response = doRequest(request, "/");
|
||||
|
||||
Pointer attr = cups.ippFindAttribute(response, "notify-subscription-id", IPP.TAG_INTEGER);
|
||||
if (attr != Pointer.NULL) { subscriptionID = cups.ippGetInteger(attr, 0); }
|
||||
|
||||
cups.ippDelete(response);
|
||||
}
|
||||
|
||||
static void endSubscription(int id) {
|
||||
switch (id) {
|
||||
case IPP.INT_ERROR:
|
||||
case IPP.INT_UNDEFINED:
|
||||
return; // no subscription to end
|
||||
}
|
||||
Pointer request = cups.ippNewRequest(IPP.CANCEL_SUBSCRIPTION);
|
||||
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET,
|
||||
URIUtil.encodePath("ipp://localhost:" + IPP.PORT));
|
||||
cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "notify-subscription-id", id);
|
||||
|
||||
Pointer response = doRequest(request, "/");
|
||||
cups.ippDelete(response);
|
||||
}
|
||||
|
||||
public static ArrayList<Integer> listJobs(String printerName) {
|
||||
Pointer request = cups.ippNewRequest(IPP.GET_JOBS);
|
||||
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER);
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET,
|
||||
URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers/" + printerName));
|
||||
|
||||
Pointer response = doRequest(request, "/");
|
||||
ArrayList<Integer> ret = parseJobIds(response);
|
||||
cups.ippDelete(response);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static void cancelJob(int jobId) {
|
||||
Pointer request = cups.ippNewRequest(IPP.CANCEL_JOB);
|
||||
|
||||
cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET,
|
||||
URIUtil.encodePath("ipp://localhost:" + IPP.PORT));
|
||||
cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "job-id", jobId);
|
||||
Pointer response = doRequest(request, "/");
|
||||
cups.ippDelete(response);
|
||||
}
|
||||
|
||||
public synchronized static void freeIppObjs() {
|
||||
if (http != null) {
|
||||
endSubscription(subscriptionID);
|
||||
subscriptionID = IPP.INT_UNDEFINED;
|
||||
cups.httpClose(http);
|
||||
http = null;
|
||||
}
|
||||
}
|
||||
|
||||
static ArrayList<Integer> parseJobIds(Pointer response) {
|
||||
ArrayList<Pointer> attributes = getAttributes(response);
|
||||
ArrayList<Integer> ret = new ArrayList<>();
|
||||
for (Pointer attribute : attributes) {
|
||||
if (cups.ippGetName(attribute) != null && cups.ippGetName(attribute).equals("job-id")) {
|
||||
ret.add(cups.ippGetInteger(attribute, 0));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static ArrayList<Pointer> getAttributes(Pointer response) {
|
||||
ArrayList<Pointer> attributes = new ArrayList<>();
|
||||
Pointer attr = Cups.INSTANCE.ippFirstAttribute(response);
|
||||
while(attr != Pointer.NULL) {
|
||||
attributes.add(attr);
|
||||
attr = Cups.INSTANCE.ippNextAttribute(response);
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static void parseResponse(Pointer response) {
|
||||
ArrayList<Pointer> attributes = getAttributes(response);
|
||||
for (Pointer attribute : attributes) {
|
||||
System.out.println(parseAttr(attribute));
|
||||
}
|
||||
System.out.println("------------------------");
|
||||
}
|
||||
|
||||
static String parseAttr(Pointer attr){
|
||||
int valueTag = Cups.INSTANCE.ippGetValueTag(attr);
|
||||
int attrCount = Cups.INSTANCE.ippGetCount(attr);
|
||||
StringBuilder data = new StringBuilder();
|
||||
String attrName = Cups.INSTANCE.ippGetName(attr);
|
||||
for (int i = 0; i < attrCount; i++) {
|
||||
if (valueTag == Cups.INSTANCE.ippTagValue("Integer")) {
|
||||
data.append(Cups.INSTANCE.ippGetInteger(attr, i));
|
||||
} else if (valueTag == Cups.INSTANCE.ippTagValue("Boolean")) {
|
||||
data.append(Cups.INSTANCE.ippGetInteger(attr, i) == 1);
|
||||
} else if (valueTag == Cups.INSTANCE.ippTagValue("Enum")) {
|
||||
data.append(Cups.INSTANCE.ippEnumString(attrName, Cups.INSTANCE.ippGetInteger(attr, i)));
|
||||
} else {
|
||||
data.append(Cups.INSTANCE.ippGetString(attr, i, ""));
|
||||
}
|
||||
if (i + 1 < attrCount) {
|
||||
data.append(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (attrName == null){
|
||||
return "------------------------";
|
||||
}
|
||||
return String.format("%s: %d %s {%s}", attrName, attrCount, Cups.INSTANCE.ippTagString(valueTag), data);
|
||||
}
|
||||
}
|
||||
76
old code/tray/src/qz/printer/status/NativeStatus.java
Executable file
76
old code/tray/src/qz/printer/status/NativeStatus.java
Executable file
@@ -0,0 +1,76 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import qz.printer.status.job.CupsJobStatusMap;
|
||||
import qz.printer.status.job.NativeJobStatus;
|
||||
import qz.printer.status.job.WmiJobStatusMap;
|
||||
import qz.printer.status.printer.CupsPrinterStatusMap;
|
||||
import qz.printer.status.printer.NativePrinterStatus;
|
||||
import qz.printer.status.printer.WmiPrinterStatusMap;
|
||||
import qz.utils.ByteUtilities;
|
||||
|
||||
public interface NativeStatus {
|
||||
interface NativeMap {
|
||||
NativeStatus getParent();
|
||||
Object getRawCode();
|
||||
}
|
||||
|
||||
NativeStatus getDefault(); //static
|
||||
String name();
|
||||
Level getLevel();
|
||||
|
||||
/**
|
||||
* Printers/Jobs generally have a single status at a time however, bitwise
|
||||
* operators allow multiple statuses so we'll prepare an array to accommodate
|
||||
*/
|
||||
static Status[] fromWmiJobStatus(int bitwiseCode, String printer, int jobId, String jobName) {
|
||||
int[] rawCodes = ByteUtilities.unwind(bitwiseCode);
|
||||
NativeJobStatus[] parentCodes = new NativeJobStatus[rawCodes.length];
|
||||
for(int i = 0; i < rawCodes.length; i++) {
|
||||
parentCodes[i] = WmiJobStatusMap.match(rawCodes[i]);
|
||||
}
|
||||
|
||||
Status[] statusArray = new Status[rawCodes.length];
|
||||
for(int i = 0; i < rawCodes.length; i++) {
|
||||
statusArray[i] = new Status(parentCodes[i], printer, rawCodes[i], jobId, jobName);
|
||||
}
|
||||
return statusArray;
|
||||
}
|
||||
|
||||
static Status[] fromWmiPrinterStatus(int bitwiseCode, String printer) {
|
||||
int[] rawCodes = ByteUtilities.unwind(bitwiseCode);
|
||||
// WmiPrinterStatusMap has an explicit 0x00000000 = OK, so we'll need to shim that
|
||||
if(rawCodes.length == 0) {
|
||||
rawCodes = new int[] { (Integer)WmiPrinterStatusMap.OK.getRawCode() };
|
||||
}
|
||||
NativePrinterStatus[] parentCodes = new NativePrinterStatus[rawCodes.length];
|
||||
for(int i = 0; i < rawCodes.length; i++) {
|
||||
parentCodes[i] = WmiPrinterStatusMap.match(rawCodes[i]);
|
||||
}
|
||||
|
||||
Status[] statusArray = new Status[rawCodes.length];
|
||||
for(int i = 0; i < rawCodes.length; i++) {
|
||||
statusArray[i] = new Status(parentCodes[i], printer, rawCodes[i]);
|
||||
}
|
||||
return statusArray;
|
||||
}
|
||||
|
||||
|
||||
static Status fromCupsJobStatus(String reason, String state, String printer, int jobId, String jobName) {
|
||||
// First check known job-state-reason pairs
|
||||
NativeJobStatus cupsJobStatus = CupsJobStatusMap.matchReason(reason);
|
||||
if(cupsJobStatus == null) {
|
||||
// Don't return the raw job-state-reason if we couldn't find it mapped, return job-state instead
|
||||
return new Status(CupsJobStatusMap.matchState(state), printer, state, jobId, jobName);
|
||||
} else if(cupsJobStatus == NativeJobStatus.UNMAPPED) {
|
||||
// Still lookup the job-state, but let the user know what the unmapped job-state-reason was
|
||||
return new Status(CupsJobStatusMap.matchState(state), printer, reason, jobId, jobName);
|
||||
}
|
||||
return new Status(cupsJobStatus, printer, reason, jobId, jobName);
|
||||
}
|
||||
|
||||
|
||||
static Status fromCupsPrinterStatus(String reason, String state, String printer) {
|
||||
return CupsPrinterStatusMap.createStatus(reason, state, printer);
|
||||
}
|
||||
}
|
||||
98
old code/tray/src/qz/printer/status/Status.java
Executable file
98
old code/tray/src/qz/printer/status/Status.java
Executable file
@@ -0,0 +1,98 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import qz.printer.PrintServiceMatcher;
|
||||
import qz.printer.info.NativePrinter;
|
||||
import qz.printer.status.job.NativeJobStatus;
|
||||
import qz.printer.status.printer.NativePrinterStatus;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
/**
|
||||
* Container object for both printer and job statuses
|
||||
*/
|
||||
public class Status {
|
||||
private NativeStatus code;
|
||||
private String printer;
|
||||
private Object rawCode;
|
||||
private EventType eventType;
|
||||
private int jobId; // job statuses only
|
||||
private String jobName; // job status only
|
||||
|
||||
enum EventType {
|
||||
JOB,
|
||||
JOB_DATA,
|
||||
PRINTER;
|
||||
}
|
||||
|
||||
public Status(NativePrinterStatus code, String printer, Object rawCode) {
|
||||
this.code = code;
|
||||
this.printer = printer;
|
||||
this.rawCode = rawCode;
|
||||
this.jobId = -1;
|
||||
this.eventType = EventType.PRINTER;
|
||||
}
|
||||
|
||||
public Status(NativeJobStatus code, String printer, Object rawCode, int jobId, String jobName) {
|
||||
this.code = code;
|
||||
this.printer = printer;
|
||||
this.rawCode = rawCode;
|
||||
this.jobId = jobId;
|
||||
this.jobName = jobName;
|
||||
this.eventType = EventType.JOB;
|
||||
}
|
||||
|
||||
public String sanitizePrinterName() {
|
||||
if(!SystemUtilities.isMac()) {
|
||||
return printer;
|
||||
}
|
||||
|
||||
// On MacOS the description is used as the printer name
|
||||
NativePrinter nativePrinter = PrintServiceMatcher.matchPrinter(printer, true);
|
||||
if (nativePrinter == null) {
|
||||
// If the printer description is missing from the map (usually because the printer was deleted), use the cups id instead
|
||||
return printer;
|
||||
}
|
||||
return nativePrinter.getPrintService().value().getName();
|
||||
}
|
||||
|
||||
public NativeStatus getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public Object getRawCode() {
|
||||
return rawCode;
|
||||
}
|
||||
|
||||
public String getPrinter() {
|
||||
return printer;
|
||||
}
|
||||
|
||||
public EventType getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
public String getJobName() {
|
||||
return jobName;
|
||||
}
|
||||
|
||||
public int getJobId() {
|
||||
return jobId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if(obj != null && obj instanceof Status) {
|
||||
Status status = (Status)obj;
|
||||
return status.eventType == eventType && status.printer.equals(printer) && status.jobId == jobId && rawCode.equals(status.rawCode);
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return code.name() + ": Level: " + code.getLevel() +
|
||||
", From: " + sanitizePrinterName() +
|
||||
", EventType: " + eventType +
|
||||
", Code: " + rawCode +
|
||||
(jobId > 0 ? ", JobId: " + jobId : "") +
|
||||
(jobName != null ? ", Job Name: " + jobName : "");
|
||||
}
|
||||
}
|
||||
230
old code/tray/src/qz/printer/status/StatusMonitor.java
Executable file
230
old code/tray/src/qz/printer/status/StatusMonitor.java
Executable file
@@ -0,0 +1,230 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import com.sun.jna.platform.win32.Winspool;
|
||||
import com.sun.jna.platform.win32.WinspoolUtil;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.eclipse.jetty.util.MultiMap;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import qz.printer.PrintServiceMatcher;
|
||||
import qz.printer.info.NativePrinterMap;
|
||||
import qz.utils.PrintingUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.util.*;
|
||||
|
||||
import static qz.utils.SystemUtilities.isWindows;
|
||||
|
||||
/**
|
||||
* Created by Kyle on 2/23/2017.
|
||||
*/
|
||||
public class StatusMonitor {
|
||||
private static final Logger log = LogManager.getLogger(StatusMonitor.class);
|
||||
|
||||
public static final String ALL_PRINTERS = "";
|
||||
|
||||
private static Thread printerConnectionsThread;
|
||||
private static Thread statusEventDispatchThread;
|
||||
private static final HashMap<String,Thread> notificationThreadCollection = new HashMap<>();
|
||||
private static final HashMap<SocketConnection, StatusSession> statusSessions = new HashMap<>();
|
||||
private static final MultiMap<SocketConnection> clientPrinterConnections = new MultiMap<>();
|
||||
private static final LinkedList<Status> statusQueue = new LinkedList<>();
|
||||
|
||||
public synchronized static boolean launchNotificationThreads() {
|
||||
ArrayList<String> printerNameList = new ArrayList<>();
|
||||
|
||||
Winspool.PRINTER_INFO_2[] printers = WinspoolUtil.getAllPrinterInfo2();
|
||||
for (Winspool.PRINTER_INFO_2 printer : printers) {
|
||||
printerNameList.add(printer.pPrinterName);
|
||||
if (!notificationThreadCollection.containsKey(printer.pPrinterName)) {
|
||||
Thread notificationThread = new WmiPrinterStatusThread(printer);
|
||||
notificationThreadCollection.put(printer.pPrinterName, notificationThread);
|
||||
notificationThread.start();
|
||||
}
|
||||
}
|
||||
//interrupt threads that don't have associated printers
|
||||
for (Map.Entry<String,Thread> e : notificationThreadCollection.entrySet()) {
|
||||
if (!printerNameList.contains(e.getKey())) {
|
||||
e.getValue().interrupt();
|
||||
notificationThreadCollection.remove(e.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
if (printerConnectionsThread == null) {
|
||||
printerConnectionsThread = new WmiPrinterConnectionsThread();
|
||||
printerConnectionsThread.start();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized static void relaunchThreads() {
|
||||
launchNotificationThreads();
|
||||
}
|
||||
|
||||
public synchronized static void closeNotificationThreads() {
|
||||
for (Thread t : notificationThreadCollection.values()) {
|
||||
t.interrupt();
|
||||
}
|
||||
notificationThreadCollection.clear();
|
||||
|
||||
if (printerConnectionsThread != null) {
|
||||
printerConnectionsThread.interrupt();
|
||||
printerConnectionsThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized static boolean isListening(SocketConnection connection) {
|
||||
return statusSessions.containsKey(connection);
|
||||
}
|
||||
|
||||
public synchronized static boolean startListening(SocketConnection connection, Session session, JSONObject params) throws JSONException {
|
||||
JSONArray printerNames = params.getJSONArray("printerNames");
|
||||
statusSessions.putIfAbsent(connection, new StatusSession(session));
|
||||
|
||||
if (printerNames.isNull(0)) { //listen to all printers
|
||||
addClientPrinterConnection(ALL_PRINTERS, connection, params);
|
||||
} else { // listen to specific printer(s)
|
||||
for (int i = 0; i < printerNames.length(); i++) {
|
||||
String printerName = printerNames.getString(i);
|
||||
if (SystemUtilities.isMac()) printerName = macNameFix(printerName);
|
||||
|
||||
if (printerName == null || printerName.equals("")) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
addClientPrinterConnection(printerName, connection, params);
|
||||
}
|
||||
}
|
||||
|
||||
if (isWindows()) {
|
||||
return launchNotificationThreads();
|
||||
} else {
|
||||
if (!CupsStatusServer.isRunning()) { CupsStatusServer.runServer(); }
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized static void stopListening(SocketConnection connection) {
|
||||
statusSessions.remove(connection);
|
||||
closeListener(connection);
|
||||
}
|
||||
|
||||
private synchronized static void addClientPrinterConnection(String printerName, SocketConnection connection, JSONObject params) {
|
||||
boolean jobData = params.optBoolean("jobData", false);
|
||||
int maxJobData = params.optInt("maxJobData", -1);
|
||||
PrintingUtilities.Flavor dataFlavor = PrintingUtilities.Flavor.parse(params, PrintingUtilities.Flavor.PLAIN);
|
||||
|
||||
if (jobData) {
|
||||
statusSessions.get(connection).enableJobDataOnPrinter(printerName, maxJobData, dataFlavor);
|
||||
}
|
||||
if (!clientPrinterConnections.containsKey(printerName)) {
|
||||
clientPrinterConnections.add(printerName, connection);
|
||||
} else if (!clientPrinterConnections.getValues(printerName).contains(connection)) {
|
||||
clientPrinterConnections.add(printerName, connection);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized static void sendStatuses(SocketConnection connection) {
|
||||
boolean sendForAllPrinters = false;
|
||||
ArrayList<Status> statuses = isWindows() ? WmiPrinterStatusThread.getAllStatuses(): CupsUtils.getAllStatuses();
|
||||
|
||||
// First check if we're listening on all printers for this connection
|
||||
List<SocketConnection> connections = clientPrinterConnections.get(ALL_PRINTERS);
|
||||
if (connections != null) {
|
||||
sendForAllPrinters = connections.contains(connection);
|
||||
}
|
||||
|
||||
for (Status status : statuses) {
|
||||
if (sendForAllPrinters) {
|
||||
statusSessions.get(connection).statusChanged(status, () -> stopListening(connection));
|
||||
} else {
|
||||
// Only send the status of the printers requested
|
||||
connections = clientPrinterConnections.get(status.getPrinter());
|
||||
if ((connections != null) && connections.contains(connection)) {
|
||||
statusSessions.get(connection).statusChanged(status, () -> stopListening(connection));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized static void closeListener(SocketConnection connection) {
|
||||
clientPrinterConnections.entrySet().removeIf((Map.Entry<String, List<SocketConnection>> entry) -> (
|
||||
entry.getValue().contains(connection)
|
||||
));
|
||||
if (clientPrinterConnections.isEmpty()) {
|
||||
if (isWindows()) {
|
||||
closeNotificationThreads();
|
||||
} else {
|
||||
CupsStatusServer.stopServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized static void launchStatusEventDispatchThread() {
|
||||
// Null is our main test to see if the thread needs to restart. If the thread was suspended, it won't be null, so check to see if it is alive as well.
|
||||
if (statusEventDispatchThread != null && statusEventDispatchThread.isAlive()) return;
|
||||
statusEventDispatchThread = new Thread(() -> {
|
||||
while (!Thread.currentThread().isInterrupted() && dispatchStatusEvent()) {
|
||||
// If we don't yield, this will constantly run dispatchStatusEvent and lock up the class, even though this thread isn't synchronized.
|
||||
Thread.yield();
|
||||
}
|
||||
if (Thread.currentThread().isInterrupted()) log.warn("statusEventDispatchThread Interrupted");
|
||||
}, "statusEventDispatchThread");
|
||||
statusEventDispatchThread.start();
|
||||
}
|
||||
|
||||
public synchronized static void statusChanged(Status[] statuses) {
|
||||
// Add statuses to the queue, statusEventDispatchThread will resolve these one at a time until the queue is empty
|
||||
Collections.addAll(statusQueue, statuses);
|
||||
if (!statusQueue.isEmpty()) {
|
||||
// If statusEventDispatchThread isn't already running, launch it
|
||||
launchStatusEventDispatchThread();
|
||||
}
|
||||
}
|
||||
|
||||
// This is the main body of the statusEventDispatchThread.
|
||||
// Dispatch one status event to n clients connection, based on clientPrinterConnections
|
||||
// Returns false when there are no more statuses in the queue
|
||||
private synchronized static boolean dispatchStatusEvent() {
|
||||
if (statusQueue.isEmpty()) {
|
||||
// Returning false will kill statusEventDispatchThread, but we also want to null out the value while we are still in a synchronized method
|
||||
statusEventDispatchThread = null;
|
||||
return false;
|
||||
}
|
||||
Status status = statusQueue.removeFirst();
|
||||
|
||||
HashSet<SocketConnection> listeningConnections = new HashSet<>();
|
||||
if (clientPrinterConnections.containsKey(status.getPrinter())) {
|
||||
// Find every client that subscribed to this printer
|
||||
listeningConnections.addAll(clientPrinterConnections.get(status.getPrinter()));
|
||||
}
|
||||
if (clientPrinterConnections.containsKey(ALL_PRINTERS)) {
|
||||
// And find every client that subscribed to all printers
|
||||
listeningConnections.addAll(clientPrinterConnections.get(ALL_PRINTERS));
|
||||
}
|
||||
|
||||
// Notify each client subscription
|
||||
for (SocketConnection connection : listeningConnections) {
|
||||
statusSessions.get(connection).statusChanged(status, () -> stopListening(connection));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String macNameFix(String printerName) {
|
||||
// Since 2.0: Mac printers use descriptions as printer names; Find CUPS ID by Description
|
||||
String returnString = NativePrinterMap.getInstance().lookupPrinterId(printerName);
|
||||
// Handle edge-case where printer was recently renamed/added
|
||||
if (returnString == null) {
|
||||
// Call PrintServiceLookup.lookupPrintServices again
|
||||
PrintServiceMatcher.getNativePrinterList(true);
|
||||
returnString = NativePrinterMap.getInstance().lookupPrinterId(printerName);
|
||||
}
|
||||
return returnString;
|
||||
}
|
||||
}
|
||||
136
old code/tray/src/qz/printer/status/StatusSession.java
Executable file
136
old code/tray/src/qz/printer/status/StatusSession.java
Executable file
@@ -0,0 +1,136 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import qz.App;
|
||||
import qz.printer.status.job.WmiJobStatusMap;
|
||||
import qz.utils.*;
|
||||
import qz.ws.PrintSocketClient;
|
||||
import qz.ws.StreamEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static qz.printer.status.StatusMonitor.ALL_PRINTERS;
|
||||
|
||||
public class StatusSession {
|
||||
private static final Logger log = LogManager.getLogger(StatusSession.class);
|
||||
private Session session;
|
||||
private HashMap<String, Spooler> printerSpoolerMap = new HashMap<>();
|
||||
|
||||
private class Spooler implements Cloneable {
|
||||
public Path path;
|
||||
public int maxJobData;
|
||||
public PrintingUtilities.Flavor dataFlavor;
|
||||
|
||||
public Spooler() {
|
||||
this(null, -1, PrintingUtilities.Flavor.PLAIN);
|
||||
}
|
||||
|
||||
public Spooler(Path path, int maxJobData, PrintingUtilities.Flavor dataFlavor) {
|
||||
this.path = path;
|
||||
this.maxJobData = maxJobData;
|
||||
this.dataFlavor = dataFlavor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spooler clone() {
|
||||
return new Spooler(path, maxJobData, dataFlavor);
|
||||
}
|
||||
}
|
||||
|
||||
public StatusSession(Session session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public void statusChanged(Status status, Runnable closeHandler) {
|
||||
PrintSocketClient.sendStream(session, createStatusStream(status), closeHandler);
|
||||
// If this statusSession has printers flagged to return jobData, issue a jobData event after any 'retained' job events
|
||||
if (status.getCode() == WmiJobStatusMap.RETAINED.getParent() && isDataPrinter(status.getPrinter())) {
|
||||
PrintSocketClient.sendStream(session, createJobDataStream(status), closeHandler);
|
||||
}
|
||||
}
|
||||
|
||||
public void enableJobDataOnPrinter(String printer, int maxJobData, PrintingUtilities.Flavor dataFlavor) throws UnsupportedOperationException {
|
||||
if (!SystemUtilities.isWindows()) {
|
||||
throw new UnsupportedOperationException("Job data listeners are only supported on Windows");
|
||||
}
|
||||
if (!PrefsSearch.getBoolean(ArgValue.PRINTER_STATUS_JOB_DATA, false, App.getTrayProperties())) {
|
||||
throw new UnsupportedOperationException("Job data listeners are currently disabled");
|
||||
}
|
||||
if (printerSpoolerMap.containsKey(printer)) {
|
||||
printerSpoolerMap.get(printer).maxJobData = maxJobData;
|
||||
} else {
|
||||
// Lookup spooler path lazily
|
||||
printerSpoolerMap.put(printer, new Spooler(null, maxJobData, dataFlavor));
|
||||
}
|
||||
if (printer.equals(ALL_PRINTERS)) {
|
||||
// If we have started job-data listening on all printer, the new parameters need to be added to all existing printers
|
||||
for(Map.Entry<String, Spooler> entry : printerSpoolerMap.entrySet()) {
|
||||
entry.getValue().maxJobData = maxJobData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private StreamEvent createJobDataStream(Status status) {
|
||||
StreamEvent streamEvent = new StreamEvent(StreamEvent.Stream.PRINTER, StreamEvent.Type.ACTION)
|
||||
.withData("printerName", status.sanitizePrinterName())
|
||||
.withData("eventType", Status.EventType.JOB_DATA)
|
||||
.withData("jobID", status.getJobId())
|
||||
.withData("jobName", status.getJobName())
|
||||
.withData("data", getJobData(status.getJobId(), status.getPrinter()));
|
||||
return streamEvent;
|
||||
}
|
||||
|
||||
private StreamEvent createStatusStream(Status status) {
|
||||
StreamEvent streamEvent = new StreamEvent(StreamEvent.Stream.PRINTER, StreamEvent.Type.ACTION)
|
||||
.withData("printerName", status.sanitizePrinterName())
|
||||
.withData("eventType", status.getEventType())
|
||||
.withData("statusText", status.getCode().name())
|
||||
.withData("severity", status.getCode().getLevel())
|
||||
.withData("statusCode", status.getRawCode())
|
||||
.withData("message", status.toString());
|
||||
if(status.getJobId() > 0) {
|
||||
streamEvent.withData("jobId", status.getJobId());
|
||||
}
|
||||
if(status.getJobName() != null) {
|
||||
streamEvent.withData("jobName", status.getJobName());
|
||||
}
|
||||
return streamEvent;
|
||||
}
|
||||
|
||||
private String getJobData(int jobId, String printer) {
|
||||
String data = null;
|
||||
try {
|
||||
if (!printerSpoolerMap.containsKey(printer)) {
|
||||
// If not listening on this printer, assume we're listening on ALL_PRINTERS
|
||||
Spooler spooler;
|
||||
if(printerSpoolerMap.containsKey(ALL_PRINTERS)) {
|
||||
spooler = printerSpoolerMap.get(ALL_PRINTERS).clone();
|
||||
} else {
|
||||
// we should never get here
|
||||
spooler = new Spooler();
|
||||
}
|
||||
printerSpoolerMap.put(printer, spooler);
|
||||
}
|
||||
Spooler spooler = printerSpoolerMap.get(printer);
|
||||
if (spooler.path == null) spooler.path = WindowsUtilities.getSpoolerLocation(printer);
|
||||
if (spooler.maxJobData != -1 && Files.size(spooler.path) > spooler.maxJobData) {
|
||||
throw new IOException("File too large, omitting result. Size:" + Files.size(spooler.path) + " MaxJobData:" + spooler.maxJobData);
|
||||
}
|
||||
data = spooler.dataFlavor.toString(Files.readAllBytes(spooler.path.resolve(String.format("%05d", jobId) + ".SPL")));
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.error("Failed to retrieve job data from job #{}", jobId, e);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private boolean isDataPrinter(String printer) {
|
||||
return (printerSpoolerMap.containsKey(ALL_PRINTERS) || printerSpoolerMap.containsKey(printer));
|
||||
}
|
||||
}
|
||||
52
old code/tray/src/qz/printer/status/WmiPrinterConnectionsThread.java
Executable file
52
old code/tray/src/qz/printer/status/WmiPrinterConnectionsThread.java
Executable file
@@ -0,0 +1,52 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import com.sun.jna.platform.win32.Winspool;
|
||||
import com.sun.jna.platform.win32.WinspoolUtil;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public class WmiPrinterConnectionsThread extends Thread {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(WmiPrinterConnectionsThread.class);
|
||||
|
||||
private boolean running = true;
|
||||
|
||||
public WmiPrinterConnectionsThread() {
|
||||
super("Printer Connection Monitor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Winspool.PRINTER_INFO_1[] currentPrinterList = WinspoolUtil.getPrinterInfo1();
|
||||
|
||||
while(running) {
|
||||
try { sleep(1000); } catch(Exception ignore) {}
|
||||
|
||||
Winspool.PRINTER_INFO_1[] newPrinterList = WinspoolUtil.getPrinterInfo1();
|
||||
|
||||
if (!arrayEquiv(currentPrinterList, newPrinterList)) {
|
||||
StatusMonitor.relaunchThreads();
|
||||
}
|
||||
|
||||
currentPrinterList = newPrinterList;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean arrayEquiv(Winspool.PRINTER_INFO_1[] a, Winspool.PRINTER_INFO_1[] b) {
|
||||
if (a.length != b.length) { return false; }
|
||||
|
||||
for(int i = 0; i < a.length; i++) {
|
||||
if (!a[i].pName.equals(b[i].pName)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
running = false;
|
||||
super.interrupt();
|
||||
}
|
||||
}
|
||||
268
old code/tray/src/qz/printer/status/WmiPrinterStatusThread.java
Executable file
268
old code/tray/src/qz/printer/status/WmiPrinterStatusThread.java
Executable file
@@ -0,0 +1,268 @@
|
||||
package qz.printer.status;
|
||||
|
||||
import com.sun.jna.Structure;
|
||||
import com.sun.jna.platform.win32.*;
|
||||
import com.sun.jna.ptr.PointerByReference;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.printer.status.job.WmiJobStatusMap;
|
||||
import qz.printer.status.printer.NativePrinterStatus;
|
||||
import qz.printer.status.printer.WmiPrinterStatusMap;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static qz.printer.status.printer.WmiPrinterStatusMap.*;
|
||||
|
||||
public class WmiPrinterStatusThread extends Thread {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(StatusMonitor.class);
|
||||
private final Winspool spool = Winspool.INSTANCE;
|
||||
private final String printerName;
|
||||
private final HashMap<Integer, String> docNames = new HashMap<>();
|
||||
private final HashMap<Integer, ArrayList<Integer>> pendingJobStatuses = new HashMap<>();
|
||||
private final HashMap<Integer, Integer> lastJobStatusCodes = new HashMap<>();
|
||||
|
||||
private boolean holdsJobs;
|
||||
private int statusField;
|
||||
private int attributeField;
|
||||
// Last "combined" printer status, see also combineStatus()
|
||||
private int lastPrinterStatus;
|
||||
|
||||
private boolean wasOk = false;
|
||||
private boolean closing = false;
|
||||
|
||||
private WinNT.HANDLE hChangeObject;
|
||||
private WinDef.DWORDByReference pdwChangeResult;
|
||||
|
||||
Winspool.PRINTER_NOTIFY_OPTIONS listenOptions;
|
||||
Winspool.PRINTER_NOTIFY_OPTIONS statusOptions;
|
||||
|
||||
// Honor translated strings, if available
|
||||
private static final ArrayList<String> invalidNames = new ArrayList<>();
|
||||
static {
|
||||
try {
|
||||
invalidNames.add(User32Util.loadString("%SystemRoot%\\system32\\localspl.dll,108"));
|
||||
invalidNames.add(User32Util.loadString("%SystemRoot%\\system32\\localspl.dll,107"));
|
||||
} catch(Exception e) {
|
||||
log.warn("Unable to obtain strings, defaulting to en-US values.", e);
|
||||
invalidNames.add("Local Downlevel Document");
|
||||
invalidNames.add("Remote Downlevel Document");
|
||||
}
|
||||
}
|
||||
|
||||
public WmiPrinterStatusThread(Winspool.PRINTER_INFO_2 printerInfo2) {
|
||||
super("Printer Status Monitor " + printerInfo2.pPrinterName);
|
||||
printerName = printerInfo2.pPrinterName;
|
||||
holdsJobs = (printerInfo2.Attributes & Winspool.PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS) > 0;
|
||||
statusField = printerInfo2.Status;
|
||||
attributeField = printerInfo2.Attributes;
|
||||
lastPrinterStatus = combineStatus(statusField, attributeField);
|
||||
|
||||
listenOptions = new Winspool.PRINTER_NOTIFY_OPTIONS();
|
||||
listenOptions.Version = 2;
|
||||
listenOptions.Flags = Winspool.PRINTER_NOTIFY_OPTIONS_REFRESH;
|
||||
listenOptions.Count = 2;
|
||||
|
||||
Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[] mem = (Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[])
|
||||
new Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference().toArray(2);
|
||||
mem[0].Type = Winspool.JOB_NOTIFY_TYPE;
|
||||
mem[0].setFields(new short[] {Winspool.JOB_NOTIFY_FIELD_STATUS, Winspool.JOB_NOTIFY_FIELD_DOCUMENT });
|
||||
mem[1].Type = Winspool.PRINTER_NOTIFY_TYPE;
|
||||
mem[1].setFields(new short[] {Winspool.PRINTER_NOTIFY_FIELD_STATUS, Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES });
|
||||
listenOptions.pTypes = mem[0];
|
||||
|
||||
statusOptions = new Winspool.PRINTER_NOTIFY_OPTIONS();
|
||||
statusOptions.Version = 2;
|
||||
// Status option 'refresh' leads to a loss of data associated with our lock. I don't know why.
|
||||
// statusOptions.Flags = Winspool.PRINTER_NOTIFY_OPTIONS_REFRESH;
|
||||
statusOptions.Count = 2;
|
||||
|
||||
mem = (Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[])
|
||||
new Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference().toArray(2);
|
||||
mem[0].Type = Winspool.JOB_NOTIFY_TYPE;
|
||||
mem[0].setFields(new short[] { Winspool.JOB_NOTIFY_FIELD_STATUS, Winspool.JOB_NOTIFY_FIELD_DOCUMENT });
|
||||
mem[1].Type = Winspool.PRINTER_NOTIFY_TYPE;
|
||||
mem[1].setFields(new short[] { Winspool.PRINTER_NOTIFY_FIELD_STATUS, Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES });
|
||||
statusOptions.pTypes = mem[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
attachToSystem();
|
||||
|
||||
if (hChangeObject != null) {
|
||||
while(!closing) {
|
||||
waitOnChange();
|
||||
if (closing) { break; }
|
||||
ingestChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void attachToSystem() {
|
||||
WinNT.HANDLEByReference phPrinterObject = new WinNT.HANDLEByReference();
|
||||
spool.OpenPrinter(printerName, phPrinterObject, null);
|
||||
|
||||
pdwChangeResult = new WinDef.DWORDByReference();
|
||||
//The second param determines what kind of event releases our lock
|
||||
//See https://msdn.microsoft.com/en-us/library/windows/desktop/dd162722(v=vs.85).aspx
|
||||
hChangeObject = spool.FindFirstPrinterChangeNotification(phPrinterObject.getValue(), Winspool.PRINTER_CHANGE_JOB, 0, listenOptions);
|
||||
}
|
||||
|
||||
private void waitOnChange() {
|
||||
Kernel32.INSTANCE.WaitForSingleObject(hChangeObject, WinBase.INFINITE);
|
||||
}
|
||||
|
||||
private void ingestChange() {
|
||||
PointerByReference dataPointer = new PointerByReference();
|
||||
if (spool.FindNextPrinterChangeNotification(hChangeObject, pdwChangeResult, statusOptions, dataPointer)) {
|
||||
// Many events fire with dataPointer == null, see also https://stackoverflow.com/questions/16283827
|
||||
if (dataPointer.getValue() != null) {
|
||||
Winspool.PRINTER_NOTIFY_INFO data = Structure.newInstance(Winspool.PRINTER_NOTIFY_INFO.class, dataPointer.getValue());
|
||||
data.read();
|
||||
|
||||
for (Winspool.PRINTER_NOTIFY_INFO_DATA d: data.aData) {
|
||||
decodeStatus(d);
|
||||
}
|
||||
sendPendingStatuses();
|
||||
Winspool.INSTANCE.FreePrinterNotifyInfo(data.getPointer());
|
||||
}
|
||||
} else {
|
||||
issueError();
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeStatus(Winspool.PRINTER_NOTIFY_INFO_DATA d) {
|
||||
if (d.Type == Winspool.PRINTER_NOTIFY_TYPE) {
|
||||
if (d.Field == Winspool.PRINTER_NOTIFY_FIELD_STATUS) { // Printer Status Changed
|
||||
statusField = d.NotifyData.adwData[0];
|
||||
} else if (d.Field == Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES) { // Printer Attributes Changed
|
||||
attributeField = d.NotifyData.adwData[0];
|
||||
holdsJobs = (d.NotifyData.adwData[0] & Winspool.PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS) != 0;
|
||||
} else {
|
||||
log.warn("Unknown event field {}", d.Field);
|
||||
}
|
||||
|
||||
int combinedStatus = combineStatus(statusField, attributeField);
|
||||
if (combinedStatus != lastPrinterStatus) {
|
||||
Status[] statuses = NativeStatus.fromWmiPrinterStatus(combinedStatus, printerName);
|
||||
StatusMonitor.statusChanged(statuses);
|
||||
|
||||
// If the printer was in an error state before and is not now, send an 'OK'
|
||||
boolean isOk = (combinedStatus & NOT_OK_MASK) == 0;
|
||||
if (isOk && !wasOk) {
|
||||
// If the status is 0x00000000, fromWmiPrinterStatus returns 'OK'. We don't want to send a duplicate.
|
||||
if (combinedStatus != 0) StatusMonitor.statusChanged(new Status[]{new Status(NativePrinterStatus.OK, printerName, 0)});
|
||||
}
|
||||
wasOk = isOk;
|
||||
|
||||
lastPrinterStatus = combinedStatus;
|
||||
}
|
||||
} else if (d.Type == Winspool.JOB_NOTIFY_TYPE) {
|
||||
// Job Name Set or Changed
|
||||
if (d.Field == Winspool.JOB_NOTIFY_FIELD_DOCUMENT) {
|
||||
// The element containing our Doc name is not always the first item of the event
|
||||
// The Job name is only sent once, catalog it for later statuses
|
||||
docNames.put(d.Id, d.NotifyData.Data.pBuf.getWideString(0));
|
||||
// Job Status Changed
|
||||
} else if (d.Field == Winspool.JOB_NOTIFY_FIELD_STATUS) {
|
||||
//If there is no list for a given ID, create a new one and add it to the collection under said ID
|
||||
ArrayList<Integer> statusList = pendingJobStatuses.computeIfAbsent(d.Id, k -> new ArrayList<>());
|
||||
statusList.add(d.NotifyData.adwData[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitwise-safe combination of statusField and attributeField's PRINTER_ATTRIBUTE_WORK_OFFLINE.
|
||||
*
|
||||
* Due to PRINTER_ATTRIBUTE_WORK_OFFLINE's overlapping bitwise value, we must use a
|
||||
* non-overlapping value, ATTRIBUTE_WORK_OFFLINE.
|
||||
*
|
||||
* See also: https://stackoverflow.com/questions/41437023
|
||||
*/
|
||||
private static int combineStatus(int statusField, int attributeField) {
|
||||
int workOfflineFlag = (attributeField & Winspool.PRINTER_ATTRIBUTE_WORK_OFFLINE) == 0 ? 0 : (int)WmiPrinterStatusMap.ATTRIBUTE_WORK_OFFLINE.getRawCode();
|
||||
return statusField | workOfflineFlag;
|
||||
}
|
||||
|
||||
private void sendPendingStatuses() {
|
||||
if (pendingJobStatuses.size() == 0) return;
|
||||
for (Iterator<Map.Entry<Integer, ArrayList<Integer>>> i = pendingJobStatuses.entrySet().iterator(); i.hasNext();) {
|
||||
Map.Entry<Integer, ArrayList<Integer>> jobCodesEntry = i.next();
|
||||
ArrayList<Integer> codes = jobCodesEntry.getValue();
|
||||
int jobId = jobCodesEntry.getKey();
|
||||
|
||||
// Wait until we have a real docName
|
||||
if (invalidNames.contains(docNames.get(jobId))) continue;
|
||||
|
||||
// Workaround for double 'printed' statuses
|
||||
if (holdsJobs && docNames.get(jobId) == null && codes.size() == 1 && codes.get(0) == (int)WmiJobStatusMap.PRINTED.getRawCode()) {
|
||||
i.remove();
|
||||
lastJobStatusCodes.remove(jobId);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int code: codes) {
|
||||
int oldStatusCode = lastJobStatusCodes.getOrDefault(jobId, 0);
|
||||
|
||||
// This only sets status flags if they are not in oldStatusCode
|
||||
int statusToReport = code & (~oldStatusCode);
|
||||
if (statusToReport != 0) {
|
||||
StatusMonitor.statusChanged(NativeStatus.fromWmiJobStatus(statusToReport, printerName, jobId, docNames.get(jobId)));
|
||||
}
|
||||
lastJobStatusCodes.put(jobId, code);
|
||||
}
|
||||
i.remove();
|
||||
|
||||
|
||||
int code = codes.get(codes.size() - 1);
|
||||
boolean isFinalCode = (code & (int)WmiJobStatusMap.DELETED.getRawCode()) > 0;
|
||||
|
||||
// If the printer holds jobs, the last event we will see is 'printed' or 'deleted' and not 'printing', otherwise it will be just 'deleted'.
|
||||
if (holdsJobs) {
|
||||
isFinalCode |= (code & (int)WmiJobStatusMap.PRINTED.getRawCode()) > 0;
|
||||
isFinalCode &= (code & (int)WmiJobStatusMap.PRINTING.getRawCode()) == 0;
|
||||
}
|
||||
// If that was the last status we will see from a job, remove it from our lists.
|
||||
if (isFinalCode) {
|
||||
docNames.remove(jobId);
|
||||
lastJobStatusCodes.remove(jobId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void issueError() {
|
||||
int errorCode = Kernel32.INSTANCE.GetLastError();
|
||||
log.error("WMI Error number: {}, This should be reported", errorCode);
|
||||
Status[] unknownError = { new Status(NativePrinterStatus.UNMAPPED, printerName, WmiPrinterStatusMap.UNKNOWN_STATUS.getRawCode()) };
|
||||
StatusMonitor.statusChanged(unknownError);
|
||||
try {
|
||||
//if the error repeats, we don't want to lock up the cpu
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch(Exception ignore) {}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
closing = true;
|
||||
spool.FindClosePrinterChangeNotification(hChangeObject);
|
||||
super.interrupt();
|
||||
}
|
||||
|
||||
public static ArrayList<Status> getAllStatuses() {
|
||||
ArrayList<Status> statuses = new ArrayList<>();
|
||||
Winspool.PRINTER_INFO_2[] wmiPrinters = WinspoolUtil.getAllPrinterInfo2();
|
||||
for(Winspool.PRINTER_INFO_2 printerInfo2 : wmiPrinters) {
|
||||
WinNT.HANDLEByReference phPrinter = new WinNT.HANDLEByReference();
|
||||
Winspool.INSTANCE.OpenPrinter(printerInfo2.pPrinterName, phPrinter, null);
|
||||
for(Winspool.JOB_INFO_1 info : WinspoolUtil.getJobInfo1(phPrinter)) {
|
||||
Collections.addAll(statuses, NativeStatus.fromWmiJobStatus(info.Status, printerInfo2.pPrinterName, info.JobId, info.pDocument));
|
||||
}
|
||||
Collections.addAll(statuses, NativeStatus.fromWmiPrinterStatus(combineStatus(printerInfo2.Status, printerInfo2.Attributes), printerInfo2.pPrinterName));
|
||||
}
|
||||
return statuses;
|
||||
}
|
||||
}
|
||||
155
old code/tray/src/qz/printer/status/job/CupsJobStatusMap.java
Executable file
155
old code/tray/src/qz/printer/status/job/CupsJobStatusMap.java
Executable file
@@ -0,0 +1,155 @@
|
||||
package qz.printer.status.job;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.printer.status.NativeStatus;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import static qz.printer.status.job.CupsJobStatusMap.CupsJobStatusType.*;
|
||||
|
||||
/**
|
||||
* Created by Tres on 12/23/2020
|
||||
*/
|
||||
public enum CupsJobStatusMap implements NativeStatus.NativeMap {
|
||||
// job-state
|
||||
PENDING(STATE, NativeJobStatus.SPOOLING), // pending
|
||||
PENDING_HELD(STATE, NativeJobStatus.PAUSED), // pending-held
|
||||
PROCESSING(STATE, NativeJobStatus.SPOOLING), // processing
|
||||
PROCESSING_STOPPED(STATE, NativeJobStatus.PAUSED), // processing-stopped
|
||||
CANCELED(STATE, NativeJobStatus.CANCELED), // canceled
|
||||
ABORTED(STATE, NativeJobStatus.ABORTED), // aborted
|
||||
COMPLETED(STATE, NativeJobStatus.COMPLETE), // completed
|
||||
|
||||
// job-state-reasons. NativeJobStatus.UNMAPPED will fallback to the job-state instead
|
||||
ABORTED_BY_SYSTEM(REASON, NativeJobStatus.ABORTED), // aborted-by-system
|
||||
ACCOUNT_AUTHORIZATION_FAILED(REASON, NativeJobStatus.UNMAPPED), // account-authorization-failed
|
||||
ACCOUNT_CLOSED(REASON, NativeJobStatus.UNMAPPED), // account-closed
|
||||
ACCOUNT_INFO_NEEDED(REASON, NativeJobStatus.USER_INTERVENTION), // account-info-needed
|
||||
ACCOUNT_LIMIT_REACHED(REASON, NativeJobStatus.UNMAPPED), // account-limit-reached
|
||||
COMPRESSION_ERROR(REASON, NativeJobStatus.UNMAPPED), // compression-error
|
||||
CONFLICTING_ATTRIBUTES(REASON, NativeJobStatus.UNMAPPED), // conflicting-attributes
|
||||
CONNECTED_TO_DESTINATION(REASON, NativeJobStatus.UNMAPPED), // connected-to-destination
|
||||
CONNECTING_TO_DESTINATION(REASON, NativeJobStatus.UNMAPPED), // connecting-to-destination
|
||||
DESTINATION_URI_FAILED(REASON, NativeJobStatus.UNMAPPED), // destination-uri-failed
|
||||
DIGITAL_SIGNATURE_DID_NOT_VERIFY(REASON, NativeJobStatus.UNMAPPED), // digital-signature-did-not-verify
|
||||
DIGITAL_SIGNATURE_TYPE_NOT_SUPPORTED(REASON, NativeJobStatus.UNMAPPED), // digital-signature-type-not-supported
|
||||
DOCUMENT_ACCESS_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-access-error
|
||||
DOCUMENT_FORMAT_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-format-error
|
||||
DOCUMENT_PASSWORD_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-password-error
|
||||
DOCUMENT_PERMISSION_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-permission-error
|
||||
DOCUMENT_SECURITY_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-security-error
|
||||
DOCUMENT_UNPRINTABLE_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-unprintable-error
|
||||
ERRORS_DETECTED(REASON, NativeJobStatus.UNMAPPED), // errors-detected
|
||||
JOB_CANCELED_AT_DEVICE(REASON, NativeJobStatus.CANCELED), // job-canceled-at-device
|
||||
JOB_CANCELED_BY_OPERATOR(REASON, NativeJobStatus.CANCELED), // job-canceled-by-operator
|
||||
JOB_CANCELED_BY_USER(REASON, NativeJobStatus.CANCELED), // job-canceled-by-user
|
||||
JOB_COMPLETED_SUCCESSFULLY(REASON, NativeJobStatus.COMPLETE), // job-completed-successfully
|
||||
JOB_COMPLETED_WITH_ERRORS(REASON, NativeJobStatus.COMPLETE), // job-completed-with-errors
|
||||
JOB_COMPLETED_WITH_WARNINGS(REASON, NativeJobStatus.COMPLETE), // job-completed-with-warnings
|
||||
JOB_DATA_INSUFFICIENT(REASON, NativeJobStatus.UNMAPPED), // job-data-insufficient
|
||||
JOB_DELAY_OUTPUT_UNTIL_SPECIFIED(REASON, NativeJobStatus.SCHEDULED), // job-delay-output-until-specified
|
||||
JOB_DIGITAL_SIGNATURE_WAIT(REASON, NativeJobStatus.UNMAPPED), // job-digital-signature-wait
|
||||
JOB_FETCHABLE(REASON, NativeJobStatus.UNMAPPED), // job-fetchable
|
||||
JOB_HELD_FOR_REVIEW(REASON, NativeJobStatus.SPOOLING), // job-held-for-review
|
||||
JOB_HOLD_UNTIL_SPECIFIED(REASON, NativeJobStatus.PAUSED), // job-hold-until-specified
|
||||
JOB_INCOMING(REASON, NativeJobStatus.UNMAPPED), // job-incoming
|
||||
JOB_INTERPRETING(REASON, NativeJobStatus.UNMAPPED), // job-interpreting
|
||||
JOB_OUTGOING(REASON, NativeJobStatus.UNMAPPED), // job-outgoing
|
||||
JOB_PASSWORD_WAIT(REASON, NativeJobStatus.USER_INTERVENTION), // job-password-wait
|
||||
JOB_PRINTED_SUCCESSFULLY(REASON, NativeJobStatus.COMPLETE), // job-printed-successfully
|
||||
JOB_PRINTED_WITH_ERRORS(REASON, NativeJobStatus.COMPLETE), // job-printed-with-errors
|
||||
JOB_PRINTED_WITH_WARNINGS(REASON, NativeJobStatus.COMPLETE), // job-printed-with-warnings
|
||||
JOB_PRINTING(REASON, NativeJobStatus.PRINTING), // job-printing
|
||||
JOB_QUEUED(REASON, NativeJobStatus.SPOOLING), // job-queued
|
||||
JOB_QUEUED_FOR_MARKER(REASON, NativeJobStatus.SPOOLING), // job-queued-for-marker
|
||||
JOB_RELEASE_WAIT(REASON, NativeJobStatus.UNMAPPED), // job-release-wait
|
||||
JOB_RESTARTABLE(REASON, NativeJobStatus.UNMAPPED), // job-restartable
|
||||
JOB_RESUMING(REASON, NativeJobStatus.SPOOLING), // job-resuming
|
||||
JOB_SAVED_SUCCESSFULLY(REASON, NativeJobStatus.RETAINED), // job-saved-successfully
|
||||
JOB_SAVED_WITH_ERRORS(REASON, NativeJobStatus.RETAINED), // job-saved-with-errors
|
||||
JOB_SAVED_WITH_WARNINGS(REASON, NativeJobStatus.RETAINED), // job-saved-with-warnings
|
||||
JOB_SAVING(REASON, NativeJobStatus.UNMAPPED), // job-saving
|
||||
JOB_SPOOLING(REASON, NativeJobStatus.UNMAPPED), // job-spooling
|
||||
JOB_STREAMING(REASON, NativeJobStatus.UNMAPPED), // job-streaming
|
||||
JOB_SUSPENDED(REASON, NativeJobStatus.PAUSED), // job-suspended
|
||||
JOB_SUSPENDED_BY_OPERATOR(REASON, NativeJobStatus.PAUSED), // job-suspended-by-operator
|
||||
JOB_SUSPENDED_BY_SYSTEM(REASON, NativeJobStatus.PAUSED), // job-suspended-by-system
|
||||
JOB_SUSPENDED_BY_USER(REASON, NativeJobStatus.PAUSED), // job-suspended-by-user
|
||||
JOB_SUSPENDING(REASON, NativeJobStatus.UNMAPPED), // job-suspending
|
||||
JOB_TRANSFERRING(REASON, NativeJobStatus.UNMAPPED), // job-transferring
|
||||
JOB_TRANSFORMING(REASON, NativeJobStatus.UNMAPPED), // job-transforming
|
||||
PRINTER_STOPPED(REASON, NativeJobStatus.PAUSED), // printer-stopped
|
||||
PRINTER_STOPPED_PARTLY(REASON, NativeJobStatus.UNMAPPED), // printer-stopped-partly
|
||||
PROCESSING_TO_STOP_POINT(REASON, NativeJobStatus.UNMAPPED), // processing-to-stop-point
|
||||
QUEUED_IN_DEVICE(REASON, NativeJobStatus.UNMAPPED), // queued-in-device
|
||||
RESOURCES_ARE_NOT_READY(REASON, NativeJobStatus.UNMAPPED), // resources-are-not-ready
|
||||
RESOURCES_ARE_NOT_SUPPORTED(REASON, NativeJobStatus.UNMAPPED), // resources-are-not-supported
|
||||
SERVICE_OFF_LINE(REASON, NativeJobStatus.UNMAPPED), // service-off-line
|
||||
SUBMISSION_INTERRUPTED(REASON, NativeJobStatus.UNMAPPED), // submission-interrupted
|
||||
UNSUPPORTED_ATTRIBUTES_OR_VALUES(REASON, NativeJobStatus.UNMAPPED), // unsupported-attributes-or-values
|
||||
UNSUPPORTED_COMPRESSION(REASON, NativeJobStatus.UNMAPPED), // unsupported-compression
|
||||
UNSUPPORTED_DOCUMENT_FORMAT(REASON, NativeJobStatus.UNMAPPED), // unsupported-document-format
|
||||
WAITING_FOR_USER_ACTION(REASON, NativeJobStatus.USER_INTERVENTION), // waiting-for-user-action
|
||||
WARNINGS_DETECTED(REASON, NativeJobStatus.UNKNOWN); // warnings-detected
|
||||
|
||||
private static final Logger log = LogManager.getLogger(CupsJobStatusMap.class);
|
||||
private static SortedMap<String,NativeJobStatus> sortedReasonLookupTable;
|
||||
private static SortedMap<String,NativeJobStatus> sortedStateLookupTable;
|
||||
|
||||
private final NativeJobStatus parent;
|
||||
private final CupsJobStatusType type;
|
||||
|
||||
enum CupsJobStatusType {
|
||||
STATE,
|
||||
REASON;
|
||||
}
|
||||
|
||||
CupsJobStatusMap(CupsJobStatusType type, NativeJobStatus parent) {
|
||||
this.type = type;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public static NativeJobStatus matchReason(String code) {
|
||||
// Initialize a sorted map to speed up lookups
|
||||
if(sortedReasonLookupTable == null) {
|
||||
sortedReasonLookupTable = new TreeMap<>();
|
||||
for(CupsJobStatusMap value : values()) {
|
||||
if(value.type == REASON) {
|
||||
sortedReasonLookupTable.put(value.name().toLowerCase(Locale.ENGLISH).replace("_", "-"), value.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NativeJobStatus status = sortedReasonLookupTable.get(code);
|
||||
if(status == null && !code.equalsIgnoreCase("none")) {
|
||||
// Don't warn for "none"
|
||||
log.warn("Printer job state-reason \"{}\" was not found", code);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
public static NativeJobStatus matchState(String state) {
|
||||
// Initialize a sorted map to speed up lookups
|
||||
if(sortedStateLookupTable == null) {
|
||||
sortedStateLookupTable = new TreeMap<>();
|
||||
for(CupsJobStatusMap value : values()) {
|
||||
if(value.type == STATE) {
|
||||
sortedStateLookupTable.put(value.name().toLowerCase(Locale.ENGLISH).replace("_", "-"), value.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedStateLookupTable.getOrDefault(state, NativeJobStatus.UNKNOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeJobStatus getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getRawCode() {
|
||||
return name().toLowerCase(Locale.ENGLISH).replace("_", "-");
|
||||
}
|
||||
}
|
||||
45
old code/tray/src/qz/printer/status/job/NativeJobStatus.java
Executable file
45
old code/tray/src/qz/printer/status/job/NativeJobStatus.java
Executable file
@@ -0,0 +1,45 @@
|
||||
package qz.printer.status.job;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import qz.printer.status.NativeStatus;
|
||||
|
||||
/**
|
||||
* Created by kyle on 7/7/17.
|
||||
*/
|
||||
public enum NativeJobStatus implements NativeStatus {
|
||||
ABORTED(Level.ERROR),
|
||||
CANCELED(Level.WARN),
|
||||
COMPLETE(Level.INFO),
|
||||
DELETED(Level.INFO),
|
||||
DELETING(Level.INFO),
|
||||
ERROR(Level.ERROR),
|
||||
OFFLINE(Level.ERROR),
|
||||
PRINTING(Level.INFO),
|
||||
SPOOLING(Level.INFO),
|
||||
SCHEDULED(Level.INFO),
|
||||
PAPEROUT(Level.WARN),
|
||||
RETAINED(Level.INFO),
|
||||
PAUSED(Level.WARN),
|
||||
SENT(Level.INFO),
|
||||
RESTART(Level.WARN),
|
||||
RENDERING_LOCALLY(Level.INFO),
|
||||
USER_INTERVENTION(Level.WARN),
|
||||
UNMAPPED(Level.FATAL), // should never make it to the user
|
||||
UNKNOWN(Level.INFO);
|
||||
|
||||
private Level level;
|
||||
|
||||
NativeJobStatus(Level level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeStatus getDefault() {
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
60
old code/tray/src/qz/printer/status/job/WmiJobStatusMap.java
Executable file
60
old code/tray/src/qz/printer/status/job/WmiJobStatusMap.java
Executable file
@@ -0,0 +1,60 @@
|
||||
package qz.printer.status.job;
|
||||
|
||||
import qz.printer.status.NativeStatus;
|
||||
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Created by tresf on 12/10/2020
|
||||
*/
|
||||
public enum WmiJobStatusMap implements NativeStatus.NativeMap {
|
||||
EMPTY(NativeJobStatus.UNKNOWN, -0x00000001), // Fallback for a no-status message
|
||||
PAUSED(NativeJobStatus.PAUSED, 0x00000001), // Job is paused
|
||||
ERROR(NativeJobStatus.ERROR, 0x00000002), // An error is associated with the job
|
||||
DELETING(NativeJobStatus.DELETING, 0x00000004), // Job is being deleted
|
||||
SPOOLING(NativeJobStatus.SPOOLING, 0x00000008), // Job is spooling
|
||||
PRINTING(NativeJobStatus.PRINTING, 0x00000010), // Job is printing
|
||||
OFFLINE(NativeJobStatus.OFFLINE, 0x00000020), // Job is printing
|
||||
PAPEROUT(NativeJobStatus.PAPEROUT, 0x00000040), // Printer is out of paper
|
||||
PRINTED(NativeJobStatus.COMPLETE, 0x00000080), // Job has printed
|
||||
DELETED(NativeJobStatus.DELETED, 0x00000100), // Job has been deleted
|
||||
BLOCKED_DEVQ(NativeJobStatus.ABORTED, 0x00000200), // The driver cannot print the job
|
||||
RESTART(NativeJobStatus.RESTART, 0x00000800), // Job has been restarted
|
||||
COMPLETE(NativeJobStatus.SENT, 0x00001000), // Windows XP and later: Job is sent to the printer, but the job may not be printed yet
|
||||
RETAINED(NativeJobStatus.RETAINED, 0x00002000), // Windows Vista and later: Job has been retained in the print queue and cannot be deleted (Edit: it actually can https://github.com/qzind/tray/issues/1305)
|
||||
RENDERING_LOCALLY(NativeJobStatus.RENDERING_LOCALLY, 0x00004000), // Job rendering locally on the client
|
||||
USER_INTERVENTION(NativeJobStatus.USER_INTERVENTION, 0x40000000); // Printer has an error that requires the user to do something
|
||||
|
||||
private static SortedMap<Integer,NativeJobStatus> sortedLookupTable;
|
||||
|
||||
private final NativeJobStatus parent;
|
||||
private final int rawCode;
|
||||
|
||||
WmiJobStatusMap(NativeJobStatus parent, int rawCode) {
|
||||
this.parent = parent;
|
||||
this.rawCode = rawCode;
|
||||
}
|
||||
|
||||
public static NativeJobStatus match(int code) {
|
||||
// Initialize a sorted map to speed up lookups
|
||||
if(sortedLookupTable == null) {
|
||||
sortedLookupTable = new TreeMap<>();
|
||||
for(WmiJobStatusMap value : values()) {
|
||||
sortedLookupTable.put(value.rawCode, value.parent);
|
||||
}
|
||||
}
|
||||
|
||||
return sortedLookupTable.get(code);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeStatus getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getRawCode() {
|
||||
return rawCode;
|
||||
}
|
||||
}
|
||||
959
old code/tray/src/qz/printer/status/printer/CupsPrinterStatusMap.java
Executable file
959
old code/tray/src/qz/printer/status/printer/CupsPrinterStatusMap.java
Executable file
@@ -0,0 +1,959 @@
|
||||
package qz.printer.status.printer;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.printer.status.NativeStatus;
|
||||
import qz.printer.status.Status;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static qz.printer.status.printer.CupsPrinterStatusMap.CupsPrinterStatusType.*;
|
||||
|
||||
public enum CupsPrinterStatusMap implements NativeStatus.NativeMap {
|
||||
// printer-state
|
||||
IDLE(STATE, NativePrinterStatus.OK), // idle
|
||||
PROCESSING(STATE, NativePrinterStatus.PROCESSING), // processing
|
||||
STOPPED(STATE, NativePrinterStatus.PAUSED), // stopped
|
||||
|
||||
// printer-state-reasons. NativePrinterStatus.UNMAPPED will fallback to the printer-state instead
|
||||
// Mapped printer-state-reasons
|
||||
OFFLINE_REPORT(REASON, NativePrinterStatus.OFFLINE), // "offline-report"
|
||||
OTHER(REASON, NativePrinterStatus.UNMAPPED), // "other"
|
||||
MEDIA_NEEDED(REASON, NativePrinterStatus.PAPER_OUT), // "media-needed"
|
||||
MEDIA_JAM(REASON, NativePrinterStatus.PAPER_JAM), // "media-jam"
|
||||
MOVING_TO_PAUSED(REASON, NativePrinterStatus.OK), // "moving-to-paused"
|
||||
PAUSED(REASON, NativePrinterStatus.UNMAPPED), // "paused"
|
||||
SHUTDOWN(REASON, NativePrinterStatus.OFFLINE), // "shutdown"
|
||||
CONNECTING_TO_DEVICE(REASON, NativePrinterStatus.PROCESSING), // "connecting-to-device"
|
||||
TIMED_OUT(REASON, NativePrinterStatus.NOT_AVAILABLE), // "timed-out"
|
||||
STOPPING(REASON, NativePrinterStatus.OK), // "stopping"
|
||||
STOPPED_PARTLY(REASON, NativePrinterStatus.PAUSED), // "stopped-partly"
|
||||
TONER_LOW(REASON, NativePrinterStatus.TONER_LOW), // "toner-low"
|
||||
TONER_EMPTY(REASON, NativePrinterStatus.NO_TONER), // "toner-empty"
|
||||
SPOOL_AREA_FULL(REASON, NativePrinterStatus.OUT_OF_MEMORY), // "spool-area-full"
|
||||
COVER_OPEN(REASON, NativePrinterStatus.DOOR_OPEN), // "cover-open"
|
||||
INTERLOCK_OPEN(REASON, NativePrinterStatus.DOOR_OPEN), // "interlock-open"
|
||||
DOOR_OPEN(REASON, NativePrinterStatus.DOOR_OPEN), // "door-open"
|
||||
INPUT_TRAY_MISSING(REASON, NativePrinterStatus.PAPER_PROBLEM), // "input-tray-missing"
|
||||
MEDIA_LOW(REASON, NativePrinterStatus.PAPER_PROBLEM), // "media-low"
|
||||
MEDIA_EMPTY(REASON, NativePrinterStatus.PAPER_OUT), // "media-empty"
|
||||
OUTPUT_TRAY_MISSING(REASON, NativePrinterStatus.PAPER_PROBLEM), // "output-tray-missing"
|
||||
//not a great match
|
||||
OUTPUT_AREA_ALMOST_FULL(REASON, NativePrinterStatus.TONER_LOW), // "output-area-almost-full"
|
||||
OUTPUT_AREA_FULL(REASON, NativePrinterStatus.OUTPUT_BIN_FULL), // "output-area-full"
|
||||
MARKER_SUPPLY_LOW(REASON, NativePrinterStatus.TONER_LOW), // "marker-supply-low"
|
||||
MARKER_SUPPLY_EMPTY(REASON, NativePrinterStatus.NO_TONER), // "marker-supply-empty"
|
||||
// not a great match
|
||||
MARKER_WASTE_ALMOST_FULL(REASON, NativePrinterStatus.TONER_LOW), // "marker-waste-almost-full"
|
||||
MARKER_WASTE_FULL(REASON, NativePrinterStatus.NO_TONER), // "marker-waste-full"
|
||||
FUSER_OVER_TEMP(REASON, NativePrinterStatus.WARMING_UP), // "fuser-over-temp"
|
||||
FUSER_UNDER_TEMP(REASON, NativePrinterStatus.WARMING_UP), // "fuser-under-temp"
|
||||
// not a great match
|
||||
OPC_NEAR_EOL(REASON, NativePrinterStatus.TONER_LOW), // "opc-near-eol"
|
||||
OPC_LIFE_OVER(REASON, NativePrinterStatus.NO_TONER), // "opc-life-over"
|
||||
DEVELOPER_LOW(REASON, NativePrinterStatus.TONER_LOW), // "developer-low"
|
||||
DEVELOPER_EMPTY(REASON, NativePrinterStatus.NO_TONER), // "developer-empty"
|
||||
INTERPRETER_RESOURCE_UNAVAILABLE(REASON, NativePrinterStatus.SERVER_UNKNOWN), // "interpreter-resource-unavailable"
|
||||
|
||||
// CUPS defined states (defined by CUPS, but not part of the IPP specification)
|
||||
OFFLINE(REASON, NativePrinterStatus.OFFLINE), // "offline"
|
||||
CUPS_INSECURE_FILTER_WARNING(REASON, NativePrinterStatus.SERVER_UNKNOWN), // "cups-insecure-filter-warning"
|
||||
CUPS_MISSING_FILTER_WARNING(REASON, NativePrinterStatus.ERROR), // "cups-missing-filter-warning"
|
||||
CUPS_WAITING_FOR_JOB_COMPLETED(REASON, NativePrinterStatus.PRINTING), // "cups-waiting-for-job-completed");
|
||||
|
||||
// Deprecated CUPS defined states (outdated or incorrect values known to occur)
|
||||
CUPS_INSECURE_FILTER_ERROR(REASON, NativePrinterStatus.SERVER_UNKNOWN), // "cups-insecure-filter-error"
|
||||
CUPS_MISSING_FILTER_ERROR(REASON, NativePrinterStatus.ERROR), // "cups-missing-filter-error"
|
||||
CUPS_INSECURE_FILTER(REASON, NativePrinterStatus.SERVER_UNKNOWN), // "cups-insecure-filter"
|
||||
CUPS_MISSING_FILTER(REASON, NativePrinterStatus.ERROR), // "cups-missing-filter"
|
||||
|
||||
// SNMP statuses with no existing CUPS definition
|
||||
SERVICE_NEEDED(REASON, NativePrinterStatus.UNMAPPED), // "service-needed"
|
||||
|
||||
// Unmapped printer-state-reasons
|
||||
ALERT_REMOVAL_OF_BINARY_CHANGE_ENTRY(REASON, NativePrinterStatus.UNMAPPED), // alert-removal-of-binary-change-entry
|
||||
BANDER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // bander-added
|
||||
BANDER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // bander-almost-empty
|
||||
BANDER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // bander-almost-full
|
||||
BANDER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // bander-at-limit
|
||||
BANDER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // bander-closed
|
||||
BANDER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // bander-configuration-change
|
||||
BANDER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // bander-cover-closed
|
||||
BANDER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // bander-cover-open
|
||||
BANDER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // bander-empty
|
||||
BANDER_FULL(REASON, NativePrinterStatus.UNMAPPED), // bander-full
|
||||
BANDER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // bander-interlock-closed
|
||||
BANDER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // bander-interlock-open
|
||||
BANDER_JAM(REASON, NativePrinterStatus.UNMAPPED), // bander-jam
|
||||
BANDER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // bander-life-almost-over
|
||||
BANDER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // bander-life-over
|
||||
BANDER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // bander-memory-exhausted
|
||||
BANDER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // bander-missing
|
||||
BANDER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-motor-failure
|
||||
BANDER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // bander-near-limit
|
||||
BANDER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // bander-offline
|
||||
BANDER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // bander-opened
|
||||
BANDER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // bander-over-temperature
|
||||
BANDER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // bander-power-saver
|
||||
BANDER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-recoverable-failure
|
||||
BANDER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // bander-recoverable-storage
|
||||
BANDER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // bander-removed
|
||||
BANDER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // bander-resource-added
|
||||
BANDER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // bander-resource-removed
|
||||
BANDER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-thermistor-failure
|
||||
BANDER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-timing-failure
|
||||
BANDER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // bander-turned-off
|
||||
BANDER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // bander-turned-on
|
||||
BANDER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // bander-under-temperature
|
||||
BANDER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-unrecoverable-failure
|
||||
BANDER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // bander-unrecoverable-storage-error
|
||||
BANDER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // bander-warming-up
|
||||
BINDER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // binder-added
|
||||
BINDER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // binder-almost-empty
|
||||
BINDER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // binder-almost-full
|
||||
BINDER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // binder-at-limit
|
||||
BINDER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // binder-closed
|
||||
BINDER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // binder-configuration-change
|
||||
BINDER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // binder-cover-closed
|
||||
BINDER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // binder-cover-open
|
||||
BINDER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // binder-empty
|
||||
BINDER_FULL(REASON, NativePrinterStatus.UNMAPPED), // binder-full
|
||||
BINDER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // binder-interlock-closed
|
||||
BINDER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // binder-interlock-open
|
||||
BINDER_JAM(REASON, NativePrinterStatus.UNMAPPED), // binder-jam
|
||||
BINDER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // binder-life-almost-over
|
||||
BINDER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // binder-life-over
|
||||
BINDER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // binder-memory-exhausted
|
||||
BINDER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // binder-missing
|
||||
BINDER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-motor-failure
|
||||
BINDER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // binder-near-limit
|
||||
BINDER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // binder-offline
|
||||
BINDER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // binder-opened
|
||||
BINDER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // binder-over-temperature
|
||||
BINDER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // binder-power-saver
|
||||
BINDER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-recoverable-failure
|
||||
BINDER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // binder-recoverable-storage
|
||||
BINDER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // binder-removed
|
||||
BINDER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // binder-resource-added
|
||||
BINDER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // binder-resource-removed
|
||||
BINDER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-thermistor-failure
|
||||
BINDER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-timing-failure
|
||||
BINDER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // binder-turned-off
|
||||
BINDER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // binder-turned-on
|
||||
BINDER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // binder-under-temperature
|
||||
BINDER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-unrecoverable-failure
|
||||
BINDER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // binder-unrecoverable-storage-error
|
||||
BINDER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // binder-warming-up
|
||||
CAMERA_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // camera-failure
|
||||
CHAMBER_COOLING(REASON, NativePrinterStatus.UNMAPPED), // chamber-cooling
|
||||
CHAMBER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // chamber-failure
|
||||
CHAMBER_HEATING(REASON, NativePrinterStatus.UNMAPPED), // chamber-heating
|
||||
CHAMBER_TEMPERATURE_HIGH(REASON, NativePrinterStatus.UNMAPPED), // chamber-temperature-high
|
||||
CHAMBER_TEMPERATURE_LOW(REASON, NativePrinterStatus.UNMAPPED), // chamber-temperature-low
|
||||
CLEANER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // cleaner-life-almost-over
|
||||
CLEANER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // cleaner-life-over
|
||||
CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // configuration-change
|
||||
DEACTIVATED(REASON, NativePrinterStatus.UNMAPPED), // deactivated
|
||||
DELETED(REASON, NativePrinterStatus.UNMAPPED), // deleted
|
||||
DIE_CUTTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-added
|
||||
DIE_CUTTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-almost-empty
|
||||
DIE_CUTTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-almost-full
|
||||
DIE_CUTTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-at-limit
|
||||
DIE_CUTTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-closed
|
||||
DIE_CUTTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-configuration-change
|
||||
DIE_CUTTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-cover-closed
|
||||
DIE_CUTTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-cover-open
|
||||
DIE_CUTTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-empty
|
||||
DIE_CUTTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-full
|
||||
DIE_CUTTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-interlock-closed
|
||||
DIE_CUTTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-interlock-open
|
||||
DIE_CUTTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-jam
|
||||
DIE_CUTTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-life-almost-over
|
||||
DIE_CUTTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-life-over
|
||||
DIE_CUTTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-memory-exhausted
|
||||
DIE_CUTTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-missing
|
||||
DIE_CUTTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-motor-failure
|
||||
DIE_CUTTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-near-limit
|
||||
DIE_CUTTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-offline
|
||||
DIE_CUTTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-opened
|
||||
DIE_CUTTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-over-temperature
|
||||
DIE_CUTTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-power-saver
|
||||
DIE_CUTTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-recoverable-failure
|
||||
DIE_CUTTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-recoverable-storage
|
||||
DIE_CUTTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-removed
|
||||
DIE_CUTTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-resource-added
|
||||
DIE_CUTTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-resource-removed
|
||||
DIE_CUTTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-thermistor-failure
|
||||
DIE_CUTTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-timing-failure
|
||||
DIE_CUTTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-turned-off
|
||||
DIE_CUTTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-turned-on
|
||||
DIE_CUTTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-under-temperature
|
||||
DIE_CUTTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-unrecoverable-failure
|
||||
DIE_CUTTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-unrecoverable-storage-error
|
||||
DIE_CUTTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-warming-up
|
||||
EXTRUDER_COOLING(REASON, NativePrinterStatus.UNMAPPED), // extruder-cooling
|
||||
EXTRUDER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // extruder-failure
|
||||
EXTRUDER_HEATING(REASON, NativePrinterStatus.UNMAPPED), // extruder-heating
|
||||
EXTRUDER_JAM(REASON, NativePrinterStatus.UNMAPPED), // extruder-jam
|
||||
EXTRUDER_TEMPERATURE_HIGH(REASON, NativePrinterStatus.UNMAPPED), // extruder-temperature-high
|
||||
EXTRUDER_TEMPERATURE_LOW(REASON, NativePrinterStatus.UNMAPPED), // extruder-temperature-low
|
||||
FAN_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // fan-failure
|
||||
FAX_MODEM_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-life-almost-over
|
||||
FAX_MODEM_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-life-over
|
||||
FAX_MODEM_MISSING(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-missing
|
||||
FAX_MODEM_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-turned-off
|
||||
FAX_MODEM_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-turned-on
|
||||
FOLDER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // folder-added
|
||||
FOLDER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // folder-almost-empty
|
||||
FOLDER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // folder-almost-full
|
||||
FOLDER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // folder-at-limit
|
||||
FOLDER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // folder-closed
|
||||
FOLDER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // folder-configuration-change
|
||||
FOLDER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // folder-cover-closed
|
||||
FOLDER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // folder-cover-open
|
||||
FOLDER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // folder-empty
|
||||
FOLDER_FULL(REASON, NativePrinterStatus.UNMAPPED), // folder-full
|
||||
FOLDER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // folder-interlock-closed
|
||||
FOLDER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // folder-interlock-open
|
||||
FOLDER_JAM(REASON, NativePrinterStatus.UNMAPPED), // folder-jam
|
||||
FOLDER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // folder-life-almost-over
|
||||
FOLDER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // folder-life-over
|
||||
FOLDER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // folder-memory-exhausted
|
||||
FOLDER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // folder-missing
|
||||
FOLDER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-motor-failure
|
||||
FOLDER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // folder-near-limit
|
||||
FOLDER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // folder-offline
|
||||
FOLDER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // folder-opened
|
||||
FOLDER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // folder-over-temperature
|
||||
FOLDER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // folder-power-saver
|
||||
FOLDER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-recoverable-failure
|
||||
FOLDER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // folder-recoverable-storage
|
||||
FOLDER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // folder-removed
|
||||
FOLDER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // folder-resource-added
|
||||
FOLDER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // folder-resource-removed
|
||||
FOLDER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-thermistor-failure
|
||||
FOLDER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-timing-failure
|
||||
FOLDER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // folder-turned-off
|
||||
FOLDER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // folder-turned-on
|
||||
FOLDER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // folder-under-temperature
|
||||
FOLDER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-unrecoverable-failure
|
||||
FOLDER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // folder-unrecoverable-storage-error
|
||||
FOLDER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // folder-warming-up
|
||||
HIBERNATE(REASON, NativePrinterStatus.UNMAPPED), // hibernate
|
||||
HOLD_NEW_JOBS(REASON, NativePrinterStatus.UNMAPPED), // hold-new-jobs
|
||||
IDENTIFY_PRINTER_REQUESTED(REASON, NativePrinterStatus.UNMAPPED), // identify-printer-requested
|
||||
IMPRINTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-added
|
||||
IMPRINTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // imprinter-almost-empty
|
||||
IMPRINTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // imprinter-almost-full
|
||||
IMPRINTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // imprinter-at-limit
|
||||
IMPRINTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-closed
|
||||
IMPRINTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-configuration-change
|
||||
IMPRINTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-cover-closed
|
||||
IMPRINTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // imprinter-cover-open
|
||||
IMPRINTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // imprinter-empty
|
||||
IMPRINTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // imprinter-full
|
||||
IMPRINTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-interlock-closed
|
||||
IMPRINTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // imprinter-interlock-open
|
||||
IMPRINTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // imprinter-jam
|
||||
IMPRINTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // imprinter-life-almost-over
|
||||
IMPRINTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // imprinter-life-over
|
||||
IMPRINTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-memory-exhausted
|
||||
IMPRINTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // imprinter-missing
|
||||
IMPRINTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-motor-failure
|
||||
IMPRINTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // imprinter-near-limit
|
||||
IMPRINTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-offline
|
||||
IMPRINTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-opened
|
||||
IMPRINTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-over-temperature
|
||||
IMPRINTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // imprinter-power-saver
|
||||
IMPRINTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-recoverable-failure
|
||||
IMPRINTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-recoverable-storage
|
||||
IMPRINTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-removed
|
||||
IMPRINTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-resource-added
|
||||
IMPRINTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-resource-removed
|
||||
IMPRINTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-thermistor-failure
|
||||
IMPRINTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-timing-failure
|
||||
IMPRINTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // imprinter-turned-off
|
||||
IMPRINTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // imprinter-turned-on
|
||||
IMPRINTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-under-temperature
|
||||
IMPRINTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-unrecoverable-failure
|
||||
IMPRINTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // imprinter-unrecoverable-storage-error
|
||||
IMPRINTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // imprinter-warming-up
|
||||
INPUT_CANNOT_FEED_SIZE_SELECTED(REASON, NativePrinterStatus.UNMAPPED), // input-cannot-feed-size-selected
|
||||
INPUT_MANUAL_INPUT_REQUEST(REASON, NativePrinterStatus.UNMAPPED), // input-manual-input-request
|
||||
INPUT_MEDIA_COLOR_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-color-change
|
||||
INPUT_MEDIA_FORM_PARTS_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-form-parts-change
|
||||
INPUT_MEDIA_SIZE_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-size-change
|
||||
INPUT_MEDIA_TRAY_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // input-media-tray-failure
|
||||
INPUT_MEDIA_TRAY_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // input-media-tray-feed-error
|
||||
INPUT_MEDIA_TRAY_JAM(REASON, NativePrinterStatus.UNMAPPED), // input-media-tray-jam
|
||||
INPUT_MEDIA_TYPE_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-type-change
|
||||
INPUT_MEDIA_WEIGHT_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-weight-change
|
||||
INPUT_PICK_ROLLER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // input-pick-roller-failure
|
||||
INPUT_PICK_ROLLER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // input-pick-roller-life-over
|
||||
INPUT_PICK_ROLLER_LIFE_WARN(REASON, NativePrinterStatus.UNMAPPED), // input-pick-roller-life-warn
|
||||
INPUT_PICK_ROLLER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // input-pick-roller-missing
|
||||
INPUT_TRAY_ELEVATION_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // input-tray-elevation-failure
|
||||
INPUT_TRAY_POSITION_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // input-tray-position-failure
|
||||
INSERTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // inserter-added
|
||||
INSERTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // inserter-almost-empty
|
||||
INSERTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // inserter-almost-full
|
||||
INSERTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // inserter-at-limit
|
||||
INSERTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // inserter-closed
|
||||
INSERTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // inserter-configuration-change
|
||||
INSERTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // inserter-cover-closed
|
||||
INSERTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // inserter-cover-open
|
||||
INSERTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // inserter-empty
|
||||
INSERTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // inserter-full
|
||||
INSERTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // inserter-interlock-closed
|
||||
INSERTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // inserter-interlock-open
|
||||
INSERTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // inserter-jam
|
||||
INSERTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // inserter-life-almost-over
|
||||
INSERTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // inserter-life-over
|
||||
INSERTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // inserter-memory-exhausted
|
||||
INSERTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // inserter-missing
|
||||
INSERTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-motor-failure
|
||||
INSERTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // inserter-near-limit
|
||||
INSERTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // inserter-offline
|
||||
INSERTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // inserter-opened
|
||||
INSERTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-over-temperature
|
||||
INSERTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // inserter-power-saver
|
||||
INSERTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-recoverable-failure
|
||||
INSERTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // inserter-recoverable-storage
|
||||
INSERTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // inserter-removed
|
||||
INSERTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // inserter-resource-added
|
||||
INSERTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // inserter-resource-removed
|
||||
INSERTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-thermistor-failure
|
||||
INSERTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-timing-failure
|
||||
INSERTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // inserter-turned-off
|
||||
INSERTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // inserter-turned-on
|
||||
INSERTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-under-temperature
|
||||
INSERTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-unrecoverable-failure
|
||||
INSERTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // inserter-unrecoverable-storage-error
|
||||
INSERTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // inserter-warming-up
|
||||
INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // interlock-closed
|
||||
INTERPRETER_CARTRIDGE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-cartridge-added
|
||||
INTERPRETER_CARTRIDGE_DELETED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-cartridge-deleted
|
||||
INTERPRETER_COMPLEX_PAGE_ENCOUNTERED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-complex-page-encountered
|
||||
INTERPRETER_MEMORY_DECREASE(REASON, NativePrinterStatus.UNMAPPED), // interpreter-memory-decrease
|
||||
INTERPRETER_MEMORY_INCREASE(REASON, NativePrinterStatus.UNMAPPED), // interpreter-memory-increase
|
||||
INTERPRETER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-resource-added
|
||||
INTERPRETER_RESOURCE_DELETED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-resource-deleted
|
||||
LAMP_AT_EOL(REASON, NativePrinterStatus.UNMAPPED), // lamp-at-eol
|
||||
LAMP_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // lamp-failure
|
||||
LAMP_NEAR_EOL(REASON, NativePrinterStatus.UNMAPPED), // lamp-near-eol
|
||||
LASER_AT_EOL(REASON, NativePrinterStatus.UNMAPPED), // laser-at-eol
|
||||
LASER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // laser-failure
|
||||
LASER_NEAR_EOL(REASON, NativePrinterStatus.UNMAPPED), // laser-near-eol
|
||||
MAKE_ENVELOPE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-added
|
||||
MAKE_ENVELOPE_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-almost-empty
|
||||
MAKE_ENVELOPE_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-almost-full
|
||||
MAKE_ENVELOPE_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-at-limit
|
||||
MAKE_ENVELOPE_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-closed
|
||||
MAKE_ENVELOPE_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-configuration-change
|
||||
MAKE_ENVELOPE_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-cover-closed
|
||||
MAKE_ENVELOPE_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-cover-open
|
||||
MAKE_ENVELOPE_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-empty
|
||||
MAKE_ENVELOPE_FULL(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-full
|
||||
MAKE_ENVELOPE_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-interlock-closed
|
||||
MAKE_ENVELOPE_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-interlock-open
|
||||
MAKE_ENVELOPE_JAM(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-jam
|
||||
MAKE_ENVELOPE_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-life-almost-over
|
||||
MAKE_ENVELOPE_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-life-over
|
||||
MAKE_ENVELOPE_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-memory-exhausted
|
||||
MAKE_ENVELOPE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-missing
|
||||
MAKE_ENVELOPE_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-motor-failure
|
||||
MAKE_ENVELOPE_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-near-limit
|
||||
MAKE_ENVELOPE_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-offline
|
||||
MAKE_ENVELOPE_OPENED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-opened
|
||||
MAKE_ENVELOPE_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-over-temperature
|
||||
MAKE_ENVELOPE_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-power-saver
|
||||
MAKE_ENVELOPE_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-recoverable-failure
|
||||
MAKE_ENVELOPE_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-recoverable-storage
|
||||
MAKE_ENVELOPE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-removed
|
||||
MAKE_ENVELOPE_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-resource-added
|
||||
MAKE_ENVELOPE_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-resource-removed
|
||||
MAKE_ENVELOPE_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-thermistor-failure
|
||||
MAKE_ENVELOPE_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-timing-failure
|
||||
MAKE_ENVELOPE_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-turned-off
|
||||
MAKE_ENVELOPE_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-turned-on
|
||||
MAKE_ENVELOPE_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-under-temperature
|
||||
MAKE_ENVELOPE_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-unrecoverable-failure
|
||||
MAKE_ENVELOPE_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-unrecoverable-storage-error
|
||||
MAKE_ENVELOPE_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-warming-up
|
||||
MARKER_ADJUSTING_PRINT_QUALITY(REASON, NativePrinterStatus.UNMAPPED), // marker-adjusting-print-quality
|
||||
MARKER_CLEANER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-cleaner-missing
|
||||
MARKER_DEVELOPER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-developer-almost-empty
|
||||
MARKER_DEVELOPER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-developer-empty
|
||||
MARKER_DEVELOPER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-developer-missing
|
||||
MARKER_FUSER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-fuser-missing
|
||||
MARKER_FUSER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // marker-fuser-thermistor-failure
|
||||
MARKER_FUSER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // marker-fuser-timing-failure
|
||||
MARKER_INK_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-ink-almost-empty
|
||||
MARKER_INK_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-ink-empty
|
||||
MARKER_INK_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-ink-missing
|
||||
MARKER_OPC_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-opc-missing
|
||||
MARKER_PRINT_RIBBON_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-print-ribbon-almost-empty
|
||||
MARKER_PRINT_RIBBON_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-print-ribbon-empty
|
||||
MARKER_PRINT_RIBBON_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-print-ribbon-missing
|
||||
MARKER_SUPPLY_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-supply-almost-empty
|
||||
MARKER_SUPPLY_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-supply-missing
|
||||
MARKER_TONER_CARTRIDGE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-toner-cartridge-missing
|
||||
MARKER_TONER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-toner-missing
|
||||
MARKER_WASTE_INK_RECEPTACLE_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-ink-receptacle-almost-full
|
||||
MARKER_WASTE_INK_RECEPTACLE_FULL(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-ink-receptacle-full
|
||||
MARKER_WASTE_INK_RECEPTACLE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-ink-receptacle-missing
|
||||
MARKER_WASTE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-missing
|
||||
MARKER_WASTE_TONER_RECEPTACLE_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-toner-receptacle-almost-full
|
||||
MARKER_WASTE_TONER_RECEPTACLE_FULL(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-toner-receptacle-full
|
||||
MARKER_WASTE_TONER_RECEPTACLE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-toner-receptacle-missing
|
||||
MATERIAL_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // material-empty
|
||||
MATERIAL_LOW(REASON, NativePrinterStatus.UNMAPPED), // material-low
|
||||
MATERIAL_NEEDED(REASON, NativePrinterStatus.UNMAPPED), // material-needed
|
||||
MEDIA_DRYING(REASON, NativePrinterStatus.UNMAPPED), // media-drying
|
||||
MEDIA_PATH_CANNOT_DUPLEX_MEDIA_SELECTED(REASON, NativePrinterStatus.UNMAPPED), // media-path-cannot-duplex-media-selected
|
||||
MEDIA_PATH_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // media-path-failure
|
||||
MEDIA_PATH_INPUT_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // media-path-input-empty
|
||||
MEDIA_PATH_INPUT_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // media-path-input-feed-error
|
||||
MEDIA_PATH_INPUT_JAM(REASON, NativePrinterStatus.UNMAPPED), // media-path-input-jam
|
||||
MEDIA_PATH_INPUT_REQUEST(REASON, NativePrinterStatus.UNMAPPED), // media-path-input-request
|
||||
MEDIA_PATH_JAM(REASON, NativePrinterStatus.UNMAPPED), // media-path-jam
|
||||
MEDIA_PATH_MEDIA_TRAY_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // media-path-media-tray-almost-full
|
||||
MEDIA_PATH_MEDIA_TRAY_FULL(REASON, NativePrinterStatus.UNMAPPED), // media-path-media-tray-full
|
||||
MEDIA_PATH_MEDIA_TRAY_MISSING(REASON, NativePrinterStatus.UNMAPPED), // media-path-media-tray-missing
|
||||
MEDIA_PATH_OUTPUT_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // media-path-output-feed-error
|
||||
MEDIA_PATH_OUTPUT_FULL(REASON, NativePrinterStatus.UNMAPPED), // media-path-output-full
|
||||
MEDIA_PATH_OUTPUT_JAM(REASON, NativePrinterStatus.UNMAPPED), // media-path-output-jam
|
||||
MEDIA_PATH_PICK_ROLLER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // media-path-pick-roller-failure
|
||||
MEDIA_PATH_PICK_ROLLER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // media-path-pick-roller-life-over
|
||||
MEDIA_PATH_PICK_ROLLER_LIFE_WARN(REASON, NativePrinterStatus.UNMAPPED), // media-path-pick-roller-life-warn
|
||||
MEDIA_PATH_PICK_ROLLER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // media-path-pick-roller-missing
|
||||
MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // motor-failure
|
||||
OUTPUT_MAILBOX_SELECT_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // output-mailbox-select-failure
|
||||
OUTPUT_MEDIA_TRAY_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // output-media-tray-failure
|
||||
OUTPUT_MEDIA_TRAY_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // output-media-tray-feed-error
|
||||
OUTPUT_MEDIA_TRAY_JAM(REASON, NativePrinterStatus.UNMAPPED), // output-media-tray-jam
|
||||
PERFORATER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // perforater-added
|
||||
PERFORATER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // perforater-almost-empty
|
||||
PERFORATER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // perforater-almost-full
|
||||
PERFORATER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // perforater-at-limit
|
||||
PERFORATER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // perforater-closed
|
||||
PERFORATER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // perforater-configuration-change
|
||||
PERFORATER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // perforater-cover-closed
|
||||
PERFORATER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // perforater-cover-open
|
||||
PERFORATER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // perforater-empty
|
||||
PERFORATER_FULL(REASON, NativePrinterStatus.UNMAPPED), // perforater-full
|
||||
PERFORATER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // perforater-interlock-closed
|
||||
PERFORATER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // perforater-interlock-open
|
||||
PERFORATER_JAM(REASON, NativePrinterStatus.UNMAPPED), // perforater-jam
|
||||
PERFORATER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // perforater-life-almost-over
|
||||
PERFORATER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // perforater-life-over
|
||||
PERFORATER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // perforater-memory-exhausted
|
||||
PERFORATER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // perforater-missing
|
||||
PERFORATER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-motor-failure
|
||||
PERFORATER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // perforater-near-limit
|
||||
PERFORATER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // perforater-offline
|
||||
PERFORATER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // perforater-opened
|
||||
PERFORATER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-over-temperature
|
||||
PERFORATER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // perforater-power-saver
|
||||
PERFORATER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-recoverable-failure
|
||||
PERFORATER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // perforater-recoverable-storage
|
||||
PERFORATER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // perforater-removed
|
||||
PERFORATER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // perforater-resource-added
|
||||
PERFORATER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // perforater-resource-removed
|
||||
PERFORATER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-thermistor-failure
|
||||
PERFORATER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-timing-failure
|
||||
PERFORATER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // perforater-turned-off
|
||||
PERFORATER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // perforater-turned-on
|
||||
PERFORATER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-under-temperature
|
||||
PERFORATER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-unrecoverable-failure
|
||||
PERFORATER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // perforater-unrecoverable-storage-error
|
||||
PERFORATER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // perforater-warming-up
|
||||
PLATFORM_COOLING(REASON, NativePrinterStatus.UNMAPPED), // platform-cooling
|
||||
PLATFORM_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // platform-failure
|
||||
PLATFORM_HEATING(REASON, NativePrinterStatus.UNMAPPED), // platform-heating
|
||||
PLATFORM_TEMPERATURE_HIGH(REASON, NativePrinterStatus.UNMAPPED), // platform-temperature-high
|
||||
PLATFORM_TEMPERATURE_LOW(REASON, NativePrinterStatus.UNMAPPED), // platform-temperature-low
|
||||
POWER_DOWN(REASON, NativePrinterStatus.UNMAPPED), // power-down
|
||||
POWER_UP(REASON, NativePrinterStatus.UNMAPPED), // power-up
|
||||
PRINTER_MANUAL_RESET(REASON, NativePrinterStatus.UNMAPPED), // printer-manual-reset
|
||||
PRINTER_NMS_RESET(REASON, NativePrinterStatus.UNMAPPED), // printer-nms-reset
|
||||
PRINTER_READY_TO_PRINT(REASON, NativePrinterStatus.UNMAPPED), // printer-ready-to-print
|
||||
PUNCHER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // puncher-added
|
||||
PUNCHER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // puncher-almost-empty
|
||||
PUNCHER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // puncher-almost-full
|
||||
PUNCHER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // puncher-at-limit
|
||||
PUNCHER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // puncher-closed
|
||||
PUNCHER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // puncher-configuration-change
|
||||
PUNCHER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // puncher-cover-closed
|
||||
PUNCHER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // puncher-cover-open
|
||||
PUNCHER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // puncher-empty
|
||||
PUNCHER_FULL(REASON, NativePrinterStatus.UNMAPPED), // puncher-full
|
||||
PUNCHER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // puncher-interlock-closed
|
||||
PUNCHER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // puncher-interlock-open
|
||||
PUNCHER_JAM(REASON, NativePrinterStatus.UNMAPPED), // puncher-jam
|
||||
PUNCHER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // puncher-life-almost-over
|
||||
PUNCHER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // puncher-life-over
|
||||
PUNCHER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // puncher-memory-exhausted
|
||||
PUNCHER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // puncher-missing
|
||||
PUNCHER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-motor-failure
|
||||
PUNCHER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // puncher-near-limit
|
||||
PUNCHER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // puncher-offline
|
||||
PUNCHER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // puncher-opened
|
||||
PUNCHER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-over-temperature
|
||||
PUNCHER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // puncher-power-saver
|
||||
PUNCHER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-recoverable-failure
|
||||
PUNCHER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // puncher-recoverable-storage
|
||||
PUNCHER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // puncher-removed
|
||||
PUNCHER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // puncher-resource-added
|
||||
PUNCHER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // puncher-resource-removed
|
||||
PUNCHER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-thermistor-failure
|
||||
PUNCHER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-timing-failure
|
||||
PUNCHER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // puncher-turned-off
|
||||
PUNCHER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // puncher-turned-on
|
||||
PUNCHER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-under-temperature
|
||||
PUNCHER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-unrecoverable-failure
|
||||
PUNCHER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // puncher-unrecoverable-storage-error
|
||||
PUNCHER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // puncher-warming-up
|
||||
RESUMING(REASON, NativePrinterStatus.UNMAPPED), // resuming
|
||||
SCAN_MEDIA_PATH_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-failure
|
||||
SCAN_MEDIA_PATH_INPUT_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-input-empty
|
||||
SCAN_MEDIA_PATH_INPUT_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-input-feed-error
|
||||
SCAN_MEDIA_PATH_INPUT_JAM(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-input-jam
|
||||
SCAN_MEDIA_PATH_INPUT_REQUEST(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-input-request
|
||||
SCAN_MEDIA_PATH_JAM(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-jam
|
||||
SCAN_MEDIA_PATH_OUTPUT_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-output-feed-error
|
||||
SCAN_MEDIA_PATH_OUTPUT_FULL(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-output-full
|
||||
SCAN_MEDIA_PATH_OUTPUT_JAM(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-output-jam
|
||||
SCAN_MEDIA_PATH_PICK_ROLLER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-pick-roller-failure
|
||||
SCAN_MEDIA_PATH_PICK_ROLLER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-pick-roller-life-over
|
||||
SCAN_MEDIA_PATH_PICK_ROLLER_LIFE_WARN(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-pick-roller-life-warn
|
||||
SCAN_MEDIA_PATH_PICK_ROLLER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-pick-roller-missing
|
||||
SCAN_MEDIA_PATH_TRAY_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-tray-almost-full
|
||||
SCAN_MEDIA_PATH_TRAY_FULL(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-tray-full
|
||||
SCAN_MEDIA_PATH_TRAY_MISSING(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-tray-missing
|
||||
SCANNER_LIGHT_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // scanner-light-failure
|
||||
SCANNER_LIGHT_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // scanner-light-life-almost-over
|
||||
SCANNER_LIGHT_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // scanner-light-life-over
|
||||
SCANNER_LIGHT_MISSING(REASON, NativePrinterStatus.UNMAPPED), // scanner-light-missing
|
||||
SCANNER_SENSOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // scanner-sensor-failure
|
||||
SCANNER_SENSOR_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // scanner-sensor-life-almost-over
|
||||
SCANNER_SENSOR_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // scanner-sensor-life-over
|
||||
SCANNER_SENSOR_MISSING(REASON, NativePrinterStatus.UNMAPPED), // scanner-sensor-missing
|
||||
SEPARATION_CUTTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-added
|
||||
SEPARATION_CUTTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-almost-empty
|
||||
SEPARATION_CUTTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-almost-full
|
||||
SEPARATION_CUTTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-at-limit
|
||||
SEPARATION_CUTTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-closed
|
||||
SEPARATION_CUTTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-configuration-change
|
||||
SEPARATION_CUTTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-cover-closed
|
||||
SEPARATION_CUTTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-cover-open
|
||||
SEPARATION_CUTTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-empty
|
||||
SEPARATION_CUTTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-full
|
||||
SEPARATION_CUTTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-interlock-closed
|
||||
SEPARATION_CUTTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-interlock-open
|
||||
SEPARATION_CUTTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-jam
|
||||
SEPARATION_CUTTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-life-almost-over
|
||||
SEPARATION_CUTTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-life-over
|
||||
SEPARATION_CUTTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-memory-exhausted
|
||||
SEPARATION_CUTTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-missing
|
||||
SEPARATION_CUTTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-motor-failure
|
||||
SEPARATION_CUTTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-near-limit
|
||||
SEPARATION_CUTTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-offline
|
||||
SEPARATION_CUTTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-opened
|
||||
SEPARATION_CUTTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-over-temperature
|
||||
SEPARATION_CUTTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-power-saver
|
||||
SEPARATION_CUTTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-recoverable-failure
|
||||
SEPARATION_CUTTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-recoverable-storage
|
||||
SEPARATION_CUTTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-removed
|
||||
SEPARATION_CUTTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-resource-added
|
||||
SEPARATION_CUTTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-resource-removed
|
||||
SEPARATION_CUTTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-thermistor-failure
|
||||
SEPARATION_CUTTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-timing-failure
|
||||
SEPARATION_CUTTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-turned-off
|
||||
SEPARATION_CUTTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-turned-on
|
||||
SEPARATION_CUTTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-under-temperature
|
||||
SEPARATION_CUTTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-unrecoverable-failure
|
||||
SEPARATION_CUTTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-unrecoverable-storage-error
|
||||
SEPARATION_CUTTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-warming-up
|
||||
SHEET_ROTATOR_ADDED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-added
|
||||
SHEET_ROTATOR_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-almost-empty
|
||||
SHEET_ROTATOR_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-almost-full
|
||||
SHEET_ROTATOR_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-at-limit
|
||||
SHEET_ROTATOR_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-closed
|
||||
SHEET_ROTATOR_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-configuration-change
|
||||
SHEET_ROTATOR_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-cover-closed
|
||||
SHEET_ROTATOR_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-cover-open
|
||||
SHEET_ROTATOR_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-empty
|
||||
SHEET_ROTATOR_FULL(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-full
|
||||
SHEET_ROTATOR_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-interlock-closed
|
||||
SHEET_ROTATOR_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-interlock-open
|
||||
SHEET_ROTATOR_JAM(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-jam
|
||||
SHEET_ROTATOR_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-life-almost-over
|
||||
SHEET_ROTATOR_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-life-over
|
||||
SHEET_ROTATOR_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-memory-exhausted
|
||||
SHEET_ROTATOR_MISSING(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-missing
|
||||
SHEET_ROTATOR_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-motor-failure
|
||||
SHEET_ROTATOR_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-near-limit
|
||||
SHEET_ROTATOR_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-offline
|
||||
SHEET_ROTATOR_OPENED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-opened
|
||||
SHEET_ROTATOR_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-over-temperature
|
||||
SHEET_ROTATOR_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-power-saver
|
||||
SHEET_ROTATOR_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-recoverable-failure
|
||||
SHEET_ROTATOR_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-recoverable-storage
|
||||
SHEET_ROTATOR_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-removed
|
||||
SHEET_ROTATOR_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-resource-added
|
||||
SHEET_ROTATOR_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-resource-removed
|
||||
SHEET_ROTATOR_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-thermistor-failure
|
||||
SHEET_ROTATOR_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-timing-failure
|
||||
SHEET_ROTATOR_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-turned-off
|
||||
SHEET_ROTATOR_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-turned-on
|
||||
SHEET_ROTATOR_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-under-temperature
|
||||
SHEET_ROTATOR_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-unrecoverable-failure
|
||||
SHEET_ROTATOR_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-unrecoverable-storage-error
|
||||
SHEET_ROTATOR_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-warming-up
|
||||
SLITTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // slitter-added
|
||||
SLITTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // slitter-almost-empty
|
||||
SLITTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // slitter-almost-full
|
||||
SLITTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // slitter-at-limit
|
||||
SLITTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // slitter-closed
|
||||
SLITTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // slitter-configuration-change
|
||||
SLITTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // slitter-cover-closed
|
||||
SLITTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // slitter-cover-open
|
||||
SLITTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // slitter-empty
|
||||
SLITTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // slitter-full
|
||||
SLITTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // slitter-interlock-closed
|
||||
SLITTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // slitter-interlock-open
|
||||
SLITTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // slitter-jam
|
||||
SLITTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // slitter-life-almost-over
|
||||
SLITTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // slitter-life-over
|
||||
SLITTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // slitter-memory-exhausted
|
||||
SLITTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // slitter-missing
|
||||
SLITTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-motor-failure
|
||||
SLITTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // slitter-near-limit
|
||||
SLITTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // slitter-offline
|
||||
SLITTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // slitter-opened
|
||||
SLITTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-over-temperature
|
||||
SLITTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // slitter-power-saver
|
||||
SLITTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-recoverable-failure
|
||||
SLITTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // slitter-recoverable-storage
|
||||
SLITTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // slitter-removed
|
||||
SLITTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // slitter-resource-added
|
||||
SLITTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // slitter-resource-removed
|
||||
SLITTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-thermistor-failure
|
||||
SLITTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-timing-failure
|
||||
SLITTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // slitter-turned-off
|
||||
SLITTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // slitter-turned-on
|
||||
SLITTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-under-temperature
|
||||
SLITTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-unrecoverable-failure
|
||||
SLITTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // slitter-unrecoverable-storage-error
|
||||
SLITTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // slitter-warming-up
|
||||
STACKER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stacker-added
|
||||
STACKER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stacker-almost-empty
|
||||
STACKER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // stacker-almost-full
|
||||
STACKER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stacker-at-limit
|
||||
STACKER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stacker-closed
|
||||
STACKER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // stacker-configuration-change
|
||||
STACKER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stacker-cover-closed
|
||||
STACKER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stacker-cover-open
|
||||
STACKER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stacker-empty
|
||||
STACKER_FULL(REASON, NativePrinterStatus.UNMAPPED), // stacker-full
|
||||
STACKER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stacker-interlock-closed
|
||||
STACKER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stacker-interlock-open
|
||||
STACKER_JAM(REASON, NativePrinterStatus.UNMAPPED), // stacker-jam
|
||||
STACKER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // stacker-life-almost-over
|
||||
STACKER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // stacker-life-over
|
||||
STACKER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // stacker-memory-exhausted
|
||||
STACKER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // stacker-missing
|
||||
STACKER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-motor-failure
|
||||
STACKER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stacker-near-limit
|
||||
STACKER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // stacker-offline
|
||||
STACKER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // stacker-opened
|
||||
STACKER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-over-temperature
|
||||
STACKER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // stacker-power-saver
|
||||
STACKER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-recoverable-failure
|
||||
STACKER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // stacker-recoverable-storage
|
||||
STACKER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stacker-removed
|
||||
STACKER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stacker-resource-added
|
||||
STACKER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stacker-resource-removed
|
||||
STACKER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-thermistor-failure
|
||||
STACKER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-timing-failure
|
||||
STACKER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // stacker-turned-off
|
||||
STACKER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // stacker-turned-on
|
||||
STACKER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-under-temperature
|
||||
STACKER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-unrecoverable-failure
|
||||
STACKER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // stacker-unrecoverable-storage-error
|
||||
STACKER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // stacker-warming-up
|
||||
STANDBY(REASON, NativePrinterStatus.UNMAPPED), // standby
|
||||
STAPLER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stapler-added
|
||||
STAPLER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stapler-almost-empty
|
||||
STAPLER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // stapler-almost-full
|
||||
STAPLER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stapler-at-limit
|
||||
STAPLER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stapler-closed
|
||||
STAPLER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // stapler-configuration-change
|
||||
STAPLER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stapler-cover-closed
|
||||
STAPLER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stapler-cover-open
|
||||
STAPLER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stapler-empty
|
||||
STAPLER_FULL(REASON, NativePrinterStatus.UNMAPPED), // stapler-full
|
||||
STAPLER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stapler-interlock-closed
|
||||
STAPLER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stapler-interlock-open
|
||||
STAPLER_JAM(REASON, NativePrinterStatus.UNMAPPED), // stapler-jam
|
||||
STAPLER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // stapler-life-almost-over
|
||||
STAPLER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // stapler-life-over
|
||||
STAPLER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // stapler-memory-exhausted
|
||||
STAPLER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // stapler-missing
|
||||
STAPLER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-motor-failure
|
||||
STAPLER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stapler-near-limit
|
||||
STAPLER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // stapler-offline
|
||||
STAPLER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // stapler-opened
|
||||
STAPLER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-over-temperature
|
||||
STAPLER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // stapler-power-saver
|
||||
STAPLER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-recoverable-failure
|
||||
STAPLER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // stapler-recoverable-storage
|
||||
STAPLER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stapler-removed
|
||||
STAPLER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stapler-resource-added
|
||||
STAPLER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stapler-resource-removed
|
||||
STAPLER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-thermistor-failure
|
||||
STAPLER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-timing-failure
|
||||
STAPLER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // stapler-turned-off
|
||||
STAPLER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // stapler-turned-on
|
||||
STAPLER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-under-temperature
|
||||
STAPLER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-unrecoverable-failure
|
||||
STAPLER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // stapler-unrecoverable-storage-error
|
||||
STAPLER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // stapler-warming-up
|
||||
STITCHER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-added
|
||||
STITCHER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stitcher-almost-empty
|
||||
STITCHER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // stitcher-almost-full
|
||||
STITCHER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stitcher-at-limit
|
||||
STITCHER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-closed
|
||||
STITCHER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-configuration-change
|
||||
STITCHER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-cover-closed
|
||||
STITCHER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stitcher-cover-open
|
||||
STITCHER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stitcher-empty
|
||||
STITCHER_FULL(REASON, NativePrinterStatus.UNMAPPED), // stitcher-full
|
||||
STITCHER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-interlock-closed
|
||||
STITCHER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stitcher-interlock-open
|
||||
STITCHER_JAM(REASON, NativePrinterStatus.UNMAPPED), // stitcher-jam
|
||||
STITCHER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // stitcher-life-almost-over
|
||||
STITCHER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // stitcher-life-over
|
||||
STITCHER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-memory-exhausted
|
||||
STITCHER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // stitcher-missing
|
||||
STITCHER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-motor-failure
|
||||
STITCHER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stitcher-near-limit
|
||||
STITCHER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-offline
|
||||
STITCHER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-opened
|
||||
STITCHER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-over-temperature
|
||||
STITCHER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // stitcher-power-saver
|
||||
STITCHER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-recoverable-failure
|
||||
STITCHER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-recoverable-storage
|
||||
STITCHER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-removed
|
||||
STITCHER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-resource-added
|
||||
STITCHER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-resource-removed
|
||||
STITCHER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-thermistor-failure
|
||||
STITCHER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-timing-failure
|
||||
STITCHER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // stitcher-turned-off
|
||||
STITCHER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // stitcher-turned-on
|
||||
STITCHER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-under-temperature
|
||||
STITCHER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-unrecoverable-failure
|
||||
STITCHER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // stitcher-unrecoverable-storage-error
|
||||
STITCHER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // stitcher-warming-up
|
||||
SUBUNIT_ADDED(REASON, NativePrinterStatus.UNMAPPED), // subunit-added
|
||||
SUBUNIT_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // subunit-almost-empty
|
||||
SUBUNIT_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // subunit-almost-full
|
||||
SUBUNIT_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // subunit-at-limit
|
||||
SUBUNIT_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // subunit-closed
|
||||
SUBUNIT_COOLING_DOWN(REASON, NativePrinterStatus.UNMAPPED), // subunit-cooling-down
|
||||
SUBUNIT_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // subunit-empty
|
||||
SUBUNIT_FULL(REASON, NativePrinterStatus.UNMAPPED), // subunit-full
|
||||
SUBUNIT_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // subunit-life-almost-over
|
||||
SUBUNIT_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // subunit-life-over
|
||||
SUBUNIT_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // subunit-memory-exhausted
|
||||
SUBUNIT_MISSING(REASON, NativePrinterStatus.UNMAPPED), // subunit-missing
|
||||
SUBUNIT_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-motor-failure
|
||||
SUBUNIT_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // subunit-near-limit
|
||||
SUBUNIT_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // subunit-offline
|
||||
SUBUNIT_OPENED(REASON, NativePrinterStatus.UNMAPPED), // subunit-opened
|
||||
SUBUNIT_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-over-temperature
|
||||
SUBUNIT_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // subunit-power-saver
|
||||
SUBUNIT_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-recoverable-failure
|
||||
SUBUNIT_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // subunit-recoverable-storage
|
||||
SUBUNIT_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // subunit-removed
|
||||
SUBUNIT_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // subunit-resource-added
|
||||
SUBUNIT_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // subunit-resource-removed
|
||||
SUBUNIT_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-thermistor-failure
|
||||
SUBUNIT_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-timing-Failure
|
||||
SUBUNIT_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // subunit-turned-off
|
||||
SUBUNIT_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // subunit-turned-on
|
||||
SUBUNIT_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-under-temperature
|
||||
SUBUNIT_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-unrecoverable-failure
|
||||
SUBUNIT_UNRECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // subunit-unrecoverable-storage
|
||||
SUBUNIT_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // subunit-warming-up
|
||||
SUSPEND(REASON, NativePrinterStatus.UNMAPPED), // suspend
|
||||
TESTING(REASON, NativePrinterStatus.UNMAPPED), // testing
|
||||
TRIMMER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-added
|
||||
TRIMMER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // trimmer-almost-empty
|
||||
TRIMMER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // trimmer-almost-full
|
||||
TRIMMER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // trimmer-at-limit
|
||||
TRIMMER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-closed
|
||||
TRIMMER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-configuration-change
|
||||
TRIMMER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-cover-closed
|
||||
TRIMMER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // trimmer-cover-open
|
||||
TRIMMER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // trimmer-empty
|
||||
TRIMMER_FULL(REASON, NativePrinterStatus.UNMAPPED), // trimmer-full
|
||||
TRIMMER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-interlock-closed
|
||||
TRIMMER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // trimmer-interlock-open
|
||||
TRIMMER_JAM(REASON, NativePrinterStatus.UNMAPPED), // trimmer-jam
|
||||
TRIMMER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // trimmer-life-almost-over
|
||||
TRIMMER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // trimmer-life-over
|
||||
TRIMMER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-memory-exhausted
|
||||
TRIMMER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // trimmer-missing
|
||||
TRIMMER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-motor-failure
|
||||
TRIMMER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // trimmer-near-limit
|
||||
TRIMMER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-offline
|
||||
TRIMMER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-opened
|
||||
TRIMMER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-over-temperature
|
||||
TRIMMER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // trimmer-power-saver
|
||||
TRIMMER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-recoverable-failure
|
||||
TRIMMER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-recoverable-storage
|
||||
TRIMMER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-removed
|
||||
TRIMMER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-resource-added
|
||||
TRIMMER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-resource-removed
|
||||
TRIMMER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-thermistor-failure
|
||||
TRIMMER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-timing-failure
|
||||
TRIMMER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // trimmer-turned-off
|
||||
TRIMMER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // trimmer-turned-on
|
||||
TRIMMER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-under-temperature
|
||||
TRIMMER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-unrecoverable-failure
|
||||
TRIMMER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // trimmer-unrecoverable-storage-error
|
||||
TRIMMER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // trimmer-warming-up
|
||||
UNKNOWN(REASON, NativePrinterStatus.UNMAPPED), // unknown
|
||||
WRAPPER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-added
|
||||
WRAPPER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // wrapper-almost-empty
|
||||
WRAPPER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // wrapper-almost-full
|
||||
WRAPPER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // wrapper-at-limit
|
||||
WRAPPER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-closed
|
||||
WRAPPER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-configuration-change
|
||||
WRAPPER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-cover-closed
|
||||
WRAPPER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // wrapper-cover-open
|
||||
WRAPPER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // wrapper-empty
|
||||
WRAPPER_FULL(REASON, NativePrinterStatus.UNMAPPED), // wrapper-full
|
||||
WRAPPER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-interlock-closed
|
||||
WRAPPER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // wrapper-interlock-open
|
||||
WRAPPER_JAM(REASON, NativePrinterStatus.UNMAPPED), // wrapper-jam
|
||||
WRAPPER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // wrapper-life-almost-over
|
||||
WRAPPER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // wrapper-life-over
|
||||
WRAPPER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-memory-exhausted
|
||||
WRAPPER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // wrapper-missing
|
||||
WRAPPER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-motor-failure
|
||||
WRAPPER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // wrapper-near-limit
|
||||
WRAPPER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-offline
|
||||
WRAPPER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-opened
|
||||
WRAPPER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-over-temperature
|
||||
WRAPPER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // wrapper-power-saver
|
||||
WRAPPER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-recoverable-failure
|
||||
WRAPPER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-recoverable-storage
|
||||
WRAPPER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-removed
|
||||
WRAPPER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-resource-added
|
||||
WRAPPER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-resource-removed
|
||||
WRAPPER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-thermistor-failure
|
||||
WRAPPER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-timing-failure
|
||||
WRAPPER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // wrapper-turned-off
|
||||
WRAPPER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // wrapper-turned-on
|
||||
WRAPPER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-under-temperature
|
||||
WRAPPER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-unrecoverable-failure
|
||||
WRAPPER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // wrapper-unrecoverable-storage-error
|
||||
WRAPPER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED); // wrapper-warming-up
|
||||
|
||||
private static final Logger log = LogManager.getLogger(CupsPrinterStatusMap.class);
|
||||
private static final String[] SNMP_REDUNDANT_SUFFIXES = { "-warning", "-report" };
|
||||
public static SortedMap<String,NativePrinterStatus> sortedReasonLookupTable;
|
||||
public static SortedMap<String,NativePrinterStatus> sortedStateLookupTable;
|
||||
|
||||
private NativePrinterStatus parent;
|
||||
private CupsPrinterStatusType type;
|
||||
|
||||
enum CupsPrinterStatusType {
|
||||
STATE,
|
||||
REASON;
|
||||
}
|
||||
|
||||
CupsPrinterStatusMap(CupsPrinterStatusType type, NativePrinterStatus parent) {
|
||||
this.type = type;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
public static NativePrinterStatus matchReason(String code) {
|
||||
// Initialize a sorted map to speed up lookups
|
||||
if(sortedReasonLookupTable == null) {
|
||||
sortedReasonLookupTable = new TreeMap<>();
|
||||
for(CupsPrinterStatusMap value : values()) {
|
||||
if(value.type == REASON) {
|
||||
sortedReasonLookupTable.put(value.name().toLowerCase(Locale.ENGLISH).replace("_", "-"), value.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
NativePrinterStatus status = sortedReasonLookupTable.get(code);
|
||||
return status;
|
||||
}
|
||||
|
||||
public static NativePrinterStatus matchState(String state) {
|
||||
// Initialize a sorted map to speed up lookups
|
||||
if(sortedStateLookupTable == null) {
|
||||
sortedStateLookupTable = new TreeMap<>();
|
||||
for(CupsPrinterStatusMap value : values()) {
|
||||
if(value.type == STATE) {
|
||||
sortedStateLookupTable.put(value.name().toLowerCase(Locale.ENGLISH).replace("_", "-"), value.parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortedStateLookupTable.getOrDefault(state, NativePrinterStatus.UNMAPPED);
|
||||
}
|
||||
|
||||
public static Status createStatus(String reason, String state, String printer) {
|
||||
NativePrinterStatus cupsPrinterStatus = matchReason(reason);
|
||||
|
||||
// Edge-case for snmp statuses
|
||||
if(cupsPrinterStatus == null) {
|
||||
String sanitizedReason = snmpSanitize(reason);
|
||||
if (!reason.equals(sanitizedReason)) {
|
||||
cupsPrinterStatus = sortedReasonLookupTable.get(sanitizedReason);
|
||||
if (cupsPrinterStatus != null) reason = sanitizedReason;
|
||||
}
|
||||
}
|
||||
|
||||
if(cupsPrinterStatus == null && !reason.equalsIgnoreCase("none")) {
|
||||
// Don't warn for "none"
|
||||
log.warn("Printer state-reason \"{}\" was not found", reason);
|
||||
}
|
||||
|
||||
if(cupsPrinterStatus == null) {
|
||||
// Don't return the raw reason if we couldn't find it mapped, return state instead
|
||||
return new Status(matchState(state), printer, state);
|
||||
} else if(cupsPrinterStatus == NativePrinterStatus.UNMAPPED) {
|
||||
// Still return the state, but let the user know what the unmapped state reason was
|
||||
return new Status(matchState(state), printer, reason);
|
||||
}
|
||||
return new Status(cupsPrinterStatus, printer, reason);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativePrinterStatus getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getRawCode() {
|
||||
return name().toLowerCase(Locale.ENGLISH).replace("_", "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes redundant "-warning" or "-report" from SNMP-originated statuses
|
||||
*/
|
||||
public static String snmpSanitize(String cupsString) {
|
||||
for(String suffix : SNMP_REDUNDANT_SUFFIXES) {
|
||||
if (cupsString.endsWith(suffix)) {
|
||||
return cupsString.substring(0, cupsString.length() - suffix.length());
|
||||
}
|
||||
}
|
||||
return cupsString;
|
||||
}
|
||||
}
|
||||
56
old code/tray/src/qz/printer/status/printer/NativePrinterStatus.java
Executable file
56
old code/tray/src/qz/printer/status/printer/NativePrinterStatus.java
Executable file
@@ -0,0 +1,56 @@
|
||||
package qz.printer.status.printer;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import qz.printer.status.NativeStatus;
|
||||
|
||||
import static org.apache.logging.log4j.Level.*;
|
||||
|
||||
/**
|
||||
* Created by kyle on 7/7/17.
|
||||
*/
|
||||
public enum NativePrinterStatus implements NativeStatus {
|
||||
OK(INFO),
|
||||
PAUSED(WARN),
|
||||
ERROR(FATAL),
|
||||
PENDING_DELETION(WARN),
|
||||
PAPER_JAM(FATAL),
|
||||
PAPER_OUT(WARN),
|
||||
MANUAL_FEED(INFO),
|
||||
PAPER_PROBLEM(WARN),
|
||||
OFFLINE(FATAL),
|
||||
IO_ACTIVE(INFO),
|
||||
BUSY(INFO),
|
||||
PRINTING(INFO),
|
||||
OUTPUT_BIN_FULL(WARN),
|
||||
NOT_AVAILABLE(FATAL),
|
||||
WAITING(INFO),
|
||||
PROCESSING(INFO),
|
||||
INITIALIZING(INFO),
|
||||
WARMING_UP(INFO),
|
||||
TONER_LOW(WARN),
|
||||
NO_TONER(FATAL),
|
||||
PAGE_PUNT(FATAL),
|
||||
USER_INTERVENTION(WARN),
|
||||
OUT_OF_MEMORY(FATAL),
|
||||
DOOR_OPEN(WARN),
|
||||
SERVER_UNKNOWN(WARN),
|
||||
POWER_SAVE(INFO),
|
||||
UNKNOWN(INFO),
|
||||
UNMAPPED(FATAL); // should never make it to the user
|
||||
|
||||
private Level level;
|
||||
|
||||
NativePrinterStatus(Level level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeStatus getDefault() {
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Level getLevel() {
|
||||
return level;
|
||||
}
|
||||
}
|
||||
93
old code/tray/src/qz/printer/status/printer/WmiPrinterStatusMap.java
Executable file
93
old code/tray/src/qz/printer/status/printer/WmiPrinterStatusMap.java
Executable file
@@ -0,0 +1,93 @@
|
||||
package qz.printer.status.printer;
|
||||
|
||||
import qz.printer.status.NativeStatus;
|
||||
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Created by kyle on 5/18/17.
|
||||
*/
|
||||
public enum WmiPrinterStatusMap implements NativeStatus.NativeMap {
|
||||
OK(NativePrinterStatus.OK, 0x00000000, true),
|
||||
PAUSED(NativePrinterStatus.PAUSED, 0x00000001, false),
|
||||
ERROR(NativePrinterStatus.ERROR, 0x00000002, false),
|
||||
PENDING_DELETION(NativePrinterStatus.PENDING_DELETION, 0x00000004, true),
|
||||
PAPER_JAM(NativePrinterStatus.PAPER_JAM, 0x00000008, false),
|
||||
PAPER_OUT(NativePrinterStatus.PAPER_OUT, 0x00000010, false),
|
||||
MANUAL_FEED(NativePrinterStatus.MANUAL_FEED, 0x00000020, false),
|
||||
PAPER_PROBLEM(NativePrinterStatus.PAPER_PROBLEM, 0x00000040, false),
|
||||
OFFLINE(NativePrinterStatus.OFFLINE, 0x00000080, false),
|
||||
IO_ACTIVE(NativePrinterStatus.IO_ACTIVE, 0x00000100, true),
|
||||
BUSY(NativePrinterStatus.BUSY, 0x00000200, true),
|
||||
PRINTING(NativePrinterStatus.PRINTING, 0x00000400, true),
|
||||
OUTPUT_BIN_FULL(NativePrinterStatus.OUTPUT_BIN_FULL, 0x00000800, false),
|
||||
NOT_AVAILABLE(NativePrinterStatus.NOT_AVAILABLE, 0x00001000, false),
|
||||
WAITING(NativePrinterStatus.WAITING, 0x00002000, true),
|
||||
PROCESSING(NativePrinterStatus.PROCESSING, 0x00004000, true),
|
||||
INITIALIZING(NativePrinterStatus.INITIALIZING, 0x00008000, true),
|
||||
WARMING_UP(NativePrinterStatus.WARMING_UP, 0x00010000, true),
|
||||
TONER_LOW(NativePrinterStatus.TONER_LOW, 0x00020000, true),
|
||||
NO_TONER(NativePrinterStatus.NO_TONER, 0x00040000, false),
|
||||
PAGE_PUNT(NativePrinterStatus.PAGE_PUNT, 0x00080000, true),
|
||||
USER_INTERVENTION(NativePrinterStatus.USER_INTERVENTION, 0x00100000, false),
|
||||
OUT_OF_MEMORY(NativePrinterStatus.OUT_OF_MEMORY, 0x00200000, false),
|
||||
DOOR_OPEN(NativePrinterStatus.DOOR_OPEN, 0x00400000, false),
|
||||
SERVER_UNKNOWN(NativePrinterStatus.SERVER_UNKNOWN, 0x00800000, false),
|
||||
POWER_SAVE(NativePrinterStatus.POWER_SAVE, 0x01000000, true),
|
||||
|
||||
/**
|
||||
* For internal use only, not WMI values (change as needed)
|
||||
*/
|
||||
// Used for mapping PRINTER_ATTRIBUTE_WORK_OFFLINE from printer attributes to printer status
|
||||
ATTRIBUTE_WORK_OFFLINE(NativePrinterStatus.OFFLINE, 0x04000000, false),
|
||||
// "Unknown" placeholder for future/unmapped values
|
||||
UNKNOWN_STATUS(NativePrinterStatus.UNKNOWN, 0x02000000, false);
|
||||
|
||||
public static int NOT_OK_MASK = getNotOkMask();
|
||||
private static SortedMap<Integer,NativePrinterStatus> sortedLookupTable;
|
||||
|
||||
private NativePrinterStatus parent;
|
||||
private final int rawCode;
|
||||
|
||||
// Printer status isn't very good about reporting recovered errors, we'll try to track them manually
|
||||
private boolean isOk;
|
||||
|
||||
WmiPrinterStatusMap(NativePrinterStatus parent, int rawCode, boolean isOK) {
|
||||
this.parent = parent;
|
||||
this.rawCode = rawCode;
|
||||
this.isOk = isOK;
|
||||
}
|
||||
|
||||
public static NativePrinterStatus match(int rawCode) {
|
||||
// Initialize a sorted map to speed up lookups
|
||||
if(sortedLookupTable == null) {
|
||||
sortedLookupTable = new TreeMap<>();
|
||||
for(WmiPrinterStatusMap value : values()) {
|
||||
sortedLookupTable.put(value.rawCode, value.parent);
|
||||
}
|
||||
}
|
||||
|
||||
return sortedLookupTable.get(rawCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NativeStatus getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getRawCode() {
|
||||
return rawCode;
|
||||
}
|
||||
|
||||
private static int getNotOkMask() {
|
||||
int result = 0;
|
||||
for(WmiPrinterStatusMap code : values()) {
|
||||
if(!code.isOk) {
|
||||
result |= code.rawCode;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user