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.io;
017
018import de.isas.lipidomics.mztab2.validation.MzTabValidator;
019import de.isas.lipidomics.mztab2.validation.Validator;
020import de.isas.mztab2.model.MzTab;
021import de.isas.mztab2.model.ValidationMessage;
022import java.io.File;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.OutputStream;
026import java.io.OutputStreamWriter;
027import java.nio.charset.StandardCharsets;
028import java.nio.file.Path;
029import java.util.Collections;
030import java.util.List;
031import java.util.Optional;
032import java.util.UUID;
033import java.util.stream.Collectors;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabErrorType.Level;
037
038/**
039 * <p>
040 * MzTabValidatingWriter allows to write MzTab objects after validation with a
041 * custom or default validator. Use this if you want to make sure that your fail
042 * satisfies the structural and minimal reporting constraints of mzTab.
043 * Otherwise, use the MzTabNonValidatingWriter.</p>
044 *
045 * <p>
046 * To create a <b>validating</b> writer using the default checks also applied by
047 * the parser, call:</p>
048 * {@code MzTabWriter validatingWriter = new MzTabValidatingWriter.Default();}
049 * <p>
050 * Otherwise, to create a non-validating instance, call:</p>
051 * {@code MzTabWriter plainWriter = new MzTabNonValidatingWriter();}
052 *
053 * @author nilshoffmann
054 * @see MzTabValidatingWriter
055 * @see MzTabValidator
056 */
057public class MzTabValidatingWriter implements MzTabWriter<List<ValidationMessage>> {
058
059    private static final Logger LOGGER = LoggerFactory.getLogger(
060        MzTabValidatingWriter.class);
061
062    private final Validator<MzTab> validator;
063    private final boolean skipWriteOnValidationFailure;
064    private final MzTabWriterDefaults writerDefaults;
065    private List<ValidationMessage> validationMessages = null;
066
067    /**
068     * Uses default structural validation based on writing and parsing the
069     * written file with the default parsing checks. The output file will not be
070     * written, if any validation failures occur.
071     */
072    public MzTabValidatingWriter() {
073        this(new WriteAndParseValidator(System.out, Level.Info, 100),
074            new MzTabWriterDefaults(), true);
075    }
076
077    /**
078     * Uses the provided validator and default writer configuration. The output
079     * file will not be written, if any validation failures occur.
080     *
081     * @param validator the validator instance.
082     * @param skipWriteOnValidationFailure if true, skips writing of the file if
083     * validation fails.
084     */
085    public MzTabValidatingWriter(Validator<MzTab> validator,
086        boolean skipWriteOnValidationFailure) {
087        this(validator, new MzTabWriterDefaults(), skipWriteOnValidationFailure);
088    }
089
090    /**
091     * Uses the provided validator and writerDefaults.
092     *
093     * @param validator the validator instance.
094     * @param writerDefaults the default writer settings.
095     * @param skipWriteOnValidationFailure if true, skips writing of the file if
096     * validation fails.
097     */
098    public MzTabValidatingWriter(Validator<MzTab> validator,
099        MzTabWriterDefaults writerDefaults, boolean skipWriteOnValidationFailure) {
100        this.validator = validator;
101        this.writerDefaults = writerDefaults;
102        this.skipWriteOnValidationFailure = skipWriteOnValidationFailure;
103    }
104
105    /**
106     * A default validator implemenation that first writes and then parses the
107     * created temporary file, performing the parser checks.
108     */
109    public static class WriteAndParseValidator implements Validator<MzTab> {
110
111        private static final Logger LOGGER = LoggerFactory.getLogger(
112            WriteAndParseValidator.class);
113
114        private final OutputStream outputStream;
115        private final Level level;
116        private final int maxErrorCount;
117
118        /**
119         * Create a new instance of this validator.
120         *
121         * @param outputStream the output stream to write to.
122         * @param level the error level for validation.
123         * @param maxErrorCount the maximum number of errors before an overflow
124         * exception while stop further processing.
125         */
126        public WriteAndParseValidator(OutputStream outputStream, Level level,
127            int maxErrorCount) {
128            this.outputStream = outputStream;
129            this.level = level;
130            this.maxErrorCount = maxErrorCount;
131        }
132
133        @Override
134        public List<ValidationMessage> validate(MzTab mzTab) {
135            MzTabNonValidatingWriter writer = new MzTabNonValidatingWriter();
136            File mzTabFile = null;
137            try {
138                mzTabFile = File.createTempFile(UUID.randomUUID().
139                    toString(), ".mztab");
140                try (OutputStreamWriter osw = new OutputStreamWriter(
141                    new FileOutputStream(mzTabFile),
142                    StandardCharsets.UTF_8)) {
143                    writer.
144                        write(
145                            osw, mzTab);
146
147                    MzTabFileParser parser = new MzTabFileParser(mzTabFile);
148                    parser.parse(outputStream, level, maxErrorCount);
149                    return parser.getErrorList().
150                        convertToValidationMessages();
151                }
152            } catch (IOException ex) {
153                LOGGER.error(
154                    "Caught exception while trying to parse " + mzTabFile, ex);
155            } finally {
156                if (mzTabFile != null && mzTabFile.exists()) {
157                    if(!mzTabFile.delete()) {
158                        LOGGER.warn("Deletion of "+mzTabFile+" failed!");
159                    }
160                }
161            }
162            return Collections.emptyList();
163        }
164    }
165
166    @Override
167    public Optional<List<ValidationMessage>> write(OutputStreamWriter writer,
168        MzTab mzTab) throws IOException {
169        this.validationMessages = Optional.ofNullable(validator.validate(mzTab)).
170            orElse(Collections.emptyList());
171        if (skipWriteOnValidationFailure && !this.validationMessages.isEmpty()) {
172            return Optional.of(this.validationMessages);
173        }
174        new MzTabNonValidatingWriter(writerDefaults).write(writer, mzTab);
175        return Optional.of(this.validationMessages);
176    }
177
178    @Override
179    public Optional<List<ValidationMessage>> write(Path path, MzTab mzTab) throws IOException {
180        this.validationMessages = Optional.ofNullable(validator.validate(mzTab)).
181            orElse(Collections.emptyList());
182        if (skipWriteOnValidationFailure && !this.validationMessages.isEmpty()) {
183            return Optional.of(this.validationMessages);
184        }
185        new MzTabNonValidatingWriter(writerDefaults).write(path, mzTab);
186        return Optional.of(this.validationMessages);
187    }
188
189    /**
190     * Returns all validation messages ONLY at the given level. E.g. if you
191     * provide Info, you will ONLY receive Info messages, even if Warn or Error
192     * messages have been produced!
193     *
194     * @param validationMessages the messages to apply the filter on.
195     * @param level the message level.
196     * @return the list of validation messages matching the provided level.
197     */
198    public static List<ValidationMessage> getValidationMessagesForLevel(
199        Optional<List<ValidationMessage>> validationMessages,
200        ValidationMessage.MessageTypeEnum level) {
201        return validationMessages.orElse(Collections.emptyList()).
202            stream().
203            filter((message) ->
204            {
205                return message.getMessageType() == level;
206            }).
207            collect(Collectors.toList());
208    }
209
210}