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}