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}