001/* 002 * Copyright 2018 Leibniz-Institut für Analytische Wissenschaften – ISAS – e.V.. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.isas.mztab2.cli; 017 018import com.fasterxml.jackson.databind.ObjectMapper; 019import com.fasterxml.jackson.databind.SerializationFeature; 020import de.isas.mztab2.io.MzTabFileParser; 021import de.isas.mztab2.io.MzTabNonValidatingWriter; 022import de.isas.mztab2.model.MzTab; 023import de.isas.mztab2.model.ValidationMessage; 024import static de.isas.mztab2.model.ValidationMessage.MessageTypeEnum.ERROR; 025import static de.isas.mztab2.model.ValidationMessage.MessageTypeEnum.WARN; 026import de.isas.mztab2.validation.CvMappingValidator; 027import java.io.BufferedOutputStream; 028import java.io.File; 029import java.io.FileOutputStream; 030import java.io.IOException; 031import java.io.PrintStream; 032import java.net.MalformedURLException; 033import java.net.URI; 034import java.net.URISyntaxException; 035import java.time.Instant; 036import java.util.List; 037import java.util.Properties; 038import java.util.stream.Collectors; 039import javax.xml.bind.JAXBException; 040import org.apache.commons.cli.*; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabError; 044import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabErrorList; 045import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabErrorType; 046import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabErrorTypeMap; 047 048/** 049 * <p> 050 * MZTabCommandLine class.</p> 051 * 052 * @author qingwei 053 * @author nilshoffmann 054 * @since 17/09/13 055 * 056 */ 057public class MZTabCommandLine { 058 059 private static final Logger LOGGER = LoggerFactory.getLogger( 060 MZTabCommandLine.class); 061 062 private static String getAppInfo() throws IOException { 063 Properties p = new Properties(); 064 p.load(MZTabCommandLine.class.getResourceAsStream( 065 "/application.properties")); 066 StringBuilder sb = new StringBuilder(); 067 String buildDate = p.getProperty("app.build.date", "no build date"); 068 if (!"no build date".equals(buildDate)) { 069 Instant instant = Instant.ofEpochMilli(Long.parseLong(buildDate)); 070 buildDate = instant.toString(); 071 } 072 /* 073 *Property keys are in src/main/resources/application.properties 074 */ 075 sb.append("Running "). 076 append(p.getProperty("app.name", "undefined app")). 077 append("\n\r"). 078 append(" version: '"). 079 append(p.getProperty("app.version", "unknown version")). 080 append("'"). 081 append("\n\r"). 082 append(" build-date: '"). 083 append(buildDate). 084 append("'"). 085 append("\n\r"). 086 append(" scm-location: '"). 087 append(p.getProperty("scm.location", "no scm location")). 088 append("'"). 089 append("\n\r"). 090 append(" commit: '"). 091 append(p.getProperty("scm.commit.id", "no commit id")). 092 append("'"). 093 append("\n\r"). 094 append(" branch: '"). 095 append(p.getProperty("scm.branch", "no branch")). 096 append("'"). 097 append("\n\r"); 098 return sb.toString(); 099 } 100 101 /** 102 * <p> 103 * Runs the command line parser for mzTab, including validation.</p> 104 * 105 * @param args an array of {@link java.lang.String} objects. 106 * @throws java.lang.Exception if any. 107 */ 108 @SuppressWarnings("static-access") 109 public static void main(String[] args) throws Exception { 110 MZTabErrorTypeMap typeMap = new MZTabErrorTypeMap(); 111 CommandLineParser parser = new PosixParser(); 112 Options options = new Options(); 113 String helpOpt = addHelpOption(options); 114 String versionOpt = addVersionOption(options); 115 String msgOpt = addMessageOption(options); 116 String outOpt = addOutFileOption(options); 117 String checkOpt = addCheckOption(options); 118 String levelOpt = addLevelOption(options); 119 String serializeOpt = addSerializeOption(options); 120 String deserializeOpt = addDeserializeOption(options); 121 String checkSemanticOpt = addCheckSemanticOption(options); 122 123 //TODO add option to set whether extra terms not defined in mapping file create a warning or error 124// options.addOption() 125 // Parse command line 126 CommandLine line = parser.parse(options, args); 127 if (line.getOptions().length == 0 || line.hasOption(helpOpt)) { 128 HelpFormatter formatter = new HelpFormatter(); 129 formatter.printHelp("jmztabm-cli", options); 130 } else if (line.hasOption(msgOpt)) { 131 handleMsgOption(line, msgOpt, typeMap); 132 } else if (line.hasOption(versionOpt)) { 133 LOGGER.info(getAppInfo()); 134 } else { 135 boolean hadErrorsOrWarnings = handleValidationOptions(line, outOpt, 136 levelOpt, serializeOpt, 137 deserializeOpt, checkOpt, checkSemanticOpt); 138 if (hadErrorsOrWarnings) { 139 System.exit(1); 140 } 141 } 142 } 143 144 protected static String addVersionOption(Options options) { 145 String versionOpt = "version"; 146 options.addOption("v", versionOpt, false, "Print version information."); 147 return versionOpt; 148 } 149 150 protected static String addHelpOption(Options options) { 151 String helpOpt = "help"; 152 options.addOption("h", helpOpt, false, "Print help message."); 153 return helpOpt; 154 } 155 156 protected static String addCheckSemanticOption(Options options) throws IllegalArgumentException { 157 String checkSemanticOpt = "s"; 158 Option checkSemanticOption = OptionBuilder. 159 withLongOpt("checkSemantic"). 160 isRequired(false). 161 hasOptionalArgs(1). 162 withDescription("Example: -s /path/to/mappingFile.xml. Use the provided mapping file for semantic validation. If no mapping file is provided, the default one will be used. Requires an active internet connection!"). 163 create(checkSemanticOpt); 164 options.addOption(checkSemanticOption); 165 return checkSemanticOpt; 166 } 167 168 protected static String addDeserializeOption(Options options) { 169 String deserializeOpt = "fromJson"; 170 options.addOption(null, deserializeOpt, false, 171 "Example: --fromJson. Will parse inFile as JSON and write mzTab representation to disk. Requires validation to be successful!"); 172 return deserializeOpt; 173 } 174 175 protected static String addSerializeOption(Options options) { 176 String serializeOpt = "toJson"; 177 options.addOption(null, serializeOpt, false, 178 "Example: --toJson. Will write a json representation of inFile to disk. Requires validation to be successful!"); 179 return serializeOpt; 180 } 181 182 protected static String addLevelOption(Options options) { 183 String levelOpt = "l"; 184 options.addOption(levelOpt, "level", true, 185 "Choose validation level (Info, Warn, Error), default level is Info!"); 186 return levelOpt; 187 } 188 189 protected static String addCheckOption(Options options) throws IllegalArgumentException { 190 String checkOpt = "c"; 191 Option checkSemanticOption = OptionBuilder. 192 withLongOpt("check"). 193 isRequired(false). 194 hasArgs(1). 195 withDescription("Example: -c /path/to/file.mztab. Check and validate the provided a mzTab file."). 196 create(checkOpt); 197 options.addOption(checkSemanticOption); 198 return checkOpt; 199 } 200 201 protected static String addOutFileOption(Options options) { 202 String outOpt = "o"; 203 options.addOption(outOpt, "outFile", true, 204 "Example: -o \"output.txt\". Record validation messages into outfile. If not set, print validation messages to stdout/stderr."); 205 return outOpt; 206 } 207 208 protected static String addMessageOption(Options options) throws IllegalArgumentException { 209 String msgOpt = "message"; 210 Option msgOption = OptionBuilder.withLongOpt("message").hasArgs(1). 211 withDescription( 212 "Example: -m 1002. Print validation message detail information based on error code."). 213 create("m"); 214 options.addOption(msgOption); 215 return msgOpt; 216 } 217 218 protected static boolean handleValidationOptions(CommandLine line, 219 String outOpt, String levelOpt, String serializeOpt, 220 String deserializeOpt, String checkOpt, String checkSemanticOpt) throws JAXBException, IllegalArgumentException, URISyntaxException { 221 File outFile = null; 222 if (line.hasOption(outOpt)) { 223 outFile = new File(line.getOptionValue(outOpt)); 224 LOGGER.info("Redirecting validator output to file {}", outFile); 225 } 226 227 try (PrintStream out = outFile == null ? System.out : new PrintStream( 228 new BufferedOutputStream( 229 new FileOutputStream(outFile, false)), true, "UTF8")) { 230 System.setOut(out); 231 System.setErr(out); 232 LOGGER.info(getAppInfo()); 233 MZTabErrorType.Level level = MZTabErrorType.Level.Info; 234 if (line.hasOption(levelOpt)) { 235 level = MZTabErrorType.findLevel(line.getOptionValue( 236 levelOpt)); 237 LOGGER.info("Validator set to level '{}'", level); 238 } else { 239 LOGGER.info( 240 "Validator set to default level '{}'", level); 241 } 242 boolean serializeToJson = false; 243 if (line.hasOption(serializeOpt)) { 244 serializeToJson = true; 245 } 246 247 boolean deserializeFromJson = false; 248 if (line.hasOption(deserializeOpt)) { 249 deserializeFromJson = true; 250 } 251 return handleValidation(line, checkOpt, out, level, 252 checkSemanticOpt, 253 serializeToJson, deserializeFromJson); 254 } catch (IOException ex) { 255 LOGGER.error( 256 "Caught an IO Exception: ", ex); 257 return false; 258 } 259 } 260 261 protected static void handleMsgOption(CommandLine line, String msgOpt, 262 MZTabErrorTypeMap typeMap) throws NumberFormatException { 263 String[] values = line.getOptionValues(msgOpt); 264 Integer code = new Integer(values[0]); 265 MZTabErrorType type = typeMap.getType(code); 266 267 if (type == null) { 268 LOGGER.warn( 269 "Could not find MZTabErrorType for code:" + code); 270 } else { 271 LOGGER.info("MZTabErrorType for code {}: {}", code, type); 272 } 273 } 274 275 protected static boolean handleValidation(CommandLine line, String checkOpt, 276 PrintStream outFile, MZTabErrorType.Level level, String checkSemanticOpt, 277 boolean toJson, boolean fromJson) throws URISyntaxException, JAXBException, IllegalArgumentException, IOException { 278 boolean errorsOrWarnings = false; 279 if (line.hasOption(checkOpt)) { 280 String value = line.getOptionValue(checkOpt); 281 if (value == null) { 282 throw new IllegalArgumentException("No input file provided for validation!"); 283 } 284 File inFile = new File(value.trim()); 285 if (fromJson) { 286 File tmpFile = new File(inFile.getParentFile(), 287 inFile.getName() + ".mztab"); 288 MzTabNonValidatingWriter w = new MzTabNonValidatingWriter(); 289 ObjectMapper mapper = new ObjectMapper(); 290 MzTab mzTab = mapper.readValue(inFile, MzTab.class); 291 LOGGER.info("Writing JSON as mzTab to file: {}", tmpFile. 292 getAbsolutePath()); 293 w.write(tmpFile.toPath(), mzTab); 294 inFile = tmpFile; 295 } 296 LOGGER.info("Beginning validation of mztab file: {}", inFile. 297 getAbsolutePath()); 298 MzTabFileParser mzTabParser = new MzTabFileParser(inFile); 299 MZTabErrorList errorList = mzTabParser.parse(outFile, level); 300 if (!errorList.isEmpty()) { 301 long nErrorsOrWarnings = errorList.getErrorList(). 302 stream(). 303 filter((error) -> 304 { 305 MZTabError e = error; 306 return e.getType(). 307 getLevel() == MZTabErrorType.Level.Error || e. 308 getType(). 309 getLevel() == MZTabErrorType.Level.Warn; 310 }). 311 count(); 312 errorsOrWarnings = nErrorsOrWarnings > 0; 313 //these are reported to std.err already. 314 LOGGER.error( 315 "There were " + errorList.size() + " validation messages including " + nErrorsOrWarnings + " warnings or errors during validation your file, please check the output for details!"); 316 } 317 if (toJson) { 318 File jsonFile = new File(inFile.getName() + ".json"); 319 LOGGER.error( 320 "Writing mzTab object as json to " + jsonFile. 321 getAbsolutePath()); 322 ObjectMapper objectMapper = new ObjectMapper().enable( 323 SerializationFeature.INDENT_OUTPUT); 324 objectMapper. 325 writeValue(jsonFile, mzTabParser.getMZTabFile()); 326 } 327 errorsOrWarnings = errorsOrWarnings || handleSemanticValidation(line, 328 checkSemanticOpt, inFile, outFile, 329 mzTabParser, level); 330 LOGGER.info("Finished validation!"); 331 } 332 return errorsOrWarnings; 333 } 334 335 protected static boolean handleSemanticValidation(CommandLine line, 336 String checkSemanticOpt, File inFile, PrintStream outFile, 337 MzTabFileParser mzTabParser, 338 MZTabErrorType.Level level) throws JAXBException, MalformedURLException, URISyntaxException { 339 boolean errorsOrWarnings = false; 340 if (line.hasOption(checkSemanticOpt)) { 341 String semValue = line.getOptionValue( 342 checkSemanticOpt); 343 URI mappingFile; 344 if (semValue != null) { 345// if (!"mappingFile".equals(semValues[0])) { 346// LOGGER.error("Please use the checkSemantic option as follows, if you want to supply a custom mapping file: '-checkSemantic mappingFile=<path/to/mappingfile.xml>'"); 347// return true; 348// } 349 // read file from path 350 mappingFile = new File(semValue.trim()). 351 getAbsoluteFile(). 352 toURI(); 353 } else { 354 LOGGER.info( 355 "Using default mapping file from classpath: /mappings/mzTab-M-mapping.xml"); 356 // read default file 357 mappingFile = CvMappingValidator.class.getResource( 358 "/mappings/mzTab-M-mapping.xml"). 359 toURI(); 360 } 361 LOGGER.info( 362 "Beginning semantic validation of mztab file: " + inFile. 363 getAbsolutePath() + " with mapping file: " + mappingFile. 364 toASCIIString()); 365 CvMappingValidator cvMappingValidator = CvMappingValidator.of( 366 mappingFile.toURL(), true); 367 List<ValidationMessage> validationMessages = cvMappingValidator. 368 validate(mzTabParser.getMZTabFile()). 369 stream(). 370 filter((message) -> 371 { 372 switch (level) { 373 case Error: 374 return message.getMessageType() == ERROR; 375 case Warn: 376 return message.getMessageType() == ERROR || message. 377 getMessageType() == WARN; 378 case Info: 379 return true; 380 } 381 return false; 382 }). 383 collect(Collectors.toList()); 384 long nErrorsOrWarnings = validationMessages.stream(). 385 filter((validationMessage) -> 386 { 387 return validationMessage.getMessageType() == ValidationMessage.MessageTypeEnum.ERROR || validationMessage. 388 getMessageType() == ValidationMessage.MessageTypeEnum.WARN; 389 }). 390 count(); 391 errorsOrWarnings = nErrorsOrWarnings > 0; 392 if (outFile != null) { 393 for (ValidationMessage message : validationMessages) { 394 outFile.print(message); 395 outFile.println(); 396 } 397 } else { 398 for (ValidationMessage message : validationMessages) { 399 LOGGER.error("{}", message); 400 } 401 } 402 if (!validationMessages.isEmpty()) { 403 LOGGER.error( 404 "There were " + validationMessages.size() + " validation messages including " + nErrorsOrWarnings + " warnings or errors during semantic validation of your file, please check the output for details!"); 405 } else { 406 LOGGER.info( 407 "No errors found for semantic validation on level " + level); 408 } 409 } 410 return errorsOrWarnings; 411 } 412 413}