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}