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.validation;
017
018import de.isas.lipidomics.mztab2.validation.Validator;
019import de.isas.mztab2.cvmapping.CvMappingUtils;
020import de.isas.mztab2.cvmapping.CvParameterLookupService;
021import de.isas.mztab2.cvmapping.JxPathElement;
022import de.isas.mztab2.cvmapping.RemoveUserParams;
023import de.isas.mztab2.cvmapping.RuleEvaluationResult;
024import de.isas.mztab2.model.MzTab;
025import de.isas.mztab2.model.Parameter;
026import de.isas.mztab2.model.ValidationMessage;
027import de.isas.mztab2.validation.handlers.AndValidationHandler;
028import de.isas.mztab2.validation.handlers.EmptyRuleHandler;
029import de.isas.mztab2.validation.handlers.ExtraParametersValidationHandler;
030import de.isas.mztab2.validation.handlers.OrValidationHandler;
031import de.isas.mztab2.validation.handlers.ResolvingCvRuleHandler;
032import de.isas.mztab2.validation.handlers.SharedParametersValidationHandler;
033import de.isas.mztab2.validation.handlers.XorValidationHandler;
034import info.psidev.cvmapping.CvMapping;
035import info.psidev.cvmapping.CvMappingRule;
036import java.io.File;
037import java.net.URL;
038import java.util.Arrays;
039import java.util.Collections;
040import java.util.LinkedList;
041import java.util.List;
042import javax.xml.bind.JAXBContext;
043import javax.xml.bind.JAXBException;
044import javax.xml.bind.Unmarshaller;
045import lombok.extern.slf4j.Slf4j;
046import org.apache.commons.jxpath.JXPathContext;
047import org.apache.commons.jxpath.Pointer;
048import org.apache.commons.lang3.tuple.Pair;
049import uk.ac.ebi.pride.jmztab2.utils.errors.CrossCheckErrorType;
050import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabError;
051import uk.ac.ebi.pride.utilities.ols.web.service.client.OLSClient;
052import uk.ac.ebi.pride.utilities.ols.web.service.config.OLSWsConfig;
053
054/**
055 * Validator implementation that uses a provided xml mapping file with rules for
056 * required, recommended and optional CV parameters to assert that an mzTab
057 * follows these rules.
058 * 
059 * First, all preValidators are run, then, the cv parameter validation is executed, before finally, 
060 * the postValidators are run. Each validator can add validation messages to the output.
061 *
062 * @author nilshoffmann
063 */
064@Slf4j
065@lombok.Builder()
066public class CvMappingValidator implements Validator<MzTab> {
067
068    private final CvMapping mapping;
069    private final CvRuleHandler ruleHandler;
070    private final boolean errorIfTermNotInRule;
071    private final CvTermValidationHandler andHandler;
072    private final CvTermValidationHandler orHandler;
073    private final CvTermValidationHandler xorHandler;
074    private final CvTermValidationHandler extraHandler;
075    private final CvTermValidationHandler sharedHandler;
076    private final EmptyRuleHandler emptyRuleHandler;
077    private final RemoveUserParams cvTermSelectionHandler;
078    private final List<Validator<MzTab>> preValidators = new LinkedList<>();
079    private final List<Validator<MzTab>> postValidators = new LinkedList<>();
080
081    /**
082     * Create a new instance of CvMappingValidator. 
083     * 
084     * Uses a default instance of the {@link CvParameterLookupService}.
085     * 
086     * @param mappingFile the mapping file to use
087     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
088     * @return a new CvMappingValidator instance
089     * @throws JAXBException if errors occur during unmarshalling of the mapping xml file.
090     */
091    public static CvMappingValidator of(File mappingFile,
092        boolean errorIfTermNotInRule) throws JAXBException {
093        OLSWsConfig config = new OLSWsConfig();
094        OLSClient client = new OLSClient(config);
095        CvParameterLookupService service = new CvParameterLookupService(client);
096        return of(mappingFile, service, errorIfTermNotInRule);
097    }
098
099    /**
100     * Create a new instance of CvMappingValidator. 
101     * 
102     * Uses the provided {@link CvParameterLookupService}.
103     * 
104     * @param mappingFile the mapping file to use
105     * @param client the ontology lookup service client
106     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
107     * @return a new CvMappingValidator instance
108     * @throws JAXBException if errors occur during unmarshalling of the mapping xml file.
109     */
110    public static CvMappingValidator of(File mappingFile,
111        CvParameterLookupService client, boolean errorIfTermNotInRule) throws JAXBException {
112
113        JAXBContext jaxbContext = JAXBContext.newInstance(CvMapping.class);
114        Unmarshaller u = jaxbContext.createUnmarshaller();
115        CvMapping mapping = (CvMapping) u.unmarshal(mappingFile);
116        return new CvMappingValidator.CvMappingValidatorBuilder().mapping(
117            mapping).
118            ruleHandler(new ResolvingCvRuleHandler(client)).
119            errorIfTermNotInRule(errorIfTermNotInRule).
120            andHandler(new AndValidationHandler()).
121            orHandler(new OrValidationHandler()).
122            xorHandler(new XorValidationHandler()).
123            extraHandler(new ExtraParametersValidationHandler()).
124            sharedHandler(new SharedParametersValidationHandler()).
125            cvTermSelectionHandler(new RemoveUserParams()).
126            emptyRuleHandler(new EmptyRuleHandler()).
127            build().
128            withPreValidator(new CvDefinitionValidationHandler());
129    }
130
131    /**
132     * Add the provided validator implementation to the list of validators that run <b>first</b>.
133     * @param preValidator the validator
134     * @return an instance of this object
135     */
136    public CvMappingValidator withPreValidator(Validator<MzTab> preValidator) {
137        preValidators.add(preValidator);
138        return this;
139    }
140
141    /**
142     * Add the provided validator implementation to the list of validators that run <b>last</b>.
143     * @param postValidator the validator
144     * @return an instance of this object
145     */
146    public CvMappingValidator withPostValidator(Validator<MzTab> postValidator) {
147        postValidators.add(postValidator);
148        return this;
149    }
150
151    /**
152     * Create a new instance of CvMappingValidator. 
153     * 
154     * Uses a default instance of the {@link CvParameterLookupService}.
155     * 
156     * @param mappingFile the mapping file URL to use
157     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
158     * @return a new CvMappingValidator instance
159     * @throws JAXBException if errors occur during unmarshalling of the mapping xml file.
160     */
161    public static CvMappingValidator of(URL mappingFile,
162        boolean errorIfTermNotInRule) throws JAXBException {
163        OLSWsConfig config = new OLSWsConfig();
164        OLSClient client = new OLSClient(config);
165        CvParameterLookupService service = new CvParameterLookupService(client);
166        return of(mappingFile, service, errorIfTermNotInRule);
167    }
168
169    /**
170     * Create a new instance of CvMappingValidator. 
171     * 
172     * Uses the provided {@link CvParameterLookupService}.
173     * 
174     * @param mappingFile the mapping file URL to use
175     * @param client the ontology lookup service client
176     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
177     * @return a new CvMappingValidator instance
178     * @throws JAXBException if errors occur during unmarshalling of the mapping xml file.
179     */
180    public static CvMappingValidator of(URL mappingFile,
181        CvParameterLookupService client, boolean errorIfTermNotInRule) throws JAXBException {
182        JAXBContext jaxbContext = JAXBContext.newInstance(CvMapping.class);
183        Unmarshaller u = jaxbContext.createUnmarshaller();
184        CvMapping mapping = (CvMapping) u.unmarshal(mappingFile);
185        return new CvMappingValidator.CvMappingValidatorBuilder().mapping(
186            mapping).
187            ruleHandler(new ResolvingCvRuleHandler(client)).
188            errorIfTermNotInRule(errorIfTermNotInRule).
189            andHandler(new AndValidationHandler()).
190            orHandler(new OrValidationHandler()).
191            xorHandler(new XorValidationHandler()).
192            extraHandler(new ExtraParametersValidationHandler()).
193            sharedHandler(new SharedParametersValidationHandler()).
194            cvTermSelectionHandler(new RemoveUserParams()).
195            emptyRuleHandler(new EmptyRuleHandler()).
196            build().
197            withPreValidator(new CvDefinitionValidationHandler());
198    }
199
200    /**
201     * Create a new instance of CvMappingValidator. 
202     * 
203     * Uses the provided {@link CvParameterLookupService}.
204     * 
205     * @param mapping the cv mapping to use
206     * @param client the ontology lookup service client
207     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
208     * @return a new CvMappingValidator instance
209     */
210    public static CvMappingValidator of(CvMapping mapping,
211        CvParameterLookupService client,
212        boolean errorIfTermNotInRule) {
213        return new CvMappingValidator.CvMappingValidatorBuilder().mapping(
214            mapping).
215            ruleHandler(new ResolvingCvRuleHandler(client)).
216            errorIfTermNotInRule(errorIfTermNotInRule).
217            andHandler(new AndValidationHandler()).
218            orHandler(new OrValidationHandler()).
219            xorHandler(new XorValidationHandler()).
220            extraHandler(new ExtraParametersValidationHandler()).
221            sharedHandler(new SharedParametersValidationHandler()).
222            cvTermSelectionHandler(new RemoveUserParams()).
223            emptyRuleHandler(new EmptyRuleHandler()).
224            build().
225            withPreValidator(new CvDefinitionValidationHandler());
226    }
227
228    @Override
229    public List<ValidationMessage> validate(MzTab mzTab) {
230        final List<ValidationMessage> messages = new LinkedList<>();
231        log.debug("Applying {} pre validation steps.", preValidators.size());
232        preValidators.stream().
233            forEach((validator) ->
234            {
235                messages.addAll(validator.validate(mzTab));
236            });
237        messages.addAll(new CvDefinitionValidationHandler().validate(mzTab));
238        JXPathContext context = JXPathContext.newContext(mzTab);
239        log.debug("Applying {} cv rule mapping steps.", mapping.
240            getCvMappingRuleList().
241            getCvMappingRule().
242            size());
243        mapping.getCvMappingRuleList().
244            getCvMappingRule().
245            forEach((rule) ->
246            {
247                messages.addAll(handleRule(context, rule, errorIfTermNotInRule));
248            });
249        log.debug("Applying {} post validation steps.", preValidators.size());
250        postValidators.stream().
251            forEach((validator) ->
252            {
253                messages.addAll(validator.validate(mzTab));
254            });
255        return messages;
256    }
257
258    private List<ValidationMessage> handleRule(JXPathContext context,
259        CvMappingRule rule, boolean errorOnTermNotInRule) {
260        String path = rule.getCvElementPath();
261        List<Pair<Pointer, Parameter>> selection = JxPathElement.
262            toList(context, path, Parameter.class);
263
264        final List<ValidationMessage> messages = emptyRuleHandler.handleRule(
265            rule, selection);
266        if (!messages.isEmpty()) {
267            return messages;
268        }
269
270        final List<Pair<Pointer, Parameter>> filteredSelection = cvTermSelectionHandler.
271            handleSelection(selection);
272
273        // and logic means that ALL of the defined terms or their children MUST appear
274        // we only compare valid CVParameters here, user Params (no cv accession), are not compared!
275        // if combination logic is AND, child expansion needs to be disabled to avoid nonsensical combinations
276        try {
277            RuleEvaluationResult result = ruleHandler.handleRule(rule,
278                filteredSelection);
279
280            switch (rule.getCvTermsCombinationLogic()) {
281                case AND:
282                    messages.addAll(andHandler.handleParameters(result,
283                        errorOnTermNotInRule));
284                    break;
285                case OR: // any of the terms or their children need to appear
286                    messages.addAll(orHandler.handleParameters(result,
287                        errorOnTermNotInRule));
288                    break;
289                case XOR:
290                    messages.addAll(xorHandler.handleParameters(result,
291                        errorOnTermNotInRule));
292                    break;
293                default:
294                    throw new IllegalArgumentException(
295                        "Unknown combination logic value: " + rule.
296                            getCvTermsCombinationLogic() + " on rule " + CvMappingUtils.
297                            niceToString(rule) + "! Supported are: " + Arrays.
298                        toString(CvMappingRule.CvTermsCombinationLogic.
299                            values()));
300            }
301            //        messages.addAll(sharedHandler.handleParameters(result, errorOnTermNotInRule));
302            messages.addAll(extraHandler.handleParameters(result,
303                errorOnTermNotInRule));
304        } catch (RuntimeException re) {
305            log.error(
306                "Caught exception while running semantic validation on rule " + CvMappingUtils.
307                    niceToString(rule) + " with selection " + filteredSelection,
308                re);
309            MZTabError error = new MZTabError(
310                CrossCheckErrorType.SemanticValidationException, -1, re.
311                    getMessage());
312            messages.add(error.toValidationMessage());
313        }
314        return messages.isEmpty() ? Collections.emptyList() : messages;
315    }
316
317}