/* * Copyright 2017 Graz University of Technology EAAF-Core Components has been developed in a * cooperation between EGIZ, A-SIT Plus, A-SIT, and Graz University of Technology. * * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European * Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work except in * compliance with the Licence. You may obtain a copy of the Licence at: * https://joinup.ec.europa.eu/news/understanding-eupl-v12 * * Unless required by applicable law or agreed to in writing, software distributed under the Licence * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the Licence for the specific language governing permissions and limitations under * the Licence. * * This product combines work with different licenses. See the "NOTICE" text file for details on the * various modules and licenses. The "NOTICE" text file is part of the distribution. Any derivative * works that you distribute must include a readable copy of the "NOTICE" text file. */ package at.gv.egiz.eaaf.core.impl.idp.process; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.events.Attribute; import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; import javax.xml.stream.util.EventReaderDelegate; import javax.xml.transform.stax.StAXSource; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; import at.gv.egiz.eaaf.core.impl.idp.process.model.EndEvent; import at.gv.egiz.eaaf.core.impl.idp.process.model.ProcessDefinition; import at.gv.egiz.eaaf.core.impl.idp.process.model.ProcessNode; import at.gv.egiz.eaaf.core.impl.idp.process.model.StartEvent; import at.gv.egiz.eaaf.core.impl.idp.process.model.TaskInfo; import at.gv.egiz.eaaf.core.impl.idp.process.model.Transition; /** * Parses an XML representation of a process definition as defined by the * respective XML schema. The parser is thread-safe. * * @author tknall * */ public class ProcessDefinitionParser { private static final String NS = "http://reference.e-government.gv.at/namespace/moa/process/definition/v1"; private static Logger log = LoggerFactory.getLogger(ProcessDefinitionParser.class); private static class LazyProcessDefinitionSchemaHolder { private static final Schema PD_SCHEMA_INSTANCE; static { try (InputStream in = ProcessDefinitionParser.class.getResourceAsStream("/process/ProcessDefinition.xsd")) { log.trace("Compiling process definition schema."); final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); // schema is thread-safe PD_SCHEMA_INSTANCE = factory.newSchema(new StreamSource(in)); } catch (final Exception e) { throw new RuntimeException("Unable to compile process definition schema.", e); } } } /** * Parses an XML representation of a process definition. The representation is * being validated in order to suffice the related XML schema. * * @param processDefinitionInputStream The process definition. * @return A new process definition. * @throws ProcessDefinitionParserException Thrown in case of error parsing the * process definition. */ public ProcessDefinition parse(final InputStream processDefinitionInputStream) throws ProcessDefinitionParserException { XMLEventReader reader = null; final ProcessDefinition pd = new ProcessDefinition(); log.debug("Parsing and validating process definition."); try { // Standard implementation of XMLInputFactory seems not to be thread-safe final XMLInputFactory inputFactory = XMLInputFactory.newInstance(); reader = inputFactory.createXMLEventReader(processDefinitionInputStream); final List transitionElements = new ArrayList<>(); final List startEvents = new ArrayList<>(); reader = new EventReaderDelegate(reader) { @Override public XMLEvent nextEvent() throws XMLStreamException { final XMLEvent event = super.nextEvent(); switch (event.getEventType()) { case XMLStreamConstants.START_ELEMENT: final StartElement element = event.asStartElement(); final QName qname = element.getName(); if (NS.equals(qname.getNamespaceURI())) { log.trace("Found process description element '{}'.", qname.getLocalPart()); final Attribute id = element.getAttributeByName(new QName("id")); switch (qname.getLocalPart()) { case "ProcessDefinition": if (id != null) { pd.setId(id.getValue()); } break; case "StartEvent": final StartEvent startEvent = new StartEvent(); if (id != null) { startEvent.setId(id.getValue()); } startEvents.add(startEvent); break; case "EndEvent": final EndEvent endEvent = new EndEvent(); if (id != null) { endEvent.setId(id.getValue()); pd.getEndEvents().put(id.getValue(), endEvent); } break; case "Transition": transitionElements.add(element); break; case "Task": final TaskInfo taskInfo = new TaskInfo(); if (id != null) { taskInfo.setId(id.getValue()); pd.getTaskInfos().put(id.getValue(), taskInfo); } final Attribute async = element.getAttributeByName(new QName("async")); if (async != null) { taskInfo.setAsync(Boolean.valueOf(async.getValue())); } final Attribute implementingClass = element.getAttributeByName(new QName("class")); if (implementingClass != null) { taskInfo.setTaskImplementingClass(implementingClass.getValue()); } break; default: log.info("Ignore unknown event: {}", qname); break; } } break; default: log.trace("Ignore unknown event: {}", event); break; } return event; } }; // validator is not thread-safe final Validator validator = LazyProcessDefinitionSchemaHolder.PD_SCHEMA_INSTANCE.newValidator(); validator.validate(new StAXSource(reader)); log.trace("Process definition successfully schema validated."); // perform some basic checks log.trace("Building model and performing some plausibility checks."); if (startEvents.size() != 1) { throw new ProcessDefinitionParserException( "A ProcessDefinition must contain exactly one single StartEvent."); } pd.setStartEvent(startEvents.get(0)); // link transitions final Iterator transitions = transitionElements.iterator(); while (transitions.hasNext()) { final StartElement element = transitions.next(); final Transition transition = new Transition(); final Attribute id = element.getAttributeByName(new QName("id")); if (id != null) { transition.setId(id.getValue()); } final Attribute conditionExpression = element.getAttributeByName(new QName("conditionExpression")); if (conditionExpression != null) { transition.setConditionExpression(conditionExpression.getValue()); } final Attribute from = element.getAttributeByName(new QName("from")); if (from != null) { final ProcessNode fromNode = pd.getProcessNode(from.getValue()); if (fromNode == null) { throw new ProcessDefinitionParserException( "Transition's 'from'-attribute refers to a non-existing event or task '" + from.getValue() + '.'); } if (fromNode instanceof EndEvent) { throw new ProcessDefinitionParserException("Transition cannot start from end event."); } transition.setFrom(fromNode); fromNode.getOutgoingTransitions().add(transition); } final Attribute to = element.getAttributeByName(new QName("to")); if (to != null) { final ProcessNode toNode = pd.getProcessNode(to.getValue()); if (toNode == null) { throw new ProcessDefinitionParserException( "Transition's 'to'-attribute refers to a non-existing event or task '" + to.getValue() + '.'); } transition.setTo(toNode); toNode.getIncomingTransitions().add(transition); } if (transition.getConditionExpression() == null && Objects.equals(transition.getFrom(), transition.getTo())) { throw new ProcessDefinitionParserException( "Transition's 'from' equals its 'to'. Since no 'conditionExpression' " + "has been set this will cause a loop."); } } log.debug("Process definition '{}' successfully parsed.", pd.getId()); return pd; } catch (final ProcessDefinitionParserException e) { throw e; } catch (XMLStreamException | IOException e) { throw new ProcessDefinitionParserException( "Unable to read process definition from inputstream.", e); } catch (final SAXException e) { throw new ProcessDefinitionParserException("Schema validation of process description failed.", e); } catch (final Exception e) { throw new ProcessDefinitionParserException( "Internal error creating process definition from inputstream.", e); } finally { if (reader != null) { try { reader.close(); } catch (final XMLStreamException e) { e.printStackTrace(); } } } } }