MzTabValidatingWriter.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.io;

import de.isas.lipidomics.mztab2.validation.MzTabValidator;
import de.isas.lipidomics.mztab2.validation.Validator;
import de.isas.mztab2.model.MzTab;
import de.isas.mztab2.model.ValidationMessage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabErrorType.Level;

/**
 * <p>
 * MzTabValidatingWriter allows to write MzTab objects after validation with a
 * custom or default validator. Use this if you want to make sure that your fail
 * satisfies the structural and minimal reporting constraints of mzTab.
 * Otherwise, use the MzTabNonValidatingWriter.</p>
 *
 * <p>
 * To create a <b>validating</b> writer using the default checks also applied by
 * the parser, call:</p>
 * {@code MzTabWriter validatingWriter = new MzTabValidatingWriter.Default();}
 * <p>
 * Otherwise, to create a non-validating instance, call:</p>
 * {@code MzTabWriter plainWriter = new MzTabNonValidatingWriter();}
 *
 * @author nilshoffmann
 * @see MzTabValidatingWriter
 * @see MzTabValidator
 */
public class MzTabValidatingWriter implements MzTabWriter<List<ValidationMessage>> {

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

    private final Validator<MzTab> validator;
    private final boolean skipWriteOnValidationFailure;
    private final MzTabWriterDefaults writerDefaults;
    private List<ValidationMessage> validationMessages = null;

    /**
     * Uses default structural validation based on writing and parsing the
     * written file with the default parsing checks. The output file will not be
     * written, if any validation failures occur.
     */
    public MzTabValidatingWriter() {
        this(new WriteAndParseValidator(System.out, Level.Info, 100),
            new MzTabWriterDefaults(), true);
    }

    /**
     * Uses the provided validator and default writer configuration. The output
     * file will not be written, if any validation failures occur.
     *
     * @param validator the validator instance.
     * @param skipWriteOnValidationFailure if true, skips writing of the file if
     * validation fails.
     */
    public MzTabValidatingWriter(Validator<MzTab> validator,
        boolean skipWriteOnValidationFailure) {
        this(validator, new MzTabWriterDefaults(), skipWriteOnValidationFailure);
    }

    /**
     * Uses the provided validator and writerDefaults.
     *
     * @param validator the validator instance.
     * @param writerDefaults the default writer settings.
     * @param skipWriteOnValidationFailure if true, skips writing of the file if
     * validation fails.
     */
    public MzTabValidatingWriter(Validator<MzTab> validator,
        MzTabWriterDefaults writerDefaults, boolean skipWriteOnValidationFailure) {
        this.validator = validator;
        this.writerDefaults = writerDefaults;
        this.skipWriteOnValidationFailure = skipWriteOnValidationFailure;
    }

    /**
     * A default validator implemenation that first writes and then parses the
     * created temporary file, performing the parser checks.
     */
    public static class WriteAndParseValidator implements Validator<MzTab> {

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

        private final OutputStream outputStream;
        private final Level level;
        private final int maxErrorCount;

        /**
         * Create a new instance of this validator.
         *
         * @param outputStream the output stream to write to.
         * @param level the error level for validation.
         * @param maxErrorCount the maximum number of errors before an overflow
         * exception while stop further processing.
         */
        public WriteAndParseValidator(OutputStream outputStream, Level level,
            int maxErrorCount) {
            this.outputStream = outputStream;
            this.level = level;
            this.maxErrorCount = maxErrorCount;
        }

        @Override
        public List<ValidationMessage> validate(MzTab mzTab) {
            MzTabNonValidatingWriter writer = new MzTabNonValidatingWriter();
            File mzTabFile = null;
            try {
                mzTabFile = File.createTempFile(UUID.randomUUID().
                    toString(), ".mztab");
                try (OutputStreamWriter osw = new OutputStreamWriter(
                    new FileOutputStream(mzTabFile),
                    StandardCharsets.UTF_8)) {
                    writer.
                        write(
                            osw, mzTab);

                    MzTabFileParser parser = new MzTabFileParser(mzTabFile);
                    parser.parse(outputStream, level, maxErrorCount);
                    return parser.getErrorList().
                        convertToValidationMessages();
                }
            } catch (IOException ex) {
                LOGGER.error(
                    "Caught exception while trying to parse " + mzTabFile, ex);
            } finally {
                if (mzTabFile != null && mzTabFile.exists()) {
                    if(!mzTabFile.delete()) {
                        LOGGER.warn("Deletion of "+mzTabFile+" failed!");
                    }
                }
            }
            return Collections.emptyList();
        }
    }

    @Override
    public Optional<List<ValidationMessage>> write(OutputStreamWriter writer,
        MzTab mzTab) throws IOException {
        this.validationMessages = Optional.ofNullable(validator.validate(mzTab)).
            orElse(Collections.emptyList());
        if (skipWriteOnValidationFailure && !this.validationMessages.isEmpty()) {
            return Optional.of(this.validationMessages);
        }
        new MzTabNonValidatingWriter(writerDefaults).write(writer, mzTab);
        return Optional.of(this.validationMessages);
    }

    @Override
    public Optional<List<ValidationMessage>> write(Path path, MzTab mzTab) throws IOException {
        this.validationMessages = Optional.ofNullable(validator.validate(mzTab)).
            orElse(Collections.emptyList());
        if (skipWriteOnValidationFailure && !this.validationMessages.isEmpty()) {
            return Optional.of(this.validationMessages);
        }
        new MzTabNonValidatingWriter(writerDefaults).write(path, mzTab);
        return Optional.of(this.validationMessages);
    }

    /**
     * Returns all validation messages ONLY at the given level. E.g. if you
     * provide Info, you will ONLY receive Info messages, even if Warn or Error
     * messages have been produced!
     *
     * @param validationMessages the messages to apply the filter on.
     * @param level the message level.
     * @return the list of validation messages matching the provided level.
     */
    public static List<ValidationMessage> getValidationMessagesForLevel(
        Optional<List<ValidationMessage>> validationMessages,
        ValidationMessage.MessageTypeEnum level) {
        return validationMessages.orElse(Collections.emptyList()).
            stream().
            filter((message) ->
            {
                return message.getMessageType() == level;
            }).
            collect(Collectors.toList());
    }

}