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 of 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}