001// SPDX-License-Identifier: GPL-3.0-or-later
002
003package es.uvigo.esei.sing.textproc.process;
004
005import java.io.InputStream;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.List;
011import java.util.Map;
012
013import javax.xml.XMLConstants;
014import javax.xml.bind.JAXBContext;
015import javax.xml.bind.JAXBException;
016import javax.xml.bind.Unmarshaller;
017import javax.xml.bind.ValidationEvent;
018import javax.xml.bind.ValidationEventHandler;
019import javax.xml.transform.stream.StreamSource;
020import javax.xml.validation.SchemaFactory;
021import javax.xml.validation.SchemaFactoryConfigurationError;
022
023import org.xml.sax.ErrorHandler;
024import org.xml.sax.SAXException;
025import org.xml.sax.SAXParseException;
026
027import es.uvigo.esei.sing.textproc.process.xml.definition.ProcessingProcessDefinition;
028import es.uvigo.esei.sing.textproc.step.ProcessingException;
029import es.uvigo.esei.sing.textproc.step.ProcessingStepService;
030import es.uvigo.esei.sing.textproc.step.ProcessingStepServices;
031import es.uvigo.esei.sing.textproc.step.xml.definition.ProcessingStepDefinition;
032import es.uvigo.esei.sing.textproc.step.xml.definition.ProcessingStepParameter;
033import lombok.NonNull;
034
035/**
036 * This class represents a processing process, defined by a XML document, and is
037 * responsible for parsing and executing it.
038 *
039 * @author Alejandro González García
040 * @implNote The implementation of this class is thread-safe.
041 */
042public final class ProcessingProcess {
043        private static final String PROCESS_DECLARATION_XSD_RESOURCE = "/process_definition.xsd";
044
045        /**
046         * Parses and executes the process declaration defined in the given input
047         * stream. This method doesn't return until all processes were executed.
048         *
049         * @param declarationInput The input stream which contains the process
050         *                         declaration, in XML.
051         * @throws ProcessingException      If an exception occurs during parsing or
052         *                                  execution.
053         * @throws IllegalArgumentException If {@code declarationInput} is {@code null}.
054         */
055        public void executeProcessDeclaration(@NonNull final InputStream declarationInput) throws ProcessingException {
056                ProcessingProcessDefinition processDefinition;
057
058                // Unmarshall the process definition
059                try {
060                        // The required JAXB context includes the process definition itself, and
061                        // any parameter definition provided by processing step services
062                        final Collection<Class<?>> jaxbContextClasses = new HashSet<>();
063                        jaxbContextClasses.add(ProcessingProcessDefinition.class);
064
065                        for (final ProcessingStepService stepService : ProcessingStepServices.getServiceLoader()) {
066                                jaxbContextClasses.addAll(stepService.getAdditionalParameters());
067                        }
068
069                        final Unmarshaller jaxbUnmarshaller = JAXBContext.newInstance(
070                                jaxbContextClasses.toArray(new Class<?>[jaxbContextClasses.size()])
071                        ).createUnmarshaller();
072
073                        final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
074                        schemaFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); // Limit resource usage
075
076                        // Treat warnings during unmarshalling as errors
077                        schemaFactory.setErrorHandler(new ErrorHandler() {
078                                @Override
079                                public void warning(final SAXParseException exception) throws SAXException {
080                                        throw exception;
081                                }
082
083                                @Override
084                                public void error(final SAXParseException exception) throws SAXException {
085                                        throw exception;
086                                }
087
088                                @Override
089                                public void fatalError(final SAXParseException exception) throws SAXException {
090                                        throw exception;
091                                }
092                        });
093
094                        jaxbUnmarshaller.setEventHandler(new ValidationEventHandler() {
095                                @Override
096                                public boolean handleEvent(final ValidationEvent event) {
097                                        return false;
098                                }
099                        });
100
101                        jaxbUnmarshaller.setSchema(
102                                SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(
103                                        ProcessingProcess.class.getResource(PROCESS_DECLARATION_XSD_RESOURCE)
104                                )
105                        );
106
107                        processDefinition = jaxbUnmarshaller.unmarshal(
108                                new StreamSource(declarationInput), ProcessingProcessDefinition.class
109                        ).getValue();
110
111                        assert processDefinition != null : "The unmarshalled process definition element can't be null";
112                } catch (final SchemaFactoryConfigurationError | UnsupportedOperationException | JAXBException | SAXException exc) {
113                        throw new ProcessingException(
114                                "An exception occurred while reading the process declaration", exc
115                        );
116                }
117
118                // We have the process definition loaded to a object graph. Execute each step in order
119                for (final ProcessingStepDefinition stepDefinition : processDefinition.getProcessingSteps()) {
120                        Map<String, String> parametersMap;
121                        final List<ProcessingStepParameter> parameters = stepDefinition.getParameters();
122
123                        // Convert parameters in list to a map
124                        parametersMap = new HashMap<>(
125                                (int) Math.ceil(parameters.size() / 0.75)
126                        );
127                        for (final ProcessingStepParameter parameter : parameters) {
128                                parametersMap.put(parameter.getName(), parameter.getValue());
129                        }
130
131                        stepDefinition.getAction().execute(Collections.unmodifiableMap(parametersMap));
132                }
133        }
134}