/* 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<Class<? extends Attribute>, Set<Attribute>> 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<Class<? extends Attribute>, Set<Attribute>>();
              tag = parseAttributes(tmp, stream);
              operationAttributes.add(tmp);
              break;
            case IppDelimiterTag.JOB_ATTRIBUTES_TAG:
              tmp = new HashMap<Class<? extends Attribute>, Set<Attribute>>();
              tag = parseAttributes(tmp, stream);
              jobAttributes.add(tmp);
              break;
            case IppDelimiterTag.PRINTER_ATTRIBUTES_TAG:
              tmp = new HashMap<Class<? extends Attribute>, Set<Attribute>>();
              tag = parseAttributes(tmp, stream);
              printerAttributes.add(tmp);
              break;
            case IppDelimiterTag.UNSUPPORTED_ATTRIBUTES_TAG:
              System.out.println("Called");
              tmp = new HashMap<Class<? extends Attribute>, Set<Attribute>>();
              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<Class<? extends Attribute>, Set<Attribute>> 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:
              // 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 " + tag);
            case IppValueTag.NO_VALUE:
              attribute = null;

              break;
            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<Class<? extends Attribute>, Set<Attribute>> attributeGroup,
                              Attribute attribute)
    {
      Class<? extends Attribute> clazz = attribute.getCategory();
      Set<Attribute> attributeValues = attributeGroup.get(clazz);

      if (attributeValues == null) // first attribute of this category
        {
          attributeValues = new HashSet<Attribute>();
          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<Map<Class<? extends Attribute>, Set<Attribute>>> operationAttributes;
  List<Map<Class<? extends Attribute>, Set<Attribute>>> printerAttributes;
  List<Map<Class<? extends Attribute>, Set<Attribute>>> jobAttributes;
  List<Map<Class<? extends Attribute>, Set<Attribute>>> 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<Map<Class<? extends Attribute>, Set<Attribute>>>();
    jobAttributes =
      new ArrayList<Map<Class<? extends Attribute>, Set<Attribute>>>();
    printerAttributes =
      new ArrayList<Map<Class<? extends Attribute>, Set<Attribute>>>();
    unsupportedAttributes =
      new ArrayList<Map<Class<? extends Attribute>, Set<Attribute>>>();
  }

  /**
   * 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 group maps.
   */
  public List<Map<Class<? extends Attribute>, Set<Attribute>>> 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 group maps.
   */
  public List<Map<Class<? extends Attribute>, Set<Attribute>>> 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 group maps.
   */
  public List<Map<Class<? extends Attribute>, Set<Attribute>>> 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 group maps.
   */
  public List<Map<Class<? extends Attribute>, Set<Attribute>>> getUnsupportedAttributes()
  {
    return unsupportedAttributes;
  }

  /**
   * Returns the data of the response.
   *
   * @return The data as byte[].
   */
  public byte[] getData()
  {
    return data;
  }

}
