io.andyc.papercut.api.PrintApi.java Source code

Java tutorial

Introduction

Here is the source code for io.andyc.papercut.api.PrintApi.java

Source

package io.andyc.papercut.api;

/********************************************************************************
 * PapercutHtmlApi
 *
 * Copyright 2016 Andy Chrzaszcz https://github.com/andy9775
 *
 * Licensed 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.
 ********************************************************************************/

import com.fasterxml.jackson.databind.ObjectMapper;
import io.andyc.papercut.api.Exceptions.ExpiredSessionException;
import io.andyc.papercut.api.Exceptions.NoStatusURLSetException;
import io.andyc.papercut.api.Exceptions.PrintingException;
import io.andyc.papercut.api.SessionManagement.SessionFactory;
import io.andyc.papercut.api.lib.ContentTypes;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.HttpMultipartMode;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.UnsupportedMimeTypeException;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PrintApi {

    //======================= regex ==================================
    /*
    regex pattern used to extract the upload form URL from the upload from
    HTML
    e.g. will return
        var uploadFormSubmitURL = '/upload/123';
     from javascript inside of HTML
     */
    private static Pattern jsPattern = Pattern
            .compile("((var)(\\s*)(uploadFormSubmitURL)(\\s*)?(=\\s*))(.*)((\\s*)?(;))");
    /*
    regex pattern used to extract the upload from url from the URL
    JavaScript declaration
    e.g. for
    var uploadFormSubmitURL = '/upload/123';
    identifies '/upload/123'
    */
    private static Pattern urlPattern = Pattern.compile("\\'.*\\'");

    /*
    regex pattern used to extract the URL path name used to check the file
    upload status
    */
    private static Pattern statusPathPattern = Pattern
            .compile("((var)(\\s*)" + "(webPrintUID)(\\s*)?(=)(\\s*)('.*')(\\s*)?(;))");

    //==========================================================================

    /**
     * Get the different printers that we can print to and return an array of
     * the different printer types
     *
     * @return {PrinterOption[]} - An array of print options
     */
    public static ArrayList<PrinterOption> getPrinterOptions(SessionFactory.Session session)
            throws IOException, ExpiredSessionException, PrintingException {
        Elements inputValues = PrintApi.buildConnection(session, "?service=action/1/UserWebPrint/0/%24ActionLink")
                .execute().parse().select("form").select("div.wizard-body").select("table.results").select("label");

        ArrayList<PrinterOption> result = new ArrayList<>();
        for (Element element : inputValues) {
            String name = element.select("input").attr("name");
            String value = element.select("input").attr("value");
            if (name.isEmpty() || value.isEmpty()) {
                throw new PrintingException("Cannot parse name and/or value of printing options");
            }
            result.add(new PrinterOption(name, value, element.text()));

        }

        if (result.size() == 0) {
            throw new PrintingException("Cannot parse printer options");
        }
        return result;
    }

    /**
     * Prints job
     *
     * Sets the printer type, the number of copies and uploads the file
     * finalizing with submitting the file upload form
     *
     * @param printJob {PrintJob} - the file to print and settings
     *
     * @return {PrintJob} - the print job that was passed in, containing the
     * file upload status and the upload URL which can be used to check upload
     * status
     *
     * @throws IOException             - error reading file
     * @throws ExpiredSessionException - if the session is expired. Create a new
     *                                 one and try again
     * @throws PrintingException       - if an error occurred during printing
     */
    public static PrintJob print(PrintJob printJob) throws IOException, ExpiredSessionException, PrintingException {
        if (printJob.getSession().isExpired()) {
            throw new ExpiredSessionException();
        }
        Document numberOfCopiesElement = PrintApi.setPrinterType(printJob);
        Document uploadFileElement = PrintApi.setNumberOfCopies(printJob, numberOfCopiesElement);
        if (PrintApi.uploadFile(printJob, uploadFileElement)) {
            Document finalHtml = PrintApi.finalizeFileUpload(printJob);
            if (PrintApi.didUpload(finalHtml)) {
                printJob.setPrintJobStatus(PrintJobStatus.SUCCESSFUL);
                printJob.setStatusCheckURL(
                        PrintApi.getStatusCheckURL(finalHtml, printJob.getSession().getDomain()));
            } else {
                printJob.setPrintJobStatus(PrintJobStatus.FAILED);
            }
        }
        return printJob;
    }

    /**
     * Checks whether the print service is available
     *
     * @param session {Session} - the current session
     *
     * @return {boolean} - whether or no the print service is available
     *
     * @throws IOException
     */
    public static boolean canPrint(SessionFactory.Session session) throws IOException {
        return !PrintApi.buildConnection(session, "?service=action/1/UserWebPrint/0/$ActionLink").execute().parse()
                .select("div#main").toString().contains("Web Print is temporarily " + "unavailable");
    }

    /**
     * Queries the remote Papercut server to get the status of the file
     *
     * e.g.
     *
     * {"status":{"code":"hold-release","complete":false,"text":"Held in a
     * queue","formatted":"Held in a queue"},"documentName":"test.rtf",
     * "printer":"Floor1 (Full Colour)","pages":1,"cost":"$0.10"}
     *
     * @param printJob {PrintJob} - the file to check the status on
     *
     * @return {String} - JSON String which includes file metadata
     *
     * @throws NoStatusURLSetException
     * @throws IOException
     */
    public static PrintStatusData getFileStatus(PrintJob printJob) throws NoStatusURLSetException, IOException {
        if (Objects.equals(printJob.getStatusCheckURL(), "") || printJob.getStatusCheckURL() == null) {
            throw new NoStatusURLSetException();
        }

        // from: http://stackoverflow.com/a/29889139/2605221
        StringBuilder stringBuffer = new StringBuilder("");
        URL url = new URL(printJob.getStatusCheckURL());
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setRequestProperty("User-Agent", "");
        connection.setRequestMethod("GET");
        connection.setRequestProperty("Accept", "application/json");
        connection.setDoInput(true);
        connection.connect();

        InputStream inputStream = connection.getInputStream();

        BufferedReader rd = new BufferedReader(new InputStreamReader(inputStream));
        String line = "";
        while ((line = rd.readLine()) != null) {
            stringBuffer.append(line);
        }
        return new ObjectMapper().readValue(stringBuffer.toString(), PrintStatusData.class);
    }

    //======================= inner classes ==================================

    /**
     * Print Status holder
     *
     * Deserialized from JSON
     */
    @SuppressWarnings("WeakerAccess")
    public static class PrintStatusData {
        private DataContainer status;
        private String documentName = "";
        private String printer = "";
        private int pages;
        private String cost = "";

        public PrintStatusData() {
        }

        public PrintStatusData(DataContainer status, String documentName, String printer, int pages, String cost) {
            this.status = status;
            this.documentName = documentName;
            this.printer = printer;
            this.pages = pages;
            this.cost = cost;
        }

        public DataContainer getStatus() {
            return status;
        }

        public PrintStatusData setStatus(DataContainer status) {
            this.status = status;
            return this;
        }

        public String getDocumentName() {
            return documentName;
        }

        public PrintStatusData setDocumentName(String documentName) {
            this.documentName = documentName;
            return this;
        }

        public String getPrinter() {
            return printer;
        }

        public PrintStatusData setPrinter(String printer) {
            this.printer = printer;
            return this;
        }

        public int getPages() {
            return pages;
        }

        public PrintStatusData setPages(int pages) {
            this.pages = pages;
            return this;
        }

        public String getCost() {
            return cost;
        }

        public PrintStatusData setCost(String cost) {
            this.cost = cost;
            return this;
        }

        public static class DataContainer {
            private String code = "";
            private boolean complete;
            private String text = "";
            private List<Message> messages;
            private String formatted = "";

            public DataContainer() {
            }

            public DataContainer(String code, boolean complete, String text, List<Message> messages,
                    String formatted) {
                this.code = code;
                this.complete = complete;
                this.text = text;
                this.messages = messages;
                this.formatted = formatted;
            }

            public String getCode() {
                return code;
            }

            public DataContainer setCode(String code) {
                this.code = code;
                return this;
            }

            public boolean isComplete() {
                return complete;
            }

            public DataContainer setComplete(boolean complete) {
                this.complete = complete;
                return this;
            }

            public String getText() {
                return text;
            }

            public DataContainer setText(String text) {
                this.text = text;
                return this;
            }

            public List<Message> getMessages() {
                return messages;
            }

            public DataContainer setMessages(List<Message> messages) {
                this.messages = messages;
                return this;
            }

            public String getFormatted() {
                return formatted;
            }

            public DataContainer setFormatted(String formatted) {
                this.formatted = formatted;
                return this;
            }

            public static class Message {
                private String info = "";
                private String error = "";

                public Message() {
                }

                public Message(String info, String error) {
                    this.info = info;
                    this.error = error;
                }

                public String getInfo() {
                    return info;
                }

                public Message setInfo(String info) {
                    this.info = info;
                    return this;
                }

                public String getError() {
                    return error;
                }

                public Message setError(String error) {
                    this.error = error;
                    return this;
                }
            }
        }
    }

    //======================= private methods ==================================

    /**
     * Sets the preferred number of copies by parsing the HTML and submitting
     * the HTML form
     *
     * @param printJob     {PrintJob} - what to print
     * @param prevDocument {Element} - the HTML to parse (includes the set
     *                     number of copies form)
     *
     * @return {Element} - the next HTML page in the printing process
     *
     * @throws IOException
     * @throws PrintingException
     */
    static Document setNumberOfCopies(PrintJob printJob, Document prevDocument)
            throws IOException, PrintingException {

        Connection conn = PrintApi.buildConnection(printJob.getSession(), "");
        conn.data(PrintApi.buildSetNumberOfCopiesData(prevDocument, printJob));
        Document result = conn.post();
        if (!PrintApi.checkNumberOfCopiesSet(result)) {
            throw new PrintingException("Error setting the number of copies");
        }
        return result;
    }

    /**
     * Parses the set number of copies page and builds the data required to
     * submit the form
     *
     * @param printJob {PrintJon} - the print job in question
     * @param prevDoc  {Document} - the HTML page containing the form to set the
     *                 number of copies to be printed
     *
     * @return {Map<String, String>} - a HashMap containing the form data
     */
    static Map<String, String> buildSetNumberOfCopiesData(Document prevDoc, PrintJob printJob) {

        Map<String, String> result = new HashMap<>();
        for (Element element : prevDoc.select("form").select("input")) {
            String name = element.attr("name");
            String value = element.attr("value");
            if (Objects.equals(name, "$Submit$0")) {
                continue;
            }
            if (Objects.equals(name, "copies")) {
                value = String.valueOf(printJob.getCopies());
            }
            if (Objects.equals(value, "")) {
                continue;
            }
            result.put(name, value);
        }
        return result;
    }

    /**
     * Checks to see if the number of copies form was submitted successfully by
     * parsing the resulting HTML
     *
     * @param doc {Document} - the next page in the print job submission (the
     *            upload form)
     *
     * @return {boolean} - true if the number of copies was set successfully
     */
    static boolean checkNumberOfCopiesSet(Document doc) {
        return !doc.select("form#upload-form").isEmpty();
    }

    /**
     * Sets the type of printer that should be used
     *
     * @param printJob {PrintJob} - what to print
     *
     * @return {Element} - the next page in the print process
     *
     * @throws IOException
     */
    static Document setPrinterType(PrintJob printJob)
            throws IOException, ExpiredSessionException, PrintingException {

        Document printerTypeElements = PrintApi
                .buildConnection(printJob.getSession(), "?service=action/1/UserWebPrint/0/$ActionLink").execute()
                .parse();

        Connection conn = PrintApi.buildConnection(printJob.getSession(), "");
        conn.data(PrintApi.buildPrinterTypeData(printerTypeElements, printJob)); // set the form data

        Document result = conn.post();
        if (!PrintApi.checkIsPrinterTypeSet(result)) { // if not successful
            throw new PrintingException("Error Setting the printer type");
        }
        return result;
    }

    /**
     * Extracts the form data required to submit to the print service in order
     * to set the printer to print to
     *
     * @param printerType {Document} - the Document to parse and extract the
     *                    printer type form
     * @param printJob    {PrintJob} - the print job to be worked on
     *
     * @return {Map<String, String>} - the resulting form data to submit to the
     * print service
     */
    static Map<String, String> buildPrinterTypeData(Document printerType, PrintJob printJob) {
        Elements printerTypeElements = printerType.select("form#form").select("input");
        Map<String, String> result = new HashMap<>();
        for (Element element : printerTypeElements) {
            String key = element.attr("name");
            String value = element.attr("value");
            if (Objects.equals(key, "$Submit$0")) {
                continue;
            }
            if (key.equals("$RadioGroup")) {
                value = printJob.getPrinterOption().getValue();

            }
            result.put(key, value);
        }

        return result;
    }

    /**
     * Checks to see if the printer type was set
     *
     * @param checkHtml {Document} - the resulting HTML to check after the
     *                  setPrinterType for was submitted
     *
     * @return {boolean} - true if the printer type form was submitted and the
     * printer type was successfully set
     */
    static boolean checkIsPrinterTypeSet(Document checkHtml) {
        return !checkHtml.select("input[name=copies]").isEmpty();
    }

    /**
     * Uploads the file and finalizes the printing
     *
     * @param printJob    {PrintJob} - the print job
     * @param prevElement {Element} - the previous Jsoup element containing the
     *                    upload file Html
     *
     * @return {boolean} - whether or not the print job completed
     */
    static boolean uploadFile(PrintJob printJob, Document prevElement)
            throws PrintingException, UnsupportedMimeTypeException {
        String uploadUrl = printJob.getSession().getDomain().replace("/app", "")
                + PrintApi.getUploadFileUrl(prevElement); // upload directory

        HttpPost post = new HttpPost(uploadUrl);
        CloseableHttpClient client = HttpClientBuilder.create().build();

        // configure multipart post request entity builder
        MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
        entityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);

        // build the file boundary
        String boundary = "-----------------------------" + new Date().getTime();
        entityBuilder.setBoundary(boundary);

        // set the file
        File file = new File(printJob.getFilePath());
        FileBody body = new FileBody(file,
                ContentType.create(ContentTypes.getApplicationType(printJob.getFilePath())), file.getName());
        entityBuilder.addPart("file[]", body);

        // build the post request
        HttpEntity multipart = entityBuilder.build();
        post.setEntity(multipart);

        // set cookie
        post.setHeader("Cookie", printJob.getSession().getSessionKey() + "=" + printJob.getSession().getSession());

        // send
        try {
            CloseableHttpResponse response = client.execute(post);
            return response.getStatusLine().getStatusCode() == 200;
        } catch (IOException e) {
            throw new PrintingException("Error uploading the file");
        }
    }

    /**
     * Submits the file upload form and performs the final step
     *
     * @param printJob {PrintJob} - what to print
     */
    static Document finalizeFileUpload(PrintJob printJob) throws IOException {
        Connection conn = PrintApi.buildConnection(printJob.getSession(), "");

        conn.data("service", "direct/1/UserWebPrintUpload/$Form$0");
        conn.data("sp", "S1");
        return conn.post();
    }

    /**
     * Parses the HTML returned from the final upload form submission and checks
     * to see if the file was uploaded successfully
     *
     * @param prevElement {Element} - the HTML returned from submitting the
     *                    final file upload form
     *
     * @return {boolean} - true if the file uploaded successfully
     */
    static boolean didUpload(Document prevElement) {
        return prevElement.body().select("div#main").toString().contains("successfully submitted");
    }

    /**
     * Parses the final form submission HTMl and extracts the submit job url
     * which can be used to check the file upload status
     *
     * @param prevElement {Element} - the final file upload form HTML result
     *
     * @return {String} - the full URL used to check the status of the file
     * upload
     */
    static String getStatusCheckURL(Document prevElement, String baseDomain) throws MalformedURLException {

        Matcher match = PrintApi.statusPathPattern.matcher(prevElement.body().data());
        match.find();
        String printPathVariable = match.group();
        match = PrintApi.urlPattern.matcher(printPathVariable);
        match.find();
        String uploadId = match.group().replaceAll("\'", "");
        URL u = new URL(baseDomain);
        String url = baseDomain.replaceAll(u.getPath(), "");
        return url + "/rpc/web-print/job-status/" + uploadId + ".json";
    }

    /**
     * Parses the upload from HTML and returns the URL path that the file should
     * be uploaded to.
     *
     * @param prevElement {Element} - the html to parse
     *
     * @return {String } - the url path to upload a file to e.g. /upload/123
     */
    static String getUploadFileUrl(Document prevElement) throws PrintingException {

        Matcher jsMatcher = PrintApi.jsPattern.matcher(prevElement.body().select("script").first().data());
        jsMatcher.find();
        Matcher urlMatcher = PrintApi.urlPattern.matcher(jsMatcher.group());
        if (!urlMatcher.find()) {
            throw new PrintingException("Error parsing out the file upload URL path");
        }
        return urlMatcher.group().replaceAll("\'", "").trim();
    }

    /**
     * Builds the Jsoup Connection object by setting the cookies
     *
     * @param session {PrintJob}
     *
     * @return {Connection} - a new Jsoup Connection
     */
    static Connection buildConnection(SessionFactory.Session session, String url) {
        return Jsoup.connect(session.getDomain() + url).cookie(session.getSessionKey(), session.getSession())
                .cookie(session.getLanguageKey(), session.getLanguage());
    }

    /**
     * Class specifies the print option that is available. This can include the
     * printer location or whether the printer is black and white or color. All
     * values are based on the DOM node values as parsed from the HTML since the
     * server expects a certain response value.
     */
    final public static class PrinterOption implements Serializable {
        private static final long serialVersionUID = 0L;
        final private String name; // dom node name
        final private String value; // dom node value
        final private String description; // dom node text (as displayed on
        // webpage)

        public PrinterOption(String name, String value, String description) {
            this.name = name;
            this.value = value;
            this.description = description;
        }

        /**
         * @return {String} - the printer name as specified in the HTML tag
         * attribute
         */
        public String getName() {
            return name;
        }

        /**
         * This is the printer name (contents of the HTML tag) as displayed on a
         * typical web page.
         *
         * This typically should be used to identify the printer as it is
         * (mostly) unique
         *
         * @return {String} - the printer description
         */
        public String getDescription() {
            return description;
        }

        /**
         * @return {String} - the printer value as specified in the HTML tag
         * attribute
         */
        public String getValue() {
            return value;
        }
    }

}