CvMappingValidator.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.validation;

import de.isas.lipidomics.mztab2.validation.Validator;
import de.isas.mztab2.cvmapping.CvMappingUtils;
import de.isas.mztab2.cvmapping.CvParameterLookupService;
import de.isas.mztab2.cvmapping.JxPathElement;
import de.isas.mztab2.cvmapping.RemoveUserParams;
import de.isas.mztab2.cvmapping.RuleEvaluationResult;
import de.isas.mztab2.model.MzTab;
import de.isas.mztab2.model.Parameter;
import de.isas.mztab2.model.ValidationMessage;
import de.isas.mztab2.validation.handlers.AndValidationHandler;
import de.isas.mztab2.validation.handlers.EmptyRuleHandler;
import de.isas.mztab2.validation.handlers.ExtraParametersValidationHandler;
import de.isas.mztab2.validation.handlers.OrValidationHandler;
import de.isas.mztab2.validation.handlers.ResolvingCvRuleHandler;
import de.isas.mztab2.validation.handlers.SharedParametersValidationHandler;
import de.isas.mztab2.validation.handlers.XorValidationHandler;
import info.psidev.cvmapping.CvMapping;
import info.psidev.cvmapping.CvMappingRule;
import java.io.File;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.Pointer;
import org.apache.commons.lang3.tuple.Pair;
import uk.ac.ebi.pride.jmztab2.utils.errors.CrossCheckErrorType;
import uk.ac.ebi.pride.jmztab2.utils.errors.MZTabError;
import uk.ac.ebi.pride.utilities.ols.web.service.client.OLSClient;
import uk.ac.ebi.pride.utilities.ols.web.service.config.OLSWsConfig;

/**
 * Validator implementation that uses a provided xml mapping file with rules for
 * required, recommended and optional CV parameters to assert that an mzTab
 * follows these rules.
 * 
 * First, all preValidators are run, then, the cv parameter validation is executed, before finally, 
 * the postValidators are run. Each validator can add validation messages to the output.
 *
 * @author nilshoffmann
 */
@Slf4j
@lombok.Builder()
public class CvMappingValidator implements Validator<MzTab> {

    private final CvMapping mapping;
    private final CvRuleHandler ruleHandler;
    private final boolean errorIfTermNotInRule;
    private final CvTermValidationHandler andHandler;
    private final CvTermValidationHandler orHandler;
    private final CvTermValidationHandler xorHandler;
    private final CvTermValidationHandler extraHandler;
    private final CvTermValidationHandler sharedHandler;
    private final EmptyRuleHandler emptyRuleHandler;
    private final RemoveUserParams cvTermSelectionHandler;
    private final List<Validator<MzTab>> preValidators = new LinkedList<>();
    private final List<Validator<MzTab>> postValidators = new LinkedList<>();

    /**
     * Create a new instance of CvMappingValidator. 
     * 
     * Uses a default instance of the {@link CvParameterLookupService}.
     * 
     * @param mappingFile the mapping file to use
     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
     * @return a new CvMappingValidator instance
     * @throws JAXBException if errors occur during unmarshalling of the mapping xml file.
     */
    public static CvMappingValidator of(File mappingFile,
        boolean errorIfTermNotInRule) throws JAXBException {
        OLSWsConfig config = new OLSWsConfig();
        OLSClient client = new OLSClient(config);
        CvParameterLookupService service = new CvParameterLookupService(client);
        return of(mappingFile, service, errorIfTermNotInRule);
    }

