/*
 * Copyright (c) 2000, 2008, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.print;

import java.net.URI;
import java.net.URL;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.util.Vector;

import javax.print.CancelablePrintJob;
import javax.print.Doc;
import javax.print.DocFlavor;
import javax.print.DocPrintJob;
import javax.print.PrintService;
import javax.print.PrintException;
import javax.print.event.PrintJobEvent;
import javax.print.event.PrintJobListener;
import javax.print.event.PrintJobAttributeListener;

import javax.print.attribute.Attribute;
import javax.print.attribute.AttributeSet;
import javax.print.attribute.AttributeSetUtilities;
import javax.print.attribute.DocAttributeSet;
import javax.print.attribute.HashPrintJobAttributeSet;
import javax.print.attribute.HashPrintRequestAttributeSet;
import javax.print.attribute.PrintJobAttribute;
import javax.print.attribute.PrintJobAttributeSet;
import javax.print.attribute.PrintRequestAttribute;
import javax.print.attribute.PrintRequestAttributeSet;
import javax.print.attribute.standard.Copies;
import javax.print.attribute.standard.Destination;
import javax.print.attribute.standard.DocumentName;
import javax.print.attribute.standard.Fidelity;
import javax.print.attribute.standard.JobName;
import javax.print.attribute.standard.JobOriginatingUserName;
import javax.print.attribute.standard.JobSheets;
import javax.print.attribute.standard.Media;
import javax.print.attribute.standard.MediaSize;
import javax.print.attribute.standard.MediaSizeName;
import javax.print.attribute.standard.OrientationRequested;
import javax.print.attribute.standard.RequestingUserName;
import javax.print.attribute.standard.NumberUp;
import javax.print.attribute.standard.Sides;
import javax.print.attribute.standard.PrinterIsAcceptingJobs;

import java.awt.print.*;



public class UnixPrintJob implements CancelablePrintJob {
    private static String debugPrefix = "UnixPrintJob>> ";

    transient private Vector jobListeners;
    transient private Vector attrListeners;
    transient private Vector listenedAttributeSets;

    private PrintService service;
    private boolean fidelity;
    private boolean printing = false;
    private boolean printReturned = false;
    private PrintRequestAttributeSet reqAttrSet = null;
    private PrintJobAttributeSet jobAttrSet = null;
    private PrinterJob job;
    private Doc doc;
    /* these variables used globally to store reference to the print
     * data retrieved as a stream. On completion these are always closed
     * if non-null.
     */
    private InputStream instream = null;
    private Reader reader = null;

    /* default values overridden by those extracted from the attributes */
    private String jobName = "Java Printing";
    private int copies = 1;
    private MediaSizeName mediaName = MediaSizeName.NA_LETTER;
    private MediaSize     mediaSize = MediaSize.NA.LETTER;
    private CustomMediaTray     customTray = null;
    private OrientationRequested orient = OrientationRequested.PORTRAIT;
    private NumberUp nUp = null;
    private Sides sides = null;

    UnixPrintJob(PrintService service) {
        this.service = service;
        mDestination = service.getName();
        mDestType = UnixPrintJob.DESTPRINTER;
    }

    public PrintService getPrintService() {
        return service;
    }

    public PrintJobAttributeSet getAttributes() {
        synchronized (this) {
            if (jobAttrSet == null) {
                /* just return an empty set until the job is submitted */
                PrintJobAttributeSet jobSet = new HashPrintJobAttributeSet();
                return AttributeSetUtilities.unmodifiableView(jobSet);
            } else {
              return jobAttrSet;
            }
        }
    }

    public void addPrintJobListener(PrintJobListener listener) {
        synchronized (this) {
            if (listener == null) {
                return;
            }
            if (jobListeners == null) {
                jobListeners = new Vector();
            }
            jobListeners.add(listener);
        }
    }

    public void removePrintJobListener(PrintJobListener listener) {
        synchronized (this) {
            if (listener == null || jobListeners == null ) {
                return;
            }
            jobListeners.remove(listener);
            if (jobListeners.isEmpty()) {
                jobListeners = null;
            }
        }
    }


    /* Closes any stream already retrieved for the data.
     * We want to avoid unnecessarily asking the Doc to create a stream only
     * to get a reference in order to close it because the job failed.
     * If the representation class is itself a "stream", this
     * closes that stream too.
     */
    private void closeDataStreams() {

        if (doc == null) {
            return;
        }

        Object data = null;

        try {
            data = doc.getPrintData();
        } catch (IOException e) {
            return;
        }

        if (instream != null) {
            try {
                instream.close();
            } catch (IOException e) {
            } finally {
                instream = null;
            }
        }
        else if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
            } finally {
                reader = null;
            }
        }
        else if (data instanceof InputStream) {
            try {
                ((InputStream)data).close();
            } catch (IOException e) {
            }
        }
        else if (data instanceof Reader) {
            try {
                ((Reader)data).close();
            } catch (IOException e) {
            }
        }
    }

    private void notifyEvent(int reason) {

        /* since this method should always get called, here's where
         * we will perform the clean up of any data stream supplied.
         */
        switch (reason) {
            case PrintJobEvent.DATA_TRANSFER_COMPLETE:
            case PrintJobEvent.JOB_CANCELED :
            case PrintJobEvent.JOB_FAILED :
            case PrintJobEvent.NO_MORE_EVENTS :
            case PrintJobEvent.JOB_COMPLETE :
                closeDataStreams();
        }

        synchronized (this) {
            if (jobListeners != null) {
                PrintJobListener listener;
                PrintJobEvent event = new PrintJobEvent(this, reason);
                for (int i = 0; i < jobListeners.size(); i++) {
                    listener = (PrintJobListener)(jobListeners.elementAt(i));
                    switch (reason) {

                        case PrintJobEvent.JOB_CANCELED :
                            listener.printJobCanceled(event);
                            break;

                        case PrintJobEvent.JOB_FAILED :
                            listener.printJobFailed(event);
                            break;

                        case PrintJobEvent.DATA_TRANSFER_COMPLETE :
                            listener.printDataTransferCompleted(event);
                            break;

                        case PrintJobEvent.NO_MORE_EVENTS :
                            listener.printJobNoMoreEvents(event);
                            break;

                        default:
                            break;
                    }
                }
            }
       }
    }

    public void addPrintJobAttributeListener(
                                  PrintJobAttributeListener listener,
                                  PrintJobAttributeSet attributes) {
        synchronized (this) {
            if (listener == null) {
                return;
            }
            if (attrListeners == null) {
                attrListeners = new Vector();
                listenedAttributeSets = new Vector();
            }
            attrListeners.add(listener);
            if (attributes == null) {
                attributes = new HashPrintJobAttributeSet();
            }
            listenedAttributeSets.add(attributes);
        }
    }

    public void removePrintJobAttributeListener(
                                        PrintJobAttributeListener listener) {
        synchronized (this) {
            if (listener == null || attrListeners == null ) {
                return;
            }
            int index = attrListeners.indexOf(listener);
            if (index == -1) {
                return;
            } else {
                attrListeners.remove(index);
                listenedAttributeSets.remove(index);
                if (attrListeners.isEmpty()) {
                    attrListeners = null;
                    listenedAttributeSets = null;
                }
            }
        }
    }

    public void print(Doc doc, PrintRequestAttributeSet attributes)
        throws PrintException {

        synchronized (this) {
            if (printing) {
                throw new PrintException("already printing");
            } else {
                printing = true;
            }
        }

        if ((PrinterIsAcceptingJobs)(service.getAttribute(
                         PrinterIsAcceptingJobs.class)) ==
                         PrinterIsAcceptingJobs.NOT_ACCEPTING_JOBS) {
            throw new PrintException("Printer is not accepting job.");
        }

        this.doc = doc;
        /* check if the parameters are valid before doing much processing */
        DocFlavor flavor = doc.getDocFlavor();

        Object data;

        try {
            data = doc.getPrintData();
        } catch (IOException e) {
            notifyEvent(PrintJobEvent.JOB_FAILED);
            throw new PrintException("can't get print data: " + e.toString());
        }

        if (flavor == null || (!service.isDocFlavorSupported(flavor))) {
            notifyEvent(PrintJobEvent.JOB_FAILED);
            throw new PrintJobFlavorException("invalid flavor", flavor);
        }

        initializeAttributeSets(doc, attributes);

        getAttributeValues(flavor);

        // set up mOptions
        if ((service instanceof IPPPrintService) &&
            CUPSPrinter.isCupsRunning()) {

             IPPPrintService.debug_println(debugPrefix+
                        "instanceof IPPPrintService");

             if (mediaName != null) {
                 CustomMediaSizeName customMedia =
                     ((IPPPrintService)service).findCustomMedia(mediaName);
                 if (customMedia != null) {
                     mOptions = " media="+ customMedia.getChoiceName();
                 }
             }

             if (customTray != null &&
                 customTray instanceof CustomMediaTray) {
                 String choice = customTray.getChoiceName();
                 if (choice != null) {
                     mOptions += " media="+choice;
                 }
             }

             if (nUp != null) {
                 mOptions += " number-up="+nUp.getValue();
             }

             if (orient != OrientationRequested.PORTRAIT &&
                 (flavor != null) &&
                 !flavor.equals(DocFlavor.SERVICE_FORMATTED.PAGEABLE)) {
                 mOptions += " orientation-requested="+orient.getValue();
             }

             if (sides != null) {
                 mOptions += " sides="+sides;
             }

        }

        IPPPrintService.debug_println(debugPrefix+"mOptions "+mOptions);
        String repClassName = flavor.getRepresentationClassName();
        String val = flavor.getParameter("charset");
        String encoding = "us-ascii";
        if (val != null && !val.equals("")) {
            encoding = val;
        }

        if (flavor.equals(DocFlavor.INPUT_STREAM.GIF) ||
            flavor.equals(DocFlavor.INPUT_STREAM.JPEG) ||
            flavor.equals(DocFlavor.INPUT_STREAM.PNG) ||
            flavor.equals(DocFlavor.BYTE_ARRAY.GIF) ||
            flavor.equals(DocFlavor.BYTE_ARRAY.JPEG) ||
            flavor.equals(DocFlavor.BYTE_ARRAY.PNG)) {
            try {
                instream = doc.getStreamForBytes();
                if (instream == null) {
                    notifyEvent(PrintJobEvent.JOB_FAILED);
                    throw new PrintException("No stream for data");
                }
                if (!(service instanceof IPPPrintService &&
                    ((IPPPrintService)service).isIPPSupportedImages(
                                                flavor.getMimeType()))) {
                    printableJob(new ImagePrinter(instream));
                    ((UnixPrintService)service).wakeNotifier();
                    return;
                }
            } catch (ClassCastException cce) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(cce);
            } catch (IOException ioe) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(ioe);
            }
        } else if (flavor.equals(DocFlavor.URL.GIF) ||
                   flavor.equals(DocFlavor.URL.JPEG) ||
                   flavor.equals(DocFlavor.URL.PNG)) {
            try {
                URL url = (URL)data;
                if ((service instanceof IPPPrintService) &&
                    ((IPPPrintService)service).isIPPSupportedImages(
                                               flavor.getMimeType())) {
                    instream = url.openStream();
                } else {
                    printableJob(new ImagePrinter(url));
                    ((UnixPrintService)service).wakeNotifier();
                    return;
                }
            } catch (ClassCastException cce) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(cce);
            } catch (IOException e) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(e.toString());
            }
        } else if (flavor.equals(DocFlavor.CHAR_ARRAY.TEXT_PLAIN) ||
                   flavor.equals(DocFlavor.READER.TEXT_PLAIN) ||
                   flavor.equals(DocFlavor.STRING.TEXT_PLAIN)) {
            try {
                reader = doc.getReaderForText();
                if (reader == null) {
                   notifyEvent(PrintJobEvent.JOB_FAILED);
                   throw new PrintException("No reader for data");
                }
            } catch (IOException ioe) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(ioe.toString());
            }
        } else if (repClassName.equals("[B") ||
                   repClassName.equals("java.io.InputStream")) {
            try {
                instream = doc.getStreamForBytes();
                if (instream == null) {
                    notifyEvent(PrintJobEvent.JOB_FAILED);
                    throw new PrintException("No stream for data");
                }
            } catch (IOException ioe) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(ioe.toString());
            }
        } else if  (repClassName.equals("java.net.URL")) {
            /*
             * This extracts the data from the URL and passes it the content
             * directly to the print service as a file.
             * This is appropriate for the current implementation where lp or
             * lpr is always used to spool the data. We expect to revise the
             * implementation to provide more complete IPP support (ie not just
             * CUPS) and at that time the job will be spooled via IPP
             * and the URL
             * itself should be sent to the IPP print service not the content.
             */
            URL url = (URL)data;
            try {
                instream = url.openStream();
            } catch (IOException e) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(e.toString());
            }
        } else if (repClassName.equals("java.awt.print.Pageable")) {
            try {
                pageableJob((Pageable)doc.getPrintData());
                if (service instanceof IPPPrintService) {
                    ((IPPPrintService)service).wakeNotifier();
                } else {
                    ((UnixPrintService)service).wakeNotifier();
                }
                return;
            } catch (ClassCastException cce) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(cce);
            } catch (IOException ioe) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(ioe);
            }
        } else if (repClassName.equals("java.awt.print.Printable")) {
            try {
                printableJob((Printable)doc.getPrintData());
                if (service instanceof IPPPrintService) {
                    ((IPPPrintService)service).wakeNotifier();
                } else {
                    ((UnixPrintService)service).wakeNotifier();
                }
                return;
            } catch (ClassCastException cce) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(cce);
            } catch (IOException ioe) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException(ioe);
            }
        } else {
            notifyEvent(PrintJobEvent.JOB_FAILED);
            throw new PrintException("unrecognized class: "+repClassName);
        }

        // now spool the print data.
        PrinterOpener po = new PrinterOpener();
        java.security.AccessController.doPrivileged(po);
        if (po.pex != null) {
            throw po.pex;
        }
        OutputStream output = po.result;

        /* There are three cases:
         * 1) Text data from a Reader, just pass through.
         * 2) Text data from an input stream which we must read using the
         *    correct encoding
         * 3) Raw byte data from an InputStream we don't interpret as text,
         *    just pass through: eg postscript.
         */

        BufferedWriter bw = null;
        if ((instream == null && reader != null)) {
            BufferedReader br = new BufferedReader(reader);
            OutputStreamWriter osw = new OutputStreamWriter(output);
            bw = new BufferedWriter(osw);
            char []buffer = new char[1024];
            int cread;

            try {
                while ((cread = br.read(buffer, 0, buffer.length)) >=0) {
                    bw.write(buffer, 0, cread);
                }
                br.close();
                bw.flush();
                bw.close();
            } catch (IOException e) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException (e);
            }
        } else if (instream != null &&
                   flavor.getMediaType().equalsIgnoreCase("text")) {
            try {

                InputStreamReader isr = new InputStreamReader(instream,
                                                              encoding);
                BufferedReader br = new BufferedReader(isr);
                OutputStreamWriter osw = new OutputStreamWriter(output);
                bw = new BufferedWriter(osw);
                char []buffer = new char[1024];
                int cread;

                while ((cread = br.read(buffer, 0, buffer.length)) >=0) {
                    bw.write(buffer, 0, cread);
                }
                bw.flush();
            } catch (IOException e) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException (e);
            } finally {
                try {
                    if (bw != null) {
                        bw.close();
                    }
                } catch (IOException e) {
                }
            }
        } else if (instream != null) {
            BufferedInputStream bin = new BufferedInputStream(instream);
            BufferedOutputStream bout = new BufferedOutputStream(output);
            byte[] buffer = new byte[1024];
            int bread = 0;

            try {
                while ((bread = bin.read(buffer)) >= 0) {
                    bout.write(buffer, 0, bread);
                }
                bin.close();
                bout.flush();
                bout.close();
            } catch (IOException e) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                throw new PrintException (e);
            }
        }
        notifyEvent(PrintJobEvent.DATA_TRANSFER_COMPLETE);

        if (mDestType == UnixPrintJob.DESTPRINTER) {
            PrinterSpooler spooler = new PrinterSpooler();
            java.security.AccessController.doPrivileged(spooler);
            if (spooler.pex != null) {
                throw spooler.pex;
            }
        }
        notifyEvent(PrintJobEvent.NO_MORE_EVENTS);
        if (service instanceof IPPPrintService) {
            ((IPPPrintService)service).wakeNotifier();
        } else {
            ((UnixPrintService)service).wakeNotifier();
        }
    }

    public void printableJob(Printable printable) throws PrintException {
        try {
            synchronized(this) {
                if (job != null) { // shouldn't happen
                    throw new PrintException("already printing");
                } else {
                    job = new PSPrinterJob();
                }
            }
            job.setPrintService(getPrintService());
            job.setCopies(copies);
            job.setJobName(jobName);
            PageFormat pf = new PageFormat();
            if (mediaSize != null) {
                Paper p = new Paper();
                p.setSize(mediaSize.getX(MediaSize.INCH)*72.0,
                          mediaSize.getY(MediaSize.INCH)*72.0);
                p.setImageableArea(72.0, 72.0, p.getWidth()-144.0,
                                   p.getHeight()-144.0);
                pf.setPaper(p);
            }
            if (orient == OrientationRequested.REVERSE_LANDSCAPE) {
                pf.setOrientation(PageFormat.REVERSE_LANDSCAPE);
            } else if (orient == OrientationRequested.LANDSCAPE) {
                pf.setOrientation(PageFormat.LANDSCAPE);
            }
            job.setPrintable(printable, pf);
            job.print(reqAttrSet);
            notifyEvent(PrintJobEvent.DATA_TRANSFER_COMPLETE);
            return;
        } catch (PrinterException pe) {
            notifyEvent(PrintJobEvent.JOB_FAILED);
            throw new PrintException(pe);
        } finally {
            printReturned = true;
            notifyEvent(PrintJobEvent.NO_MORE_EVENTS);
        }
    }

    public void pageableJob(Pageable pageable) throws PrintException {
        try {
            synchronized(this) {
                if (job != null) { // shouldn't happen
                    throw new PrintException("already printing");
                } else {
                    job = new PSPrinterJob();
                }
            }
            job.setPrintService(getPrintService());
            job.setCopies(copies);
            job.setJobName(jobName);
            job.setPageable(pageable);
            job.print(reqAttrSet);
            notifyEvent(PrintJobEvent.DATA_TRANSFER_COMPLETE);
            return;
        } catch (PrinterException pe) {
            notifyEvent(PrintJobEvent.JOB_FAILED);
            throw new PrintException(pe);
        } finally {
            printReturned = true;
            notifyEvent(PrintJobEvent.NO_MORE_EVENTS);
        }
    }
    /* There's some inefficiency here as the job set is created even though
     * it may never be requested.
     */
    private synchronized void
        initializeAttributeSets(Doc doc, PrintRequestAttributeSet reqSet) {

        reqAttrSet = new HashPrintRequestAttributeSet();
        jobAttrSet = new HashPrintJobAttributeSet();

        Attribute[] attrs;
        if (reqSet != null) {
            reqAttrSet.addAll(reqSet);
            attrs = reqSet.toArray();
            for (int i=0; i<attrs.length; i++) {
                if (attrs[i] instanceof PrintJobAttribute) {
                    jobAttrSet.add(attrs[i]);
                }
            }
        }

        DocAttributeSet docSet = doc.getAttributes();
        if (docSet != null) {
            attrs = docSet.toArray();
            for (int i=0; i<attrs.length; i++) {
                if (attrs[i] instanceof PrintRequestAttribute) {
                    reqAttrSet.add(attrs[i]);
                }
                if (attrs[i] instanceof PrintJobAttribute) {
                    jobAttrSet.add(attrs[i]);
                }
            }
        }

        /* add the user name to the job */
        String userName = "";
        try {
          userName = System.getProperty("user.name");
        } catch (SecurityException se) {
        }

        if (userName == null || userName.equals("")) {
            RequestingUserName ruName =
                (RequestingUserName)reqSet.get(RequestingUserName.class);
            if (ruName != null) {
                jobAttrSet.add(
                    new JobOriginatingUserName(ruName.getValue(),
                                               ruName.getLocale()));
            } else {
                jobAttrSet.add(new JobOriginatingUserName("", null));
            }
        } else {
            jobAttrSet.add(new JobOriginatingUserName(userName, null));
        }

        /* if no job name supplied use doc name (if supplied), if none and
         * its a URL use that, else finally anything .. */
        if (jobAttrSet.get(JobName.class) == null) {
            JobName jobName;
            if (docSet != null && docSet.get(DocumentName.class) != null) {
                DocumentName docName =
                    (DocumentName)docSet.get(DocumentName.class);
                jobName = new JobName(docName.getValue(), docName.getLocale());
                jobAttrSet.add(jobName);
            } else {
                String str = "JPS Job:" + doc;
                try {
                    Object printData = doc.getPrintData();
                    if (printData instanceof URL) {
                        str = ((URL)(doc.getPrintData())).toString();
                    }
                } catch (IOException e) {
                }
                jobName = new JobName(str, null);
                jobAttrSet.add(jobName);
            }
        }

        jobAttrSet = AttributeSetUtilities.unmodifiableView(jobAttrSet);
    }

    private void getAttributeValues(DocFlavor flavor) throws PrintException {
        Attribute attr;
        Class category;

        if (reqAttrSet.get(Fidelity.class) == Fidelity.FIDELITY_TRUE) {
            fidelity = true;
        } else {
            fidelity = false;
        }

        Attribute []attrs = reqAttrSet.toArray();
        for (int i=0; i<attrs.length; i++) {
            attr = attrs[i];
            category = attr.getCategory();
            if (fidelity == true) {
                if (!service.isAttributeCategorySupported(category)) {
                    notifyEvent(PrintJobEvent.JOB_FAILED);
                    throw new PrintJobAttributeException(
                        "unsupported category: " + category, category, null);
                } else if
                    (!service.isAttributeValueSupported(attr, flavor, null)) {
                    notifyEvent(PrintJobEvent.JOB_FAILED);
                    throw new PrintJobAttributeException(
                        "unsupported attribute: " + attr, null, attr);
                }
            }
            if (category == Destination.class) {
                URI uri = ((Destination)attr).getURI();
                if (!"file".equals(uri.getScheme())) {
                    notifyEvent(PrintJobEvent.JOB_FAILED);
                    throw new PrintException("Not a file: URI");
                } else {
                    try {
                        mDestType = DESTFILE;
                        mDestination = (new File(uri)).getPath();
                    } catch (Exception e) {
                        throw new PrintException(e);
                    }
                    // check write access
                    SecurityManager security = System.getSecurityManager();
                    if (security != null) {
                      try {
                        security.checkWrite(mDestination);
                      } catch (SecurityException se) {
                        notifyEvent(PrintJobEvent.JOB_FAILED);
                        throw new PrintException(se);
                      }
                    }
                }
            } else if (category == JobSheets.class) {
                if ((JobSheets)attr == JobSheets.NONE) {
                   mNoJobSheet = true;
                }
            } else if (category == JobName.class) {
                jobName = ((JobName)attr).getValue();
            } else if (category == Copies.class) {
                copies = ((Copies)attr).getValue();
            } else if (category == Media.class) {
                if (attr instanceof MediaSizeName) {
                    mediaName = (MediaSizeName)attr;
                    IPPPrintService.debug_println(debugPrefix+
                                                  "mediaName "+mediaName);
                if (!service.isAttributeValueSupported(attr, null, null)) {
                    mediaSize = MediaSize.getMediaSizeForName(mediaName);
                }
              } else if (attr instanceof CustomMediaTray) {
                  customTray = (CustomMediaTray)attr;
              }
            } else if (category == OrientationRequested.class) {
                orient = (OrientationRequested)attr;
            } else if (category == NumberUp.class) {
                nUp = (NumberUp)attr;
            } else if (category == Sides.class) {
                sides = (Sides)attr;
            }
        }
    }

    private String[] printExecCmd(String printer, String options,
                                 boolean noJobSheet,
                                 String banner, int copies, String spoolFile) {
        int PRINTER = 0x1;
        int OPTIONS = 0x2;
        int BANNER  = 0x4;
        int COPIES  = 0x8;
        int NOSHEET  = 0x10;
        int pFlags = 0;
        String execCmd[];
        int ncomps = 2; // minimum number of print args
        int n = 0;

        // conveniently "lp" is the default destination for both lp and lpr.
        if (printer != null && !printer.equals("") && !printer.equals("lp")) {
            pFlags |= PRINTER;
            ncomps+=1;
        }
        if (options != null && !options.equals("")) {
            pFlags |= OPTIONS;
            ncomps+=1;
        }
        if (banner != null && !banner.equals("")) {
            pFlags |= BANNER;
            ncomps+=1;
        }
        if (copies > 1) {
            pFlags |= COPIES;
            ncomps+=1;
        }
        if (noJobSheet) {
            pFlags |= NOSHEET;
            ncomps+=1;
        }
        if (UnixPrintServiceLookup.osname.equals("SunOS")) {
            ncomps+=1; // lp uses 1 more arg than lpr (make a copy)
            execCmd = new String[ncomps];
            execCmd[n++] = "/usr/bin/lp";
            execCmd[n++] = "-c";           // make a copy of the spool file
            if ((pFlags & PRINTER) != 0) {
                execCmd[n++] = "-d" + printer;
            }
            if ((pFlags & BANNER) != 0) {
                String quoteChar = "\"";
                execCmd[n++] = "-t "  + quoteChar+banner+quoteChar;
            }
            if ((pFlags & COPIES) != 0) {
                execCmd[n++] = "-n " + copies;
            }
            if ((pFlags & NOSHEET) != 0) {
                execCmd[n++] = "-o nobanner";
            }
            if ((pFlags & OPTIONS) != 0) {
                execCmd[n++] = "-o " + options;
            }
        } else {
            execCmd = new String[ncomps];
            execCmd[n++] = "/usr/bin/lpr";
            if ((pFlags & PRINTER) != 0) {
                execCmd[n++] = "-P" + printer;
            }
            if ((pFlags & BANNER) != 0) {
                execCmd[n++] = "-J "  + banner;
            }
            if ((pFlags & COPIES) != 0) {
                execCmd[n++] = "-#" + copies;
            }
            if ((pFlags & NOSHEET) != 0) {
                execCmd[n++] = "-h";
            }
            if ((pFlags & OPTIONS) != 0) {
                execCmd[n++] = "-o" + options;
            }
        }
        execCmd[n++] = spoolFile;
        if (IPPPrintService.debugPrint) {
            System.out.println("UnixPrintJob>> execCmd");
            for (int i=0; i<execCmd.length; i++) {
                System.out.print(" "+execCmd[i]);
            }
            System.out.println();
        }
        return execCmd;
    }

    private static int DESTPRINTER = 1;
    private static int DESTFILE = 2;
    private int mDestType = DESTPRINTER;

    private File spoolFile;
    private String mDestination, mOptions="";
    private boolean mNoJobSheet = false;

    // Inner class to run "privileged" to open the printer output stream.

    private class PrinterOpener implements java.security.PrivilegedAction {
        PrintException pex;
        OutputStream result;

        public Object run() {
            try {
                if (mDestType == UnixPrintJob.DESTFILE) {
                    spoolFile = new File(mDestination);
                } else {
                    /* Write to a temporary file which will be spooled to
                     * the printer then deleted. In the case that the file
                     * is not removed for some reason, request that it is
                     * removed when the VM exits.
                     */
                    spoolFile = Files.createTempFile("javaprint", "").toFile();
                    spoolFile.deleteOnExit();
                }
                result = new FileOutputStream(spoolFile);
                return result;
            } catch (IOException ex) {
                // If there is an IOError we subvert it to a PrinterException.
                notifyEvent(PrintJobEvent.JOB_FAILED);
                pex = new PrintException(ex);
            }
            return null;
        }
    }

    // Inner class to run "privileged" to invoke the system print command

    private class PrinterSpooler implements java.security.PrivilegedAction {
        PrintException pex;

        private void handleProcessFailure(final Process failedProcess,
                final String[] execCmd, final int result) throws IOException {
            try (StringWriter sw = new StringWriter();
                    PrintWriter pw = new PrintWriter(sw)) {
                pw.append("error=").append(Integer.toString(result));
                pw.append(" running:");
                for (String arg: execCmd) {
                    pw.append(" '").append(arg).append("'");
                }
                try (InputStream is = failedProcess.getErrorStream();
                        InputStreamReader isr = new InputStreamReader(is);
                        BufferedReader br = new BufferedReader(isr)) {
                    while (br.ready()) {
                        pw.println();
                        pw.append("\t\t").append(br.readLine());
                    }
                } finally {
                    pw.flush();
                    throw new IOException(sw.toString());
                }
            }
        }

        public Object run() {
            if (spoolFile == null || !spoolFile.exists()) {
               pex = new PrintException("No spool file");
               notifyEvent(PrintJobEvent.JOB_FAILED);
               return null;
            }
            try {
                /**
                 * Spool to the printer.
                 */
                String fileName = spoolFile.getAbsolutePath();
                String execCmd[] = printExecCmd(mDestination, mOptions,
                               mNoJobSheet, jobName, copies, fileName);

                Process process = Runtime.getRuntime().exec(execCmd);
                process.waitFor();
                final int result = process.exitValue();
                if (0 != result) {
                    handleProcessFailure(process, execCmd, result);
                }
                notifyEvent(PrintJobEvent.DATA_TRANSFER_COMPLETE);
            } catch (IOException ex) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                // REMIND : 2d printing throws PrinterException
                pex = new PrintException(ex);
            } catch (InterruptedException ie) {
                notifyEvent(PrintJobEvent.JOB_FAILED);
                pex = new PrintException(ie);
            } finally {
                spoolFile.delete();
                notifyEvent(PrintJobEvent.NO_MORE_EVENTS);
            }
            return null;
        }
    }

    public void cancel() throws PrintException {
        synchronized (this) {
            if (!printing) {
                throw new PrintException("Job is not yet submitted.");
            } else if (job != null && !printReturned) {
                job.cancel();
                notifyEvent(PrintJobEvent.JOB_CANCELED);
                return;
            } else {
                throw new PrintException("Job could not be cancelled.");
            }
        }
    }
}