/* IppResponse.java -- Copyright (C) 2006 Free Software Foundation, Inc. This file is part of GNU Classpath. GNU Classpath is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. GNU Classpath is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Classpath; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Linking this library statically or dynamically with other modules is making a combined work based on this library. Thus, the terms and conditions of the GNU General Public License cover the whole combination. As a special exception, the copyright holders of this library give you permission to link this library with independent modules to produce an executable, regardless of the license terms of these independent modules, and to copy and distribute the resulting executable under terms of your choice, provided that you also meet, for each linked independent module, the terms and conditions of the license of that module. An independent module is a module which is not derived from or based on this library. If you modify this library, you may extend this exception to your version of the library, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. */ package gnu.javax.print.ipp; import gnu.classpath.debug.Component; import gnu.classpath.debug.SystemLogger; import gnu.javax.print.ipp.attribute.UnknownAttribute; import gnu.javax.print.ipp.attribute.defaults.DocumentFormatDefault; import gnu.javax.print.ipp.attribute.defaults.JobHoldUntilDefault; import gnu.javax.print.ipp.attribute.defaults.JobSheetsDefault; import gnu.javax.print.ipp.attribute.defaults.MediaDefault; import gnu.javax.print.ipp.attribute.defaults.PrinterResolutionDefault; import gnu.javax.print.ipp.attribute.job.AttributesCharset; import gnu.javax.print.ipp.attribute.job.AttributesNaturalLanguage; import gnu.javax.print.ipp.attribute.job.JobMoreInfo; import gnu.javax.print.ipp.attribute.job.JobPrinterUri; import gnu.javax.print.ipp.attribute.job.JobUri; import gnu.javax.print.ipp.attribute.printer.CharsetConfigured; import gnu.javax.print.ipp.attribute.printer.DocumentFormat; import gnu.javax.print.ipp.attribute.printer.NaturalLanguageConfigured; import gnu.javax.print.ipp.attribute.printer.PrinterCurrentTime; import gnu.javax.print.ipp.attribute.printer.PrinterDriverInstaller; import gnu.javax.print.ipp.attribute.supported.CharsetSupported; import gnu.javax.print.ipp.attribute.supported.DocumentFormatSupported; import gnu.javax.print.ipp.attribute.supported.GeneratedNaturalLanguageSupported; import gnu.javax.print.ipp.attribute.supported.JobHoldUntilSupported; import gnu.javax.print.ipp.attribute.supported.JobSheetsSupported; import gnu.javax.print.ipp.attribute.supported.MediaSupported; import gnu.javax.print.ipp.attribute.supported.PrinterResolutionSupported; import gnu.javax.print.ipp.attribute.supported.PrinterUriSupported; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import javax.print.attribute.Attribute; import javax.print.attribute.standard.CopiesSupported; import javax.print.attribute.standard.DateTimeAtCompleted; import javax.print.attribute.standard.DateTimeAtCreation; import javax.print.attribute.standard.DateTimeAtProcessing; import javax.print.attribute.standard.JobImpressionsSupported; import javax.print.attribute.standard.JobKOctetsSupported; import javax.print.attribute.standard.JobMediaSheetsSupported; import javax.print.attribute.standard.JobStateReason; import javax.print.attribute.standard.JobStateReasons; import javax.print.attribute.standard.NumberUpSupported; import javax.print.attribute.standard.PrinterMoreInfo; import javax.print.attribute.standard.PrinterMoreInfoManufacturer; import javax.print.attribute.standard.PrinterStateReason; import javax.print.attribute.standard.PrinterStateReasons; import javax.print.attribute.standard.Severity; /** * <code>IppResponse</code> models a response received from an IPP * compatible server as described in RFC 2910 IPP 1.1 Encoding and Transport. * * @author Wolfgang Baer (WBaer@gmx.de) */ public class IppResponse { /** * <code>ResponseReader</code> is responsible for parsing an IPP 1.1 * response stream. It provides access to the attribute groups after parsing * via getter methods. * <p> * The enconding of a response is structured as follows (for an official * description please have a look at the RFC document mentioned above): * <ul> * <li>version-number - 2 bytes - required</li> * <li>status-code - 2 bytes - required</li> * <li>request-id - 4 bytes - required</li> * <li>attribute-group - n bytes - 0 or more</li> * <li>end-of-attributes-tag - 1 byte - required</li> * <li>data - q bytes - optional</li> * </ul> * </p><p> * Where each attribute-group (if any) is encoded as follows: * <ul> * <li>begin-attribute-group-tag - 1 byte</li> * <li>attribute - p bytes - 0 or more</li> * </ul> * </p><p> * Encoding of attributes: * <ul> * <li>attribute-with-one-value - q bytes</li> * <li>additional-value - r bytes - 0 or more</li> * </ul> * </p><p> * Encoding of attribute-with-one-value: * <ul> * <li>value-tag - 1 byte</li> * <li>name-length (value is u) - 2 bytes</li> * <li>name - u bytes</li> * <li>value-length (value is v) - 2 bytes</li> * <li>value - v bytes</li> * </ul> * </p><p> * Encoding of additional value: * <ul> * <li>value-tag - 1 byte</li> * <li>name-length (value is 0x0000) - 2 bytes</li> * <li>value-length (value is w) - 2 bytes</li> * <li>value - w bytes</li> * </ul> * </p> * * @author Wolfgang Baer (WBaer@gmx.de) */ class ResponseReader { /** The IPP version defaults to 1.1 */ private static final short VERSION = 0x0101; /** * Parses the inputstream containing the response of the IPP request. * @param input the inputstream * @throws IppException if unexpected exceptions occur. * @throws IOException if IO problems with the underlying inputstream occur. */ public void parseResponse(InputStream input) throws IppException, IOException { DataInputStream stream = new DataInputStream(input); short version = stream.readShort(); status_code = stream.readShort(); request_id = stream.readInt(); if (VERSION != version) throw new IppException("Version mismatch - " + "implementation does not support other versions than IPP 1.1"); logger.log(Component.IPP, "Statuscode: " + Integer.toHexString(status_code) + " Request-ID: " + request_id); byte tag = 0; boolean proceed = true; HashMap tmp; // iterate over attribute-groups until end-of-attributes-tag is found while (proceed) { if (tag == 0) // only at start time tag = stream.readByte(); logger.log(Component.IPP, "DelimiterTag: " + Integer.toHexString(tag)); // check if end of attributes switch (tag) { case IppDelimiterTag.END_OF_ATTRIBUTES_TAG: proceed = false; break; case IppDelimiterTag.OPERATION_ATTRIBUTES_TAG: tmp = new HashMap(); tag = parseAttributes(tmp, stream); operationAttributes.add(tmp); break; case IppDelimiterTag.JOB_ATTRIBUTES_TAG: tmp = new HashMap(); tag = parseAttributes(tmp, stream); jobAttributes.add(tmp); break; case IppDelimiterTag.PRINTER_ATTRIBUTES_TAG: tmp = new HashMap(); tag = parseAttributes(tmp, stream); printerAttributes.add(tmp); break; case IppDelimiterTag.UNSUPPORTED_ATTRIBUTES_TAG: System.out.println("Called"); tmp = new HashMap(); tag = parseAttributes(tmp, stream); unsupportedAttributes.add(tmp); break; default: throw new IppException("Unknown tag with value " + Integer.toHexString(tag) + " occured."); } } // if there are more bytes that has to be data. ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); byte[] readbuf = new byte[2048]; int len = 0; while ((len = stream.read(readbuf)) > 0) byteStream.write(readbuf, 0, len); byteStream.flush(); data = byteStream.toByteArray(); } /** * The actual parsing of the attributes and further putting into the * provided group maps. * @param attributes the provided attribute group map. * @param stream the provided stream to read from. * @return The last read tag byte (normally a DelimiterTag) * @throws IppException if unexpected exceptions occur. * @throws IOException if IO problems with the underlying inputstream occur. */ private byte parseAttributes(Map attributes, DataInputStream stream) throws IppException, IOException { Attribute lastAttribute = null; Attribute attribute = null; // declaration of variables short nameLength; String name; short valueLength; byte[] value; // tmp variables for parsing // declared here so no name duplication occurs URI uri; String str; while (true) { byte tag = stream.readByte(); if (IppDelimiterTag.isDelimiterTag(tag)) return tag; // it must be a value tag now // so we have either a attribute-with-one-value // or (if setOf is possible) an additional-value // (1) Length of the name nameLength = stream.readShort(); // (2) The name itself // may be an additional-value if (nameLength == 0x0000) name = lastAttribute.getName(); else { byte[] nameBytes = new byte[nameLength]; stream.read(nameBytes); name = new String(nameBytes); } // (3) Length of the value valueLength = stream.readShort(); // (4) The value itself value = new byte[valueLength]; stream.read(value); // the value itself switch (tag) { // out-of-band values case IppValueTag.UNSUPPORTED: case IppValueTag.UNKNOWN: case IppValueTag.NO_VALUE: // TODO implement out-of-band handling // We currently throw an exception to see when it occurs - not yet :-) throw new IppException( "Unexpected name value for out-of-band value tag"); case IppValueTag.INTEGER: int intValue = IppUtilities.convertToInt(value); attribute = IppUtilities.getIntegerAttribute(name, intValue); break; case IppValueTag.BOOLEAN: // JPS API models boolean syntax type as enums // 0x01 = true, 0x00 = false - all are enums attribute = IppUtilities.getEnumAttribute(name, new Integer(value[0])); break; case IppValueTag.ENUM: int intVal = IppUtilities.convertToInt(value); attribute = IppUtilities.getEnumAttribute(name, new Integer(intVal)); break; case IppValueTag.OCTECTSTRING_UNSPECIFIED: // none exists according to spec // so lets report as exception to see when it occurs throw new IppException("Unspecified octet string occured."); case IppValueTag.DATETIME: Date date = parseDate(value); if (name.equals("printer-current-time")) attribute = new PrinterCurrentTime(date); else if (name.equals("date-time-at-creation")) attribute = new DateTimeAtCreation(date); else if (name.equals("date-time-at-processing")) attribute = new DateTimeAtProcessing(date); else if (name.equals("date-time-at-completed")) attribute = new DateTimeAtCompleted(date); break; case IppValueTag.RESOLUTION: int crossFeed = IppUtilities.convertToInt(value[0], value[1], value[2], value[3]); int feed = IppUtilities.convertToInt(value[4], value[5], value[6], value[7]); int units = value[8]; if (name.equals("printer-resolution-default")) attribute = new PrinterResolutionDefault(crossFeed, feed, units); else if (name.equals("printer-resolution-supported")) // may be here also attribute = new PrinterResolutionSupported(crossFeed, feed, units); break; case IppValueTag.RANGEOFINTEGER: int lower = IppUtilities.convertToInt(value[0], value[1], value[2], value[3]); int upper = IppUtilities.convertToInt(value[4], value[5], value[6], value[7]); if (name.equals("copies-supported")) attribute = new CopiesSupported(lower, upper); else if (name.equals("number-up-supported")) attribute = new NumberUpSupported(lower, upper); else if (name.equals("job-k-octets-supported")) attribute = new JobKOctetsSupported(lower, upper); else if (name.equals("job-impressions-supported")) attribute = new JobImpressionsSupported(lower, upper); else if (name.equals("job-media-sheets-supported")) attribute = new JobMediaSheetsSupported(lower, upper); break; case IppValueTag.TEXT_WITH_LANGUAGE: case IppValueTag.TEXT_WITHOUT_LANGUAGE: case IppValueTag.NAME_WITH_LANGUAGE: case IppValueTag.NAME_WITHOUT_LANGUAGE: attribute = IppUtilities.getTextAttribute(name, tag, value); break; case IppValueTag.KEYWORD: str = new String(value); if (name.equals("job-hold-until-supported")) // may also be name type attribute = new JobHoldUntilSupported(str, null); else if (name.equals("job-hold-until-default")) attribute = new JobHoldUntilDefault(str, null); else if (name.equals("media-supported")) attribute = new MediaSupported(str, null); else if (name.equals("media-default")) attribute = new MediaDefault(str, null); else if (name.equals("job-sheets-default")) attribute = new JobSheetsDefault(str, null); else if (name.equals("job-sheets-supported")) attribute = new JobSheetsSupported(str, null); else if (name.equals("job-state-reasons")) // setOf attribute = parseJobStateReasons(value, lastAttribute); else if (name.equals("printer-state-reasons")) // setOf attribute = parsePrinterStateReasons(value, lastAttribute); else attribute = IppUtilities.getEnumAttribute(name, str); // all other stuff is either an enum or needs to be mapped to an // UnknownAttribute instance. Enums catched here are: // ipp-versions-supported, pdl-override-supported, compression-supported // uri-authentication-supported, uri-security-supported, sides-supported // sides-default, multiple-document-handling-supported, multiple-document-handling-default break; case IppValueTag.URI: try { uri = new URI(new String(value)); } catch (URISyntaxException e) { throw new IppException("Wrong URI syntax encountered.", e); } if (name.equals("job-uri")) attribute = new JobUri(uri); else if (name.equals("job-printer-uri")) attribute = new JobPrinterUri(uri); else if (name.equals("job-more-info")) attribute = new JobMoreInfo(uri); else if (name.equals("printer-uri-supported")) // setOf attribute = new PrinterUriSupported(uri); else if (name.equals("printer-more-info")) attribute = new PrinterMoreInfo(uri); else if (name.equals("printer-driver-installer")) attribute = new PrinterDriverInstaller(uri); else if (name.equals("printer-more-info-manufacturer")) attribute = new PrinterMoreInfoManufacturer(uri); break; case IppValueTag.URI_SCHEME: // only one uri-scheme exists - and its an enum if (name.equals("reference-uri-schemes-supported")) attribute = IppUtilities.getEnumAttribute(name, new String(value)); break; case IppValueTag.CHARSET: str = new String(value); if (name.equals("attributes-charset")) attribute = new AttributesCharset(str); else if (name.equals("charset-configured")) attribute = new CharsetConfigured(str); else if (name.equals("charset-supported")) // setOf attribute = new CharsetSupported(str); break; case IppValueTag.NATURAL_LANGUAGE: str = new String(value); if (name.equals("attributes-natural-language")) attribute = new AttributesNaturalLanguage(str); else if (name.equals("natural-language-configured")) attribute = new NaturalLanguageConfigured(str); else if (name.equals("generated-natural-language-supported")) // setOf attribute = new GeneratedNaturalLanguageSupported(str); break; case IppValueTag.MIME_MEDIA_TYPE: str = new String(value); if (name.equals("document-format-default")) attribute = new DocumentFormatDefault(str, null); else if (name.equals("document-format-supported")) // setOf attribute = new DocumentFormatSupported(str, null); else if (name.equals("document-format")) // setOf attribute = new DocumentFormat(str, null); break; default: throw new IppException("Unknown tag with value " + Integer.toHexString(tag) + " found."); } if (attribute == null) attribute = new UnknownAttribute(tag, name, value); addAttribute(attributes, attribute); lastAttribute = attribute; logger.log(Component.IPP, "Attribute: " + name + " Value: " + attribute.toString()); } } /** * Adds a new attribute to the given attribute group. If this is the fist * occurence of this attribute category a new set is created and associated * with its category as key. * @param attributeGroup * the attribute group * @param attribute * the attribute to add */ private void addAttribute(Map attributeGroup, Attribute attribute) { Class clazz = attribute.getCategory(); Set attributeValues = (Set) attributeGroup.get(clazz); if (attributeValues == null) // first attribute of this category { attributeValues = new HashSet(); attributeGroup.put(clazz, attributeValues); } attributeValues.add(attribute); } /** * Parses a name with or without language attribute value from the byte[] * and returns the result as an object[]. * @param value the byte[] * @param lastAttr the last attribute * @return The attribute. */ private PrinterStateReasons parsePrinterStateReasons(byte[] value, Attribute lastAttr) { String str = new String(value); PrinterStateReasons attribute; if (lastAttr instanceof PrinterStateReasons) attribute = (PrinterStateReasons) lastAttr; else attribute = new PrinterStateReasons(); // special case indicating no reasons if (str.equals("none")) return attribute; Severity severity = null; PrinterStateReason reason = null; if (str.endsWith(Severity.WARNING.toString())) severity = Severity.WARNING; else if (str.endsWith(Severity.REPORT.toString())) severity = Severity.REPORT; else if (str.endsWith(Severity.ERROR.toString())) severity = Severity.ERROR; if (severity != null) str = str.substring(0, str.lastIndexOf('-')); else // we must associate a severity severity = Severity.REPORT; reason = (PrinterStateReason) IppUtilities.getEnumAttribute("printer-state-reason", str); attribute.put(reason , severity); return attribute; } /** * Parses a name with or without language attribute value from the byte[] * and returns the result as an object[]. * @param value the byte[] * @param lastAttr the last attribute * @return The attribute. */ private JobStateReasons parseJobStateReasons(byte[] value, Attribute lastAttr) { String str = new String(value); JobStateReasons attribute; if (lastAttr instanceof JobStateReasons) attribute = (JobStateReasons) lastAttr; else attribute = new JobStateReasons(); // special case indicating no reasons if (str.equals("none")) return attribute; JobStateReason reason = (JobStateReason) IppUtilities.getEnumAttribute("job-state-reason", str); attribute.add(reason); return attribute; } /** * Parses a DateTime syntax attribute and returns the constructed Date * object. * <p> * The syntax value is defined as 11 octets follwing the DateAndTime format * of RFC 1903: * <ul> * <li>field | octets | contents | range</li> * <li>1 | 1-2 | year | 0..65536</li> * <li>2 | 3 | month | 1..12</li> * <li>3 | 4 | day | 1..31</li> * <li>4 | 5 | hour | 0..23</li> * <li>5 | 6 | minutes | 0..59</li> * <li>6 | 7 | seconds | 0..60 (use 60 for leap-second)</li> * <li>7 | 8 | deci-seconds | 0..9</li> * <li>8 | 9 | direction from UTC | '+' / '-'</li> * <li>9 | 10 | hours from UTC | 0..11</li> * <li>10 | 11 | minutes from UTC | 0..59</li> * </ul> * </p> * * @param value the byte[] * @return The date object. */ private Date parseDate(byte[] value) { short year = IppUtilities.convertToShort(value[0], value[1]); Calendar cal = Calendar.getInstance(); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, value[2]); cal.set(Calendar.DAY_OF_MONTH, value[3]); cal.set(Calendar.HOUR_OF_DAY, value[4]); cal.set(Calendar.MINUTE, value[5]); cal.set(Calendar.SECOND, value[6]); cal.set(Calendar.MILLISECOND, value[7] * 100); // deci-seconds // offset from timezone int offsetMilli = value[9] * 3600000; // hours to millis offsetMilli = offsetMilli + value[10] * 60000; // minutes to millis if (((char) value[8]) == '-') offsetMilli = offsetMilli * (-1); cal.set(Calendar.ZONE_OFFSET, offsetMilli); return cal.getTime(); } } /** * Logger for tracing - enable by passing * -Dgnu.classpath.debug.components=ipp to the vm. */ static final Logger logger = SystemLogger.SYSTEM; URI uri; short operation_id; short status_code; int request_id; List operationAttributes; List printerAttributes; List jobAttributes; List unsupportedAttributes; byte[] data; /** * Creates an <code>IppResponse</code> instance. * * @param uri the uri the request was directy to. * @param operation_id the operation id of the request. */ public IppResponse(URI uri, short operation_id) { this.uri = uri; this.operation_id = operation_id; operationAttributes = new ArrayList(); jobAttributes = new ArrayList(); printerAttributes = new ArrayList(); unsupportedAttributes = new ArrayList(); } /** * Sets the data received from the request sent. * * @param input the input stream received. * @throws IppException if parsing fails. */ protected void setResponseData(InputStream input) throws IppException { ResponseReader reader = new ResponseReader(); try { reader.parseResponse(input); } catch (IOException e) { throw new IppException( "Exception during response parsing caused by IOException", e); } } /** * Returns the uri of the original request. * @return The URI of the request. */ public URI getURI() { return uri; } /** * Returns the operation id of the original request. * @return The operation id of the request. */ public int getOperationID() { return operation_id; } /** * Returns the set of job attributes group maps. * There may occur more than one group of type job attribute in a response * because of e.g. multiple job or print service informations requested. * * @return The list of job attribute grou maps. */ public List getJobAttributes() { return jobAttributes; } /** * Returns the set of operation attributes group maps. * There may occur more than one group of type job attribute in a response * because of e.g. multiple job or print service informations requested. * * @return The list of operation attribute grou maps. */ public List getOperationAttributes() { return operationAttributes; } /** * Returns the set of printer attributes group maps. * There may occur more than one group of type job attribute in a response * because of e.g. multiple job or print service informations requested. * * @return The list of printer attribute grou maps. */ public List getPrinterAttributes() { return printerAttributes; } /** * Returns the ID of the initial request. * * @return The request ID. */ public int getRequestID() { return request_id; } /** * Returns the status code of the response. * Defined in {@link IppStatusCode}. * * @return The status code. */ public short getStatusCode() { return status_code; } /** * Returns the set of unsupported attributes group maps. * There may occur more than one group of type job attribute in a response * because of e.g. multiple job or print service informations requested. * * @return The list of unsupported attribute grou maps. */ public List getUnsupportedAttributes() { return unsupportedAttributes; } /** * Returns the data of the response. * * @return The data as byte[]. */ public byte[] getData() { return data; } }