MZTabCommandLine.java

/* 
 * Copyright 2018 Leibniz-Institut für Analytische Wissenschaften – ISAS – e.V..
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.isas.mztab2.cli;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import de.isas.mztab2.io.MzTabFileParser;
import de.isas.mztab2.io.MzTabNonValidatingWriter;
import de.isas.mztab2.model.MzTab;
import de.isas.mztab2.model.ValidationMessage;
import static de.isas.mztab2.model.ValidationMessage.MessageTypeEnum.ERROR;
import static de.isas.mztab2.model.ValidationMessage.MessageTypeEnum.WARN;
import de.isas.mztab2.validation.CvMappingValidator;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBException;
import org.apache.commons.cli.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabError;
import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabErrorList;
import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabErrorType;
import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabErrorTypeMap;

/**
 * <p>
 * MZTabCommandLine class.</p>
 *
 * @author qingwei
 * @author nilshoffmann
 * @since 17/09/13
 *
 */
public class MZTabCommandLine {

    private static final Logger LOGGER = LoggerFactory.getLogger(
        MZTabCommandLine.class);

    private static String getAppInfo() throws IOException {
        Properties p = new Properties();
        p.load(MZTabCommandLine.class.getResourceAsStream(
            "/application.properties"));
        StringBuilder sb = new StringBuilder();
        String buildDate = p.getProperty("app.build.date", "no build date");
        if (!"no build date".equals(buildDate)) {
            Instant instant = Instant.ofEpochMilli(Long.parseLong(buildDate));
            buildDate = instant.toString();
        }
        /*
         *Property keys are in src/main/resources/application.properties
         */
        sb.append("Running ").
            append(p.getProperty("app.name", "undefined app")).
            append("\n\r").
            append(" version: '").
            append(p.getProperty("app.version", "unknown version")).
            append("'").
            append("\n\r").
            append(" build-date: '").
            append(buildDate).
            append("'").
            append("\n\r").
            append(" scm-location: '").
            append(p.getProperty("scm.location", "no scm location")).
            append("'").
            append("\n\r").
            append(" commit: '").
            append(p.getProperty("scm.commit.id", "no commit id")).
            append("'").
            append("\n\r").
            append(" branch: '").
            append(p.getProperty("scm.branch", "no branch")).
            append("'").
            append("\n\r");
        return sb.toString();
    }

    /**
     * <p>
     * Runs the command line parser for mzTab, including validation.</p>
     *
     * @param args an array of {@link java.lang.String} objects.
     * @throws java.lang.Exception if any.
     */
    @SuppressWarnings("static-access")
    public static void main(String[] args) throws Exception {
        MZTabErrorTypeMap typeMap = new MZTabErrorTypeMap();
        CommandLineParser parser = new PosixParser();
        Options options = new Options();
        String helpOpt = addHelpOption(options);
        String versionOpt = addVersionOption(options);
        String msgOpt = addMessageOption(options);
        String outOpt = addOutFileOption(options);
        String checkOpt = addCheckOption(options);
        String levelOpt = addLevelOption(options);
        String serializeOpt = addSerializeOption(options);
        String deserializeOpt = addDeserializeOption(options);
        String checkSemanticOpt = addCheckSemanticOption(options);

        //TODO add option to set whether extra terms not defined in mapping file create a warning or error
//        options.addOption()
        // Parse command line
        CommandLine line = parser.parse(options, args);
        if (line.getOptions().length == 0 || line.hasOption(helpOpt)) {
            HelpFormatter formatter = new HelpFormatter();
            formatter.printHelp("jmztabm-cli", options);
        } else if (line.hasOption(msgOpt)) {
            handleMsgOption(line, msgOpt, typeMap);
        } else if (line.hasOption(versionOpt)) {
            LOGGER.info(getAppInfo());
        } else {
            boolean hadErrorsOrWarnings = handleValidationOptions(line, outOpt,
                levelOpt, serializeOpt,
                deserializeOpt, checkOpt, checkSemanticOpt);
            if (hadErrorsOrWarnings) {
                System.exit(1);
            }
        }
    }

    protected static String addVersionOption(Options options) {
        String versionOpt = "version";
        options.addOption("v", versionOpt, false, "Print version information.");
        return versionOpt;
    }

    protected static String addHelpOption(Options options) {
        String helpOpt = "help";
        options.addOption("h", helpOpt, false, "Print help message.");
        return helpOpt;
    }

    protected static String addCheckSemanticOption(Options options) throws IllegalArgumentException {
        String checkSemanticOpt = "s";
        Option checkSemanticOption = OptionBuilder.
            withLongOpt("checkSemantic").
            isRequired(false).
            hasOptionalArgs(1).
            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!").
            create(checkSemanticOpt);
        options.addOption(checkSemanticOption);
        return checkSemanticOpt;
    }

