Java tutorial
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; } } }