View Javadoc
1   /* 
2    * Copyright (C) 2016 Hobrasoft s.r.o.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU Affero General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU Affero General Public License for more details.
13   *
14   * You should have received a copy of the GNU Affero General Public License
15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16   */
17  package cz.hobrasoft.pdfmu;
18  
19  import static cz.hobrasoft.pdfmu.error.ErrorType.INPUT_NOT_FOUND;
20  import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_EXPECTED_ONE_ARGUMENT;
21  import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_INVALID_CHOICE;
22  import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_TOO_FEW_ARGUMENTS;
23  import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_UNKNOWN;
24  import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_UNRECOGNIZED_ARGUMENT;
25  import static cz.hobrasoft.pdfmu.error.ErrorType.PARSER_UNRECOGNIZED_COMMAND;
26  import cz.hobrasoft.pdfmu.operation.Operation;
27  import cz.hobrasoft.pdfmu.operation.OperationAttach;
28  import cz.hobrasoft.pdfmu.operation.OperationException;
29  import cz.hobrasoft.pdfmu.operation.OperationInspect;
30  import cz.hobrasoft.pdfmu.operation.metadata.OperationMetadataSet;
31  import cz.hobrasoft.pdfmu.operation.signature.OperationSignatureAdd;
32  import cz.hobrasoft.pdfmu.operation.version.OperationVersionSet;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import static java.nio.charset.StandardCharsets.US_ASCII;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.HashSet;
39  import java.util.LinkedHashMap;
40  import java.util.Map;
41  import java.util.Properties;
42  import java.util.Set;
43  import java.util.logging.LogManager;
44  import java.util.logging.Logger;
45  import net.sourceforge.argparse4j.ArgumentParsers;
46  import net.sourceforge.argparse4j.impl.Arguments;
47  import net.sourceforge.argparse4j.inf.ArgumentParser;
48  import net.sourceforge.argparse4j.inf.ArgumentParserException;
49  import net.sourceforge.argparse4j.inf.Namespace;
50  import net.sourceforge.argparse4j.inf.Subparsers;
51  import net.sourceforge.argparse4j.internal.HelpScreenException;
52  import net.sourceforge.argparse4j.internal.UnrecognizedArgumentException;
53  import net.sourceforge.argparse4j.internal.UnrecognizedCommandException;
54  import org.apache.commons.io.IOUtils;
55  
56  /**
57   * The main class of PDFMU
58   *
59   * @author <a href="mailto:filip.bartek@hobrasoft.cz">Filip Bartek</a>
60   */
61  public class Main {
62  
63      private static void disableLoggers() {
64          // http://stackoverflow.com/a/3363747
65          LogManager.getLogManager().reset(); // Remove the handlers
66      }
67  
68      private static final Logger logger = Logger.getLogger(Main.class.getName());
69  
70      static {
71          // Configure log message format
72          // Arguments:
73          // http://docs.oracle.com/javase/7/docs/api/java/util/logging/SimpleFormatter.html#format%28java.util.logging.LogRecord%29
74          // %4$s: level
75          // %5$s: message
76          System.setProperty("java.util.logging.SimpleFormatter.format", "%4$s: %5$s%n");
77      }
78  
79      private static ArgumentParser createBasicParser() {
80          // Create a command line argument parser
81          ArgumentParser parser = ArgumentParsers.newArgumentParser("pdfmu")
82                  .description("PDF Manipulation Utility")
83                  .defaultHelp(true);
84  
85          parser.version(getProjectVersion());
86          parser.addArgument("-v", "--version")
87                  .help("show version and exit")
88                  .action(Arguments.version());
89  
90          parser.addArgument("--legal-notice")
91                  .help("show legal notice and exit")
92                  .action(new PrintAndExitAction(getLegalNotice()));
93  
94          // TODO: Use an enum
95          parser.addArgument("--output-format")
96                  .choices("text", "json")
97                  .setDefault("text")
98                  .type(String.class)
99                  .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 }