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;
018
019import static cz.hobrasoft.pdfmu.error.ErrorType.INPUT_NOT_FOUND;
020import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_EXPECTED_ONE_ARGUMENT;
021import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_INVALID_CHOICE;
022import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_TOO_FEW_ARGUMENTS;
023import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_UNKNOWN;
024import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_UNRECOGNIZED_ARGUMENT;
025import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_UNRECOGNIZED_COMMAND;
026import cz.hobrasoft.pdfmu.operation.Operation;
027import cz.hobrasoft.pdfmu.operation.OperationAttach;
028import cz.hobrasoft.pdfmu.operation.OperationException;
029import cz.hobrasoft.pdfmu.operation.OperationInspect;
030import cz.hobrasoft.pdfmu.operation.metadata.OperationMetadataSet;
031import cz.hobrasoft.pdfmu.operation.signature.OperationSignatureAdd;
032import cz.hobrasoft.pdfmu.operation.version.OperationVersionSet;
033import java.io.IOException;
034import java.io.InputStream;
035import static java.nio.charset.StandardCharsets.US_ASCII;
036import java.util.ArrayList;
037import java.util.Arrays;
038import java.util.HashSet;
039import java.util.LinkedHashMap;
040import java.util.Map;
041import java.util.Properties;
042import java.util.Set;
043import java.util.logging.LogManager;
044import java.util.logging.Logger;
045import net.sourceforge.argparse4j.ArgumentParsers;
046import net.sourceforge.argparse4j.impl.Arguments;
047import net.sourceforge.argparse4j.inf.ArgumentParser;
048import net.sourceforge.argparse4j.inf.ArgumentParserException;
049import net.sourceforge.argparse4j.inf.Namespace;
050import net.sourceforge.argparse4j.inf.Subparsers;
051import net.sourceforge.argparse4j.internal.HelpScreenException;
052import net.sourceforge.argparse4j.internal.UnrecognizedArgumentException;
053import net.sourceforge.argparse4j.internal.UnrecognizedCommandException;
054import org.apache.commons.io.IOUtils;
055
056/**
057 * The main class of PDFMU
058 *
059 * @author <a href="mailto:filip.bartek@hobrasoft.cz">Filip Bartek</a>
060 */
061public class Main {
062
063    private static void disableLoggers() {
064        // http://stackoverflow.com/a/3363747
065        LogManager.getLogManager().reset(); // Remove the handlers
066    }
067
068    private static final Logger logger = Logger.getLogger(Main.class.getName());
069
070    static {
071        // Configure log message format
072        // Arguments:
073        // http://docs.oracle.com/javase/7/docs/api/java/util/logging/SimpleFormatter.html#format%28java.util.logging.LogRecord%29
074        // %4$s: level
075        // %5$s: message
076        System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s: %5$s%n");
077    }
078
079    private static ArgumentParser createBasicParser() {
080        // Create a command line argument parser
081        ArgumentParser parser = ArgumentParsers.newArgumentParser("pdfmu")
082                .description("PDF Manipulation Utility")
083                .defaultHelp(true);
084
085        parser.version(getProjectVersion());
086        parser.addArgument("-v", "--version")
087                .help("show version and exit")
088                .action(Arguments.version());
089
090        parser.addArgument("--legal-notice")
091                .help("show legal notice and exit")
092                .action(new PrintAndExitAction(getLegalNotice()));
093
094        // TODO: Use an enum
095        parser.addArgument("--output-format")
096                .choices("text", "json")
097                .setDefault("text")
098                .type(String.class)
099                .help("format of stderr output");
100
101        return parser;
102    }
103
104    private static final String POM_PROPERTIES_RESOURCE_NAME = "pom.properties";
105    private static final Properties POM_PROPERTIES = new Properties();
106
107    private static void loadPomProperties() {
108        ClassLoader classLoader = Main.class.getClassLoader();
109        InputStream in = classLoader.getResourceAsStream(POM_PROPERTIES_RESOURCE_NAME);
110        if (in != null) {
111            try {
112                POM_PROPERTIES.load(in);
113            } catch (IOException ex) {
114                logger.severe(String.format("Could not load the POM properties file: %s", ex));
115            }
116            try {
117                in.close();
118            } catch (IOException ex) {
119                logger.severe(String.format("Could not close the POM properties file: %s", ex));
120            }
121        } else {
122            logger.severe("Could not open the POM properties file.");
123        }
124    }
125
126    private static final String LEGAL_NOTICE_RESOURCE_NAME = "cz/hobrasoft/pdfmu/legalNotice.txt";
127    private static String legalNotice;
128
129    private static void loadLegalNotice() {
130        ClassLoader classLoader = Main.class.getClassLoader();
131        InputStream in = classLoader.getResourceAsStream(LEGAL_NOTICE_RESOURCE_NAME);
132        if (in != null) {
133            try {
134                legalNotice = IOUtils.toString(in, US_ASCII);
135            } catch (IOException ex) {
136                logger.severe(String.format("Could not load the legal notice file: %s", ex));
137            }
138            try {
139                in.close();
140            } catch (IOException ex) {
141                logger.severe(String.format("Could not close the legal notice file: %s", ex));
142            }
143        } else {
144            logger.severe("Could not open the legal notice file.");
145        }
146    }
147
148    static {
149        loadPomProperties();
150        loadLegalNotice();
151        assert legalNotice != null;
152    }
153
154    public static String getProjectVersion() {
155        return POM_PROPERTIES.getProperty("projectVersion");
156    }
157
158    public static String getProjectCopyright() {
159        return POM_PROPERTIES.getProperty("copyright");
160    }
161
162    public static String getLegalNotice() {
163        return String.format("%1$s\n\n%2$s", getProjectCopyright(), legalNotice);
164    }
165
166    /**
167     * Use {@link LinkedHashMap} or {@link java.util.TreeMap} as operations to
168     * specify the order in which the operations are printed.
169     *
170     * @param operations a map that assigns the operations to their names
171     * @return an argument parser with the operations attached as sub-commands
172     */
173    private static ArgumentParser createFullParser(Map<String, Operation> operations) {
174        // Create a command line argument parser
175        ArgumentParser parser = createBasicParser();
176
177        // Create a Subparsers instance for operation subparsers
178        Subparsers subparsers = parser.addSubparsers()
179                .title("operations")
180                .metavar("OPERATION")
181                .help("operation to execute")
182                .dest("operation");
183
184        // Configure operation subparsers
185        for (Map.Entry<String, Operation> e : operations.entrySet()) {
186            String name = e.getKey();
187            Operation operation = e.getValue();
188            operation.configureSubparser(subparsers.addParser(name));
189        }
190
191        return parser;
192    }
193
194    private static OperationException apeToOe(ArgumentParserException e) {
195        Set<ExceptionMessagePattern> patterns = new HashSet<>();
196
197        patterns.add(new ExceptionMessagePattern(INPUT_NOT_FOUND,
198                "argument (?<argument>.*): Insufficient permissions to read file: \'(?<file>.*)\'",
199                Arrays.asList(new String[]{"argument", "file"})));
200
201        // UnrecognizedArgumentException
202        patterns.add(new ExceptionMessagePattern(PARSER_UNRECOGNIZED_ARGUMENT,
203                "unrecognized arguments: '(?<argument>.*)'",
204                Arrays.asList(new String[]{"argument"})));
205
206        // ArgumentParserException
207        patterns.add(new ExceptionMessagePattern(PARSER_INVALID_CHOICE,
208                "argument (?<argument>.*): invalid choice: '(?<choice>.*)' \\(choose from \\{(?<validChoices>.*)\\}\\)",
209                Arrays.asList(new String[]{"argument", "choice", "validChoices"})));
210
211        // UnrecognizedCommandException
212        patterns.add(new ExceptionMessagePattern(PARSER_UNRECOGNIZED_COMMAND,
213                "invalid choice: '(?<command>.*)' \\(choose from (?<validCommands>.*)\\)",
214                Arrays.asList(new String[]{"command", "validCommands"})));
215
216        // ArgumentParserException
217        patterns.add(new ExceptionMessagePattern(PARSER_TOO_FEW_ARGUMENTS,
218                "too few arguments",
219                new ArrayList<String>()));
220
221        patterns.add(new ExceptionMessagePattern(PARSER_EXPECTED_ONE_ARGUMENT,
222                "argument (?<argument>.*): expected one argument",
223                Arrays.asList(new String[]{"argument"})));
224
225        OperationException oe = null;
226        for (ExceptionMessagePattern p : patterns) {
227            oe = p.getOperationException(e);
228            if (oe != null) {
229                break;
230            }
231        }
232
233        if (oe == null) {
234            // Unknown parser exception
235            oe = new OperationException(PARSER_UNKNOWN, e);
236        }
237
238        assert oe != null;
239        return oe;
240    }
241
242    /**
243     * The main entry point of PDFMU
244     *
245     * @param args the command line arguments
246     */
247    public static void main(String[] args) {
248        int exitStatus = 0; // Default: 0 (normal termination)
249
250        // Create a map of operations
251        Map<String, Operation> operations = new LinkedHashMap<>();
252        operations.put("inspect", OperationInspect.getInstance());
253        operations.put("update-version", OperationVersionSet.getInstance());
254        operations.put("update-properties", OperationMetadataSet.getInstance());
255        operations.put("attach", OperationAttach.getInstance());
256        operations.put("sign", OperationSignatureAdd.getInstance());
257
258        // Create a command line argument parser
259        ArgumentParser parser = createFullParser(operations);
260
261        // Parse command line arguments
262        Namespace namespace = null;
263        try {
264            // If help is requested,
265            // `parseArgs` prints help message and throws `ArgumentParserException`
266            // (so `namespace` stays null).
267            // If insufficient or invalid `args` are given,
268            // `parseArgs` throws `ArgumentParserException`.
269            namespace = parser.parseArgs(args);
270        } catch (HelpScreenException e) {
271            parser.handleError(e); // Do nothing
272        } catch (UnrecognizedCommandException e) {
273            exitStatus = PARSER_UNRECOGNIZED_COMMAND.getCode();
274            parser.handleError(e); // Print the error in human-readable format
275        } catch (UnrecognizedArgumentException e) {
276            exitStatus = PARSER_UNRECOGNIZED_ARGUMENT.getCode();
277            parser.handleError(e); // Print the error in human-readable format
278        } catch (ArgumentParserException ape) {
279            OperationException oe = apeToOe(ape);
280            exitStatus = oe.getCode();
281            // We could also write `oe` as a JSON document,
282            // but we do not know whether JSON output was requested,
283            // so we use the text output (default).
284
285            parser.handleError(ape); // Print the error in human-readable format
286        }
287
288        if (namespace == null) {
289            System.exit(exitStatus);
290        }
291
292        assert exitStatus == 0;
293
294        // Handle command line arguments
295        WritingMapper wm = null;
296
297        // Extract operation name
298        String operationName = namespace.getString("operation");
299        assert operationName != null; // The argument "operation" is a sub-command, thus it is required
300
301        // Select the operation from `operations`
302        assert operations.containsKey(operationName); // Only supported operation names are allowed
303        Operation operation = operations.get(operationName);
304        assert operation != null;
305
306        // Choose the output format
307        String outputFormat = namespace.getString("output_format");
308        switch (outputFormat) {
309            case "json":
310                // Disable loggers
311                disableLoggers();
312                // Initialize the JSON serializer
313                wm = new WritingMapper();
314                operation.setWritingMapper(wm); // Configure the operation
315                break;
316            case "text":
317                // Initialize the text output
318                TextOutput to = new TextOutput(System.err); // Bind to `System.err`
319                operation.setTextOutput(to); // Configure the operation
320                break;
321            default:
322                assert false; // The option has limited choices
323        }
324
325        // Execute the operation
326        try {
327            operation.execute(namespace);
328        } catch (OperationException ex) {
329            exitStatus = ex.getCode();
330
331            // Log the exception
332            logger.severe(ex.getLocalizedMessage());
333            Throwable cause = ex.getCause();
334            if (cause != null && cause.getMessage() != null) {
335                logger.severe(cause.getLocalizedMessage());
336            }
337
338            if (wm != null) {
339                // JSON output is enabled
340                ex.writeInWritingMapper(wm);
341            }
342        }
343        System.exit(exitStatus);
344    }
345}