001/* 002 * Copyright (C) 2016 Hobrasoft s.r.o. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU Affero General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU Affero General Public License for more details. 013 * 014 * You should have received a copy of the GNU Affero General Public License 015 * along with this program. If not, see <http://www.gnu.org/licenses/>. 016 */ 017package cz.hobrasoft.pdfmu.operation.args; 018 019import com.itextpdf.text.DocumentException; 020import com.itextpdf.text.pdf.PdfReader; 021import com.itextpdf.text.pdf.PdfStamper; 022import cz.hobrasoft.pdfmu.PdfmuUtils; 023import static cz.hobrasoft.pdfmu.error.ErrorType.OUTPUT_CLOSE; 024import static cz.hobrasoft.pdfmu.error.ErrorType.OUTPUT_EXISTS_FORCE_NOT_SET; 025import static cz.hobrasoft.pdfmu.error.ErrorType.OUTPUT_NOT_SPECIFIED; 026import static cz.hobrasoft.pdfmu.error.ErrorType.OUTPUT_OPEN; 027import static cz.hobrasoft.pdfmu.error.ErrorType.OUTPUT_STAMPER_CLOSE; 028import static cz.hobrasoft.pdfmu.error.ErrorType.OUTPUT_STAMPER_OPEN; 029import static cz.hobrasoft.pdfmu.error.ErrorType.OUTPUT_WRITE; 030import cz.hobrasoft.pdfmu.operation.OperationException; 031import java.io.ByteArrayOutputStream; 032import java.io.File; 033import java.io.FileNotFoundException; 034import java.io.FileOutputStream; 035import java.io.IOException; 036import java.io.OutputStream; 037import java.util.AbstractMap.SimpleEntry; 038import java.util.logging.Logger; 039import net.sourceforge.argparse4j.impl.Arguments; 040import net.sourceforge.argparse4j.inf.ArgumentParser; 041import net.sourceforge.argparse4j.inf.Namespace; 042 043/** 044 * The methods must be called in the following order: 045 * <ol> 046 * <li>{@link #addArguments(ArgumentParser)} 047 * <li>{@link #setFromNamespace(Namespace)} 048 * <li>{@link #setDefaultFile(File)} (optional) 049 * <li>{@link #open(PdfReader, boolean, char)} 050 * <li>{@link #close()} 051 * </ol> 052 * 053 * @author <a href="mailto:filip.bartek@hobrasoft.cz">Filip Bartek</a> 054 */ 055public class OutPdfArgs implements ArgsConfiguration, AutoCloseable { 056 057 private static final Logger logger = Logger.getLogger(OutPdfArgs.class.getName()); 058 059 private final String metavarIn; 060 private final String metavarOut = "OUT.pdf"; 061 private final boolean allowAppend; 062 063 public OutPdfArgs(String metavarIn, boolean allowAppend) { 064 this.metavarIn = metavarIn; 065 this.allowAppend = allowAppend; 066 } 067 068 @Override 069 public void addArguments(ArgumentParser parser) { 070 parser.addArgument("-o", "--out") 071 .help(String.format("output PDF document (default: <%s>)", metavarIn)) 072 .metavar(metavarOut) 073 .type(Arguments.fileType()); 074 075 if (allowAppend) { 076 parser.addArgument("--append") 077 .help("append to the document, creating a new revision. If this option is disabled, the operation invalidates all the existing signatures.") 078 .type(boolean.class) 079 .setDefault(true); 080 } 081 082 parser.addArgument("-f", "--force") 083 .help(String.format("overwrite %s if it exists", metavarOut)) 084 .type(boolean.class) 085 .action(Arguments.storeTrue()); 086 } 087 088 private File file = null; 089 private boolean overwrite = false; 090 private boolean append = false; 091 092 @Override 093 public void setFromNamespace(Namespace namespace) { 094 file = namespace.get("out"); 095 overwrite = namespace.getBoolean("force"); 096 097 if (allowAppend) { 098 append = namespace.getBoolean("append"); 099 } else { 100 append = false; 101 } 102 } 103 104 /** 105 * Set the target file if it has not been set by 106 * {@link #setFromNamespace(Namespace)}. 107 * 108 * @param file the default file to be used in case none was specified by 109 * {@link #setFromNamespace(Namespace)} 110 */ 111 public void setDefaultFile(File file) { 112 if (this.file == null) { 113 logger.info("Output file has not been specified. Assuming in-place operation."); 114 this.file = file; 115 } 116 } 117 118 private ByteArrayOutputStream os; 119 private PdfStamper stp; 120 121 private void openOs() throws OperationException { 122 assert os == null; 123 124 // Initialize the array length to the file size 125 // because the whole file will have to fit in the array anyway. 126 os = new ByteArrayOutputStream((int) file.length()); 127 } 128 129 private void openStpSignature(PdfReader pdfReader, char pdfVersion) throws OperationException { 130 assert os != null; 131 assert stp == null; 132 133 try { 134 // digitalsignatures20130304.pdf : Code sample 2.17 135 // TODO?: Make sure version is high enough 136 stp = PdfStamper.createSignature(pdfReader, os, pdfVersion, null, append); 137 } catch (DocumentException | IOException ex) { 138 throw new OperationException(OUTPUT_STAMPER_OPEN, ex, 139 PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("outputFile", file))); 140 } 141 } 142 143 private void openStpNew(PdfReader pdfReader, char pdfVersion) throws OperationException { 144 assert os != null; 145 assert stp == null; 146 147 // Open the PDF stamper 148 try { 149 stp = new PdfStamper(pdfReader, os, pdfVersion, append); 150 } catch (DocumentException | IOException ex) { 151 throw new OperationException(OUTPUT_STAMPER_OPEN, ex, 152 PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("outputFile", file))); 153 } 154 } 155 156 /** 157 * Returns a {@link PdfStamper} associated with the internal buffer. Using a 158 * buffer instead of an actual file means that the operation can be rolled 159 * back completely, leaving the output file untouched. Call {@link #close} 160 * to save the content of the buffer to the output file. 161 * 162 * @param pdfReader the input {@link PdfReader} to operate on 163 * @param signature shall we be signing the document? 164 * @param pdfVersion the last character of the PDF version number ('2' to 165 * '7'), or '\0' to keep the original version 166 * @return a {@link PdfStamper} that uses pdfReader as the source 167 * 168 * @throws OperationException if an error occurs 169 */ 170 public PdfStamper open(PdfReader pdfReader, boolean signature, char pdfVersion) throws OperationException { 171 if (file == null) { 172 throw new OperationException(OUTPUT_NOT_SPECIFIED); 173 } 174 assert file != null; 175 176 logger.info(String.format("Output file: %s", file)); 177 if (file.exists()) { 178 logger.info("Output file already exists."); 179 if (overwrite) { 180 logger.info("Will overwrite the output file (--force flag is set)."); 181 } else { 182 throw new OperationException(OUTPUT_EXISTS_FORCE_NOT_SET, 183 PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("outputFile", file))); 184 } 185 } 186 187 openOs(); 188 189 if (signature) { 190 openStpSignature(pdfReader, pdfVersion); 191 } else { 192 openStpNew(pdfReader, pdfVersion); 193 } 194 195 return stp; 196 } 197 198 /** 199 * Writes the content of the internal buffer to the output file. 200 * 201 * @throws OperationException if an error occurs when closing the 202 * {@link PdfStamper} or when writing to the output file 203 */ 204 @Override 205 public void close() throws OperationException { 206 close(false); 207 } 208 209 public void close(boolean success) throws OperationException { 210 if (stp != null) { 211 // Only attempt to close the stamper if the operation has succeeded. 212 if (success) { 213 try { 214 stp.close(); 215 } catch (DocumentException | IOException ex) { 216 throw new OperationException(OUTPUT_STAMPER_CLOSE, ex, 217 PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("outputFile", file))); 218 } 219 } 220 stp = null; 221 } 222 223 if (os != null) { 224 if (success) { 225 assert file != null; 226 logger.info(String.format("Writing the output of the operation to the output file: %s", file)); 227 228 // Save the content of `os` to `file`. 229 { // fileOs 230 OutputStream fileOs = null; 231 try { 232 fileOs = new FileOutputStream(file); 233 } catch (FileNotFoundException ex) { 234 throw new OperationException(OUTPUT_OPEN, ex, 235 PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("outputFile", file))); 236 237 } 238 assert fileOs != null; 239 try { 240 assert os != null; 241 os.writeTo(fileOs); 242 } catch (IOException ex) { 243 throw new OperationException(OUTPUT_WRITE, ex, 244 PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("outputFile", file))); 245 } 246 try { 247 fileOs.close(); 248 } catch (IOException ex) { 249 throw new OperationException(OUTPUT_CLOSE, ex, 250 PdfmuUtils.sortedMap(new SimpleEntry<String, Object>("outputFile", file))); 251 } 252 } 253 } 254 os = null; 255 } 256 } 257 258 public PdfStamper getPdfStamper() { 259 return stp; 260 } 261 262 public File getFile() { 263 return file; 264 } 265 266}