    protected static String addDeserializeOption(Options options) {
        String deserializeOpt = "fromJson";
        options.addOption(null, deserializeOpt, false,
            "Example: --fromJson. Will parse inFile as JSON and write mzTab representation to disk. Requires validation to be successful!");
        return deserializeOpt;
    }

    protected static String addSerializeOption(Options options) {
        String serializeOpt = "toJson";
        options.addOption(null, serializeOpt, false,
            "Example: --toJson. Will write a json representation of inFile to disk. Requires validation to be successful!");
        return serializeOpt;
    }

    protected static String addLevelOption(Options options) {
        String levelOpt = "l";
        options.addOption(levelOpt, "level", true,
            "Choose validation level (Info, Warn, Error), default level is Info!");
        return levelOpt;
    }
    
    protected static String addCheckOption(Options options) throws IllegalArgumentException {
        String checkOpt = "c";
        Option checkSemanticOption = OptionBuilder.
            withLongOpt("check").
            isRequired(false).
            hasArgs(1).
            withDescription("Example: -c /path/to/file.mztab. Check and validate the provided a mzTab file.").
            create(checkOpt);
        options.addOption(checkSemanticOption);
        return checkOpt;
    }

    protected static String addOutFileOption(Options options) {
        String outOpt = "o";
        options.addOption(outOpt, "outFile", true,
            "Example: -o \"output.txt\". Record validation messages into outfile. If not set, print validation messages to stdout/stderr.");
        return outOpt;
    }

    protected static String addMessageOption(Options options) throws IllegalArgumentException {
        String msgOpt = "message";
        Option msgOption = OptionBuilder.withLongOpt("message").hasArgs(1).
            withDescription(
                "Example: -m 1002. Print validation message detail information based on error code.").
            create("m");
        options.addOption(msgOption);
        return msgOpt;
    }

    protected static boolean handleValidationOptions(CommandLine line,
        String outOpt, String levelOpt, String serializeOpt,
        String deserializeOpt, String checkOpt, String checkSemanticOpt) throws JAXBException, IllegalArgumentException, URISyntaxException {
        File outFile = null;
        if (line.hasOption(outOpt)) {
            outFile = new File(line.getOptionValue(outOpt));
            LOGGER.info("Redirecting validator output to file {}", outFile);
        }

        try (PrintStream out = outFile == null ? System.out : new PrintStream(
            new BufferedOutputStream(
                new FileOutputStream(outFile, false)), true, "UTF8")) {
            System.setOut(out);
            System.setErr(out);
            LOGGER.info(getAppInfo());
            MZTabErrorType.Level level = MZTabErrorType.Level.Info;
            if (line.hasOption(levelOpt)) {
                level = MZTabErrorType.findLevel(line.getOptionValue(
                    levelOpt));
                LOGGER.info("Validator set to level '{}'", level);
            } else {
                LOGGER.info(
                    "Validator set to default level '{}'", level);
            }
            boolean serializeToJson = false;
            if (line.hasOption(serializeOpt)) {
                serializeToJson = true;
            }

            boolean deserializeFromJson = false;
            if (line.hasOption(deserializeOpt)) {
                deserializeFromJson = true;
            }
            return handleValidation(line, checkOpt, out, level,
                checkSemanticOpt,
                serializeToJson, deserializeFromJson);
        } catch (IOException ex) {
            LOGGER.error(
                "Caught an IO Exception: ", ex);
            return false;
        }
    }

    protected static void handleMsgOption(CommandLine line, String msgOpt,
        MZTabErrorTypeMap typeMap) throws NumberFormatException {
        String[] values = line.getOptionValues(msgOpt);
        Integer code = new Integer(values[0]);
        MZTabErrorType type = typeMap.getType(code);

        if (type == null) {
            LOGGER.warn(
                "Could not find MZTabErrorType for code:" + code);
        } else {
            LOGGER.info("MZTabErrorType for code {}: {}", code, type);
        }
    }

    protected static boolean handleValidation(CommandLine line, String checkOpt,
        PrintStream outFile, MZTabErrorType.Level level, String checkSemanticOpt,
        boolean toJson, boolean fromJson) throws URISyntaxException, JAXBException, IllegalArgumentException, IOException {
        boolean errorsOrWarnings = false;
        if (line.hasOption(checkOpt)) {
            String value = line.getOptionValue(checkOpt);
            if (value == null) {
                throw new IllegalArgumentException("No input file provided for validation!");
            }
            File inFile = new File(value.trim());
            if (fromJson) {
                File tmpFile = new File(inFile.getParentFile(),
                    inFile.getName() + ".mztab");
                MzTabNonValidatingWriter w = new MzTabNonValidatingWriter();
                ObjectMapper mapper = new ObjectMapper();
                MzTab mzTab = mapper.readValue(inFile, MzTab.class);
                LOGGER.info("Writing JSON as mzTab to file: {}", tmpFile.
                    getAbsolutePath());
                w.write(tmpFile.toPath(), mzTab);
                inFile = tmpFile;
            }
            LOGGER.info("Beginning validation of mztab file: {}", inFile.
                getAbsolutePath());
            MzTabFileParser mzTabParser = new MzTabFileParser(inFile);
            MZTabErrorList errorList = mzTabParser.parse(outFile, level);
            if (!errorList.isEmpty()) {
                long nErrorsOrWarnings = errorList.getErrorList().
                    stream().
                    filter((error) ->
                    {
                        MZTabError e = error;
                        return e.getType().
                                getLevel() == MZTabErrorType.Level.Error || e.
                                        getType().
                                        getLevel() == MZTabErrorType.Level.Warn;
                    }).
                    count();
                errorsOrWarnings = nErrorsOrWarnings > 0;
                //these are reported to std.err already.
                LOGGER.error(
                    "There were " + errorList.size() + " validation messages including " + nErrorsOrWarnings + " warnings or errors during validation your file, please check the output for details!");
            }
            if (toJson) {
                File jsonFile = new File(inFile.getName() + ".json");
                LOGGER.error(
                    "Writing mzTab object as json to " + jsonFile.
                        getAbsolutePath());
                ObjectMapper objectMapper = new ObjectMapper().enable(
                    SerializationFeature.INDENT_OUTPUT);
                objectMapper.
                    writeValue(jsonFile, mzTabParser.getMZTabFile());
            }
            errorsOrWarnings = errorsOrWarnings || handleSemanticValidation(line,
                checkSemanticOpt, inFile, outFile,
                mzTabParser, level);
            LOGGER.info("Finished validation!");
        }
        return errorsOrWarnings;
    }

    protected static boolean handleSemanticValidation(CommandLine line,
        String checkSemanticOpt, File inFile, PrintStream outFile,
        MzTabFileParser mzTabParser,
        MZTabErrorType.Level level) throws JAXBException, MalformedURLException, URISyntaxException {
        boolean errorsOrWarnings = false;
        if (line.hasOption(checkSemanticOpt)) {
            String semValue = line.getOptionValue(
                checkSemanticOpt);
            URI mappingFile;
            if (semValue != null) {
//                if (!"mappingFile".equals(semValues[0])) {
//                    LOGGER.error("Please use the checkSemantic option as follows, if you want to supply a custom mapping file: '-checkSemantic mappingFile=<path/to/mappingfile.xml>'");
//                    return true;
//                }
                // read file from path
                mappingFile = new File(semValue.trim()).
                    getAbsoluteFile().
                    toURI();
            } else {
                LOGGER.info(
                    "Using default mapping file from classpath: /mappings/mzTab-M-mapping.xml");
                // read default file
                mappingFile = CvMappingValidator.class.getResource(
                    "/mappings/mzTab-M-mapping.xml").
                    toURI();
            }
            LOGGER.info(
                "Beginning semantic validation of mztab file: " + inFile.
                    getAbsolutePath() + " with mapping file: " + mappingFile.
                    toASCIIString());
            CvMappingValidator cvMappingValidator = CvMappingValidator.of(
                mappingFile.toURL(), true);
            List<ValidationMessage> validationMessages = cvMappingValidator.
                validate(mzTabParser.getMZTabFile()).
                stream().
                filter((message) ->
                {
                    switch (level) {
                        case Error:
                            return message.getMessageType() == ERROR;
                        case Warn:
                            return message.getMessageType() == ERROR || message.
                                getMessageType() == WARN;
                        case Info:
                            return true;
                    }
                    return false;
                }).
                collect(Collectors.toList());
            long nErrorsOrWarnings = validationMessages.stream().
                filter((validationMessage) ->
                {
                    return validationMessage.getMessageType() == ValidationMessage.MessageTypeEnum.ERROR || validationMessage.
                        getMessageType() == ValidationMessage.MessageTypeEnum.WARN;
                }).
                count();
            errorsOrWarnings = nErrorsOrWarnings > 0;
            if (outFile != null) {
                for (ValidationMessage message : validationMessages) {
                    outFile.print(message);
                    outFile.println();
                }
            } else {
                for (ValidationMessage message : validationMessages) {
                    LOGGER.error("{}", message);
                }
            }
            if (!validationMessages.isEmpty()) {
                LOGGER.error(
                    "There were " + validationMessages.size() + " validation messages including " + nErrorsOrWarnings + " warnings or errors during semantic validation of your file, please check the output for details!");
            } else {
                LOGGER.info(
                    "No errors found for semantic validation on level " + level);
            }
        }
        return errorsOrWarnings;
    }

}