    /**
     * Create a new instance of CvMappingValidator. 
     * 
     * Uses the provided {@link CvParameterLookupService}.
     * 
     * @param mappingFile the mapping file to use
     * @param client the ontology lookup service client
     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
     * @return a new CvMappingValidator instance
     * @throws JAXBException if errors occur during unmarshalling of the mapping xml file.
     */
    public static CvMappingValidator of(File mappingFile,
        CvParameterLookupService client, boolean errorIfTermNotInRule) throws JAXBException {

        JAXBContext jaxbContext = JAXBContext.newInstance(CvMapping.class);
        Unmarshaller u = jaxbContext.createUnmarshaller();
        CvMapping mapping = (CvMapping) u.unmarshal(mappingFile);
        return new CvMappingValidator.CvMappingValidatorBuilder().mapping(
            mapping).
            ruleHandler(new ResolvingCvRuleHandler(client)).
            errorIfTermNotInRule(errorIfTermNotInRule).
            andHandler(new AndValidationHandler()).
            orHandler(new OrValidationHandler()).
            xorHandler(new XorValidationHandler()).
            extraHandler(new ExtraParametersValidationHandler()).
            sharedHandler(new SharedParametersValidationHandler()).
            cvTermSelectionHandler(new RemoveUserParams()).
            emptyRuleHandler(new EmptyRuleHandler()).
            build().
            withPreValidator(new CvDefinitionValidationHandler());
    }

    /**
     * Add the provided validator implementation to the list of validators that run <b>first</b>.
     * @param preValidator the validator
     * @return an instance of this object
     */
    public CvMappingValidator withPreValidator(Validator<MzTab> preValidator) {
        preValidators.add(preValidator);
        return this;
    }

    /**
     * Add the provided validator implementation to the list of validators that run <b>last</b>.
     * @param postValidator the validator
     * @return an instance of this object
     */
    public CvMappingValidator withPostValidator(Validator<MzTab> postValidator) {
        postValidators.add(postValidator);
        return this;
    }

    /**
     * Create a new instance of CvMappingValidator. 
     * 
     * Uses a default instance of the {@link CvParameterLookupService}.
     * 
     * @param mappingFile the mapping file URL to use
     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
     * @return a new CvMappingValidator instance
     * @throws JAXBException if errors occur during unmarshalling of the mapping xml file.
     */
    public static CvMappingValidator of(URL mappingFile,
        boolean errorIfTermNotInRule) throws JAXBException {
        OLSWsConfig config = new OLSWsConfig();
        OLSClient client = new OLSClient(config);
        CvParameterLookupService service = new CvParameterLookupService(client);
        return of(mappingFile, service, errorIfTermNotInRule);
    }

    /**
     * Create a new instance of CvMappingValidator. 
     * 
     * Uses the provided {@link CvParameterLookupService}.
     * 
     * @param mappingFile the mapping file URL to use
     * @param client the ontology lookup service client
     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
     * @return a new CvMappingValidator instance
     * @throws JAXBException if errors occur during unmarshalling of the mapping xml file.
     */
    public static CvMappingValidator of(URL mappingFile,
        CvParameterLookupService client, boolean errorIfTermNotInRule) throws JAXBException {
        JAXBContext jaxbContext = JAXBContext.newInstance(CvMapping.class);
        Unmarshaller u = jaxbContext.createUnmarshaller();
        CvMapping mapping = (CvMapping) u.unmarshal(mappingFile);
        return new CvMappingValidator.CvMappingValidatorBuilder().mapping(
            mapping).
            ruleHandler(new ResolvingCvRuleHandler(client)).
            errorIfTermNotInRule(errorIfTermNotInRule).
            andHandler(new AndValidationHandler()).
            orHandler(new OrValidationHandler()).
            xorHandler(new XorValidationHandler()).
            extraHandler(new ExtraParametersValidationHandler()).
            sharedHandler(new SharedParametersValidationHandler()).
            cvTermSelectionHandler(new RemoveUserParams()).
            emptyRuleHandler(new EmptyRuleHandler()).
            build().
            withPreValidator(new CvDefinitionValidationHandler());
    }

    /**
     * Create a new instance of CvMappingValidator. 
     * 
     * Uses the provided {@link CvParameterLookupService}.
     * 
     * @param mapping the cv mapping to use
     * @param client the ontology lookup service client
     * @param errorIfTermNotInRule raise an error if a term is not defined within an otherwise matching rule for the element
     * @return a new CvMappingValidator instance
     */
    public static CvMappingValidator of(CvMapping mapping,
        CvParameterLookupService client,
        boolean errorIfTermNotInRule) {
        return new CvMappingValidator.CvMappingValidatorBuilder().mapping(
            mapping).
            ruleHandler(new ResolvingCvRuleHandler(client)).
            errorIfTermNotInRule(errorIfTermNotInRule).
            andHandler(new AndValidationHandler()).
            orHandler(new OrValidationHandler()).
            xorHandler(new XorValidationHandler()).
            extraHandler(new ExtraParametersValidationHandler()).
            sharedHandler(new SharedParametersValidationHandler()).
            cvTermSelectionHandler(new RemoveUserParams()).
            emptyRuleHandler(new EmptyRuleHandler()).
            build().
            withPreValidator(new CvDefinitionValidationHandler());
    }

    @Override
    public List<ValidationMessage> validate(MzTab mzTab) {
        final List<ValidationMessage> messages = new LinkedList<>();
        log.debug("Applying {} pre validation steps.", preValidators.size());
        preValidators.stream().
            forEach((validator) ->
            {
                messages.addAll(validator.validate(mzTab));
            });
        messages.addAll(new CvDefinitionValidationHandler().validate(mzTab));
        JXPathContext context = JXPathContext.newContext(mzTab);
        log.debug("Applying {} cv rule mapping steps.", mapping.
            getCvMappingRuleList().
            getCvMappingRule().
            size());
        mapping.getCvMappingRuleList().
            getCvMappingRule().
            forEach((rule) ->
            {
                messages.addAll(handleRule(context, rule, errorIfTermNotInRule));
            });
        log.debug("Applying {} post validation steps.", preValidators.size());
        postValidators.stream().
            forEach((validator) ->
            {
                messages.addAll(validator.validate(mzTab));
            });
        return messages;
    }

    private List<ValidationMessage> handleRule(JXPathContext context,
        CvMappingRule rule, boolean errorOnTermNotInRule) {
        String path = rule.getCvElementPath();
        List<Pair<Pointer, Parameter>> selection = JxPathElement.
            toList(context, path, Parameter.class);

        final List<ValidationMessage> messages = emptyRuleHandler.handleRule(
            rule, selection);
        if (!messages.isEmpty()) {
            return messages;
        }

        final List<Pair<Pointer, Parameter>> filteredSelection = cvTermSelectionHandler.
            handleSelection(selection);

        // and logic means that ALL of the defined terms or their children MUST appear
        // we only compare valid CVParameters here, user Params (no cv accession), are not compared!
        // if combination logic is AND, child expansion needs to be disabled to avoid nonsensical combinations
        try {
            RuleEvaluationResult result = ruleHandler.handleRule(rule,
                filteredSelection);

            switch (rule.getCvTermsCombinationLogic()) {
                case AND:
                    messages.addAll(andHandler.handleParameters(result,
                        errorOnTermNotInRule));
                    break;
                case OR: // any of the terms or their children need to appear
                    messages.addAll(orHandler.handleParameters(result,
                        errorOnTermNotInRule));
                    break;
                case XOR:
                    messages.addAll(xorHandler.handleParameters(result,
                        errorOnTermNotInRule));
                    break;
                default:
                    throw new IllegalArgumentException(
                        "Unknown combination logic value: " + rule.
                            getCvTermsCombinationLogic() + " on rule " + CvMappingUtils.
                            niceToString(rule) + "! Supported are: " + Arrays.
                        toString(CvMappingRule.CvTermsCombinationLogic.
                            values()));
            }
            //        messages.addAll(sharedHandler.handleParameters(result, errorOnTermNotInRule));
            messages.addAll(extraHandler.handleParameters(result,
                errorOnTermNotInRule));
        } catch (RuntimeException re) {
            log.error(
                "Caught exception while running semantic validation on rule " + CvMappingUtils.
                    niceToString(rule) + " with selection " + filteredSelection,
                re);
            MZTabError error = new MZTabError(
                CrossCheckErrorType.SemanticValidationException, -1, re.
                    getMessage());
            messages.add(error.toValidationMessage());
        }
        return messages.isEmpty() ? Collections.emptyList() : messages;
    }

}