package org.activiti.bpmn.converter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
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.activiti.bpmn.constants.BpmnXMLConstants;
import org.activiti.bpmn.converter.alfresco.AlfrescoStartEventXMLConverter;
import org.activiti.bpmn.converter.alfresco.AlfrescoUserTaskXMLConverter;
import org.activiti.bpmn.converter.child.DocumentationParser;
import org.activiti.bpmn.converter.child.IOSpecificationParser;
import org.activiti.bpmn.converter.child.MultiInstanceParser;
import org.activiti.bpmn.converter.export.ActivitiListenerExport;
import org.activiti.bpmn.converter.export.BPMNDIExport;
import org.activiti.bpmn.converter.export.CollaborationExport;
import org.activiti.bpmn.converter.export.DataStoreExport;
import org.activiti.bpmn.converter.export.DefinitionsRootExport;
import org.activiti.bpmn.converter.export.MultiInstanceExport;
import org.activiti.bpmn.converter.export.ProcessExport;
import org.activiti.bpmn.converter.export.SignalAndMessageDefinitionExport;
import org.activiti.bpmn.converter.parser.BpmnEdgeParser;
import org.activiti.bpmn.converter.parser.BpmnShapeParser;
import org.activiti.bpmn.converter.parser.DataStoreParser;
import org.activiti.bpmn.converter.parser.DefinitionsParser;
import org.activiti.bpmn.converter.parser.ExtensionElementsParser;
import org.activiti.bpmn.converter.parser.ImportParser;
import org.activiti.bpmn.converter.parser.InterfaceParser;
import org.activiti.bpmn.converter.parser.ItemDefinitionParser;
import org.activiti.bpmn.converter.parser.LaneParser;
import org.activiti.bpmn.converter.parser.MessageFlowParser;
import org.activiti.bpmn.converter.parser.MessageParser;
import org.activiti.bpmn.converter.parser.ParticipantParser;
import org.activiti.bpmn.converter.parser.PotentialStarterParser;
import org.activiti.bpmn.converter.parser.ProcessParser;
import org.activiti.bpmn.converter.parser.ResourceParser;
import org.activiti.bpmn.converter.parser.SignalParser;
import org.activiti.bpmn.converter.parser.SubProcessParser;
import org.activiti.bpmn.converter.util.BpmnXMLUtil;
import org.activiti.bpmn.converter.util.InputStreamProvider;
import org.activiti.bpmn.exceptions.XMLException;
import org.activiti.bpmn.model.Activity;
import org.activiti.bpmn.model.Artifact;
import org.activiti.bpmn.model.Association;
import org.activiti.bpmn.model.BaseElement;
import org.activiti.bpmn.model.BooleanDataObject;
import org.activiti.bpmn.model.BoundaryEvent;
import org.activiti.bpmn.model.BpmnModel;
import org.activiti.bpmn.model.DateDataObject;
import org.activiti.bpmn.model.DoubleDataObject;
import org.activiti.bpmn.model.EventSubProcess;
import org.activiti.bpmn.model.FlowElement;
import org.activiti.bpmn.model.FlowNode;
import org.activiti.bpmn.model.IntegerDataObject;
import org.activiti.bpmn.model.LongDataObject;
import org.activiti.bpmn.model.Pool;
import org.activiti.bpmn.model.Process;
import org.activiti.bpmn.model.SequenceFlow;
import org.activiti.bpmn.model.StringDataObject;
import org.activiti.bpmn.model.SubProcess;
import org.activiti.bpmn.model.TextAnnotation;
import org.activiti.bpmn.model.Transaction;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;


/**
 * @author Tijs Rademakers
 * @author Joram Barrez
 */
public class MyBpmnXMLConverter implements BpmnXMLConstants {

    protected static final Logger LOGGER = LoggerFactory.getLogger(BpmnXMLConverter.class);

    protected static final String BPMN_XSD = "org/activiti/impl/bpmn/parser/BPMN20.xsd";
    protected static final String DEFAULT_ENCODING = "UTF-8";

    protected static Map<String, BaseBpmnXMLConverter> convertersToBpmnMap = new HashMap<String, BaseBpmnXMLConverter>();
    protected static Map<Class<? extends BaseElement>, BaseBpmnXMLConverter> convertersToXMLMap =
            new HashMap<Class<? extends BaseElement>, BaseBpmnXMLConverter>();

    protected ClassLoader classloader;
    protected List<String> userTaskFormTypes;
    protected List<String> startEventFormTypes;

    protected BpmnEdgeParser bpmnEdgeParser = new BpmnEdgeParser();
    protected BpmnShapeParser bpmnShapeParser = new BpmnShapeParser();
    protected DefinitionsParser definitionsParser = new DefinitionsParser();
    protected DocumentationParser documentationParser = new DocumentationParser();
    protected ExtensionElementsParser extensionElementsParser = new ExtensionElementsParser();
    protected ImportParser importParser = new ImportParser();
    protected InterfaceParser interfaceParser = new InterfaceParser();
    protected ItemDefinitionParser itemDefinitionParser = new ItemDefinitionParser();
    protected IOSpecificationParser ioSpecificationParser = new IOSpecificationParser();
    protected DataStoreParser dataStoreParser = new DataStoreParser();
    protected LaneParser laneParser = new LaneParser();
    protected MessageParser messageParser = new MessageParser();
    protected MessageFlowParser messageFlowParser = new MessageFlowParser();
    protected MultiInstanceParser multiInstanceParser = new MultiInstanceParser();
    protected ParticipantParser participantParser = new ParticipantParser();
    protected PotentialStarterParser potentialStarterParser = new PotentialStarterParser();
    protected ProcessParser processParser = new ProcessParser();
    protected ResourceParser resourceParser = new ResourceParser();
    protected SignalParser signalParser = new SignalParser();
    protected SubProcessParser subProcessParser = new SubProcessParser();

    static {
        // events
        addConverter(new EndEventXMLConverter());
        addConverter(new StartEventXMLConverter());

        // tasks
        addConverter(new BusinessRuleTaskXMLConverter());
        addConverter(new ManualTaskXMLConverter());
        addConverter(new ReceiveTaskXMLConverter());
        addConverter(new ScriptTaskXMLConverter());
        addConverter(new ServiceTaskXMLConverter());
        addConverter(new SendTaskXMLConverter());
        addConverter(new UserTaskXMLConverter());
        addConverter(new TaskXMLConverter());
        addConverter(new CallActivityXMLConverter());

        // gateways
        addConverter(new EventGatewayXMLConverter());
        addConverter(new ExclusiveGatewayXMLConverter());
        addConverter(new InclusiveGatewayXMLConverter());
        addConverter(new ParallelGatewayXMLConverter());
        addConverter(new ComplexGatewayXMLConverter());

        // connectors
        addConverter(new SequenceFlowXMLConverter());

        // catch, throw and boundary event
        addConverter(new CatchEventXMLConverter());
        addConverter(new ThrowEventXMLConverter());
        addConverter(new BoundaryEventXMLConverter());

        // artifacts
        addConverter(new TextAnnotationXMLConverter());
        addConverter(new AssociationXMLConverter());

        // data store reference
        addConverter(new DataStoreReferenceXMLConverter());

        // data objects
        addConverter(new ValuedDataObjectXMLConverter(), StringDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), BooleanDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), IntegerDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), LongDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), DoubleDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), DateDataObject.class);

        // Alfresco types
        addConverter(new AlfrescoStartEventXMLConverter());
        addConverter(new AlfrescoUserTaskXMLConverter());
    }

    public static void addConverter(BaseBpmnXMLConverter converter) {
        addConverter(converter, converter.getBpmnElementType());
    }

    public static void addConverter(BaseBpmnXMLConverter converter, Class<? extends BaseElement> elementType) {
        convertersToBpmnMap.put(converter.getXMLElementName(), converter);
        convertersToXMLMap.put(elementType, converter);
    }

    public void setClassloader(ClassLoader classloader) {
        this.classloader = classloader;
    }

    public void setUserTaskFormTypes(List<String> userTaskFormTypes) {
        this.userTaskFormTypes = userTaskFormTypes;
    }

    public void setStartEventFormTypes(List<String> startEventFormTypes) {
        this.startEventFormTypes = startEventFormTypes;
    }

    public void validateModel(InputStreamProvider inputStreamProvider) throws Exception {
        Schema schema = createSchema();

        Validator validator = schema.newValidator();
        validator.validate(new StreamSource(inputStreamProvider.getInputStream()));
    }

    public void validateModel(XMLStreamReader xmlStreamReader) throws Exception {
        Schema schema = createSchema();

        Validator validator = schema.newValidator();
        validator.validate(new StAXSource(xmlStreamReader));
    }

    protected Schema createSchema() throws SAXException {
        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema schema = null;
        if (classloader != null) {
            schema = factory.newSchema(classloader.getResource(BPMN_XSD));
        }

        if (schema == null) {
            schema = factory.newSchema(BpmnXMLConverter.class.getClassLoader().getResource(BPMN_XSD));
        }

        if (schema == null) {
            throw new XMLException("BPMN XSD could not be found");
        }
        return schema;
    }

    public BpmnModel convertToBpmnModel(InputStreamProvider inputStreamProvider, boolean validateSchema, boolean enableSafeBpmnXml) {
        return convertToBpmnModel(inputStreamProvider, validateSchema, enableSafeBpmnXml, DEFAULT_ENCODING);
    }

    public BpmnModel convertToBpmnModel(InputStreamProvider inputStreamProvider, boolean validateSchema, boolean enableSafeBpmnXml, String encoding) {
        XMLInputFactory xif = XMLInputFactory.newInstance();

        if (xif.isPropertySupported(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES)) {
            xif.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
        }

        if (xif.isPropertySupported(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES)) {
            xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
        }

        if (xif.isPropertySupported(XMLInputFactory.SUPPORT_DTD)) {
            xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        }

        InputStreamReader in = null;
        try {
            in = new InputStreamReader(inputStreamProvider.getInputStream(), encoding);
            XMLStreamReader xtr = xif.createXMLStreamReader(in);

            try {
                if (validateSchema) {

                    if (!enableSafeBpmnXml) {
                        validateModel(inputStreamProvider);
                    } else {
                        validateModel(xtr);
                    }

                    // The input stream is closed after schema validation
                    in = new InputStreamReader(inputStreamProvider.getInputStream(), encoding);
                    xtr = xif.createXMLStreamReader(in);
                }

            } catch (Exception e) {
                throw new XMLException(e.getMessage(), e);
            }

            // XML conversion
            return convertToBpmnModel(xtr);
        } catch (UnsupportedEncodingException e) {
            throw new XMLException("The bpmn 2.0 xml is not UTF8 encoded", e);
        } catch (XMLStreamException e) {
            throw new XMLException("Error while reading the BPMN 2.0 XML", e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    LOGGER.debug("Problem closing BPMN input stream", e);
                }
            }
        }
    }

    public BpmnModel convertToBpmnModel(XMLStreamReader xtr) {
        BpmnModel model = new BpmnModel();
        model.setStartEventFormTypes(startEventFormTypes);
        model.setUserTaskFormTypes(userTaskFormTypes);
        try {
            Process activeProcess = null;
            List<SubProcess> activeSubProcessList = new ArrayList<SubProcess>();
            while (xtr.hasNext()) {
                try {
                    xtr.next();
                } catch (Exception e) {
                    LOGGER.debug("Error reading XML document", e);
                    throw new XMLException("Error reading XML", e);
                }

                if (xtr.isEndElement() && ELEMENT_SUBPROCESS.equals(xtr.getLocalName())) {
                    activeSubProcessList.remove(activeSubProcessList.size() - 1);
                }

                if (xtr.isEndElement() && ELEMENT_TRANSACTION.equals(xtr.getLocalName())) {
                    activeSubProcessList.remove(activeSubProcessList.size() - 1);
                }

                if (xtr.isStartElement() == false) {
                    continue;
                }

                if (ELEMENT_DEFINITIONS.equals(xtr.getLocalName())) {
                    definitionsParser.parse(xtr, model);

                } else if (ELEMENT_RESOURCE.equals(xtr.getLocalName())) {
                    resourceParser.parse(xtr, model);

                } else if (ELEMENT_SIGNAL.equals(xtr.getLocalName())) {
                    signalParser.parse(xtr, model);

                } else if (ELEMENT_MESSAGE.equals(xtr.getLocalName())) {
                    messageParser.parse(xtr, model);

                } else if (ELEMENT_ERROR.equals(xtr.getLocalName())) {

                    if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, ATTRIBUTE_ID))) {
                        model.addError(xtr.getAttributeValue(null, ATTRIBUTE_ID),
                                xtr.getAttributeValue(null, ATTRIBUTE_ERROR_CODE));
                    }

                } else if (ELEMENT_IMPORT.equals(xtr.getLocalName())) {
                    importParser.parse(xtr, model);

                } else if (ELEMENT_ITEM_DEFINITION.equals(xtr.getLocalName())) {
                    itemDefinitionParser.parse(xtr, model);

                } else if (ELEMENT_DATA_STORE.equals(xtr.getLocalName())) {
                    dataStoreParser.parse(xtr, model);

                } else if (ELEMENT_INTERFACE.equals(xtr.getLocalName())) {
                    interfaceParser.parse(xtr, model);

                } else if (ELEMENT_IOSPECIFICATION.equals(xtr.getLocalName())) {
                    ioSpecificationParser.parseChildElement(xtr, activeProcess, model);

                } else if (ELEMENT_PARTICIPANT.equals(xtr.getLocalName())) {
                    participantParser.parse(xtr, model);

                } else if (ELEMENT_MESSAGE_FLOW.equals(xtr.getLocalName())) {
                    messageFlowParser.parse(xtr, model);

                } else if (ELEMENT_PROCESS.equals(xtr.getLocalName())) {

                    Process process = processParser.parse(xtr, model);
                    if (process != null) {
                        activeProcess = process;
                    }

                } else if (ELEMENT_POTENTIAL_STARTER.equals(xtr.getLocalName())) {
                    potentialStarterParser.parse(xtr, activeProcess);

                } else if (ELEMENT_LANE.equals(xtr.getLocalName())) {
                    laneParser.parse(xtr, activeProcess, model);

                } else if (ELEMENT_DOCUMENTATION.equals(xtr.getLocalName())) {

                    BaseElement parentElement = null;
                    if (!activeSubProcessList.isEmpty()) {
                        parentElement = activeSubProcessList.get(activeSubProcessList.size() - 1);
                    } else if (activeProcess != null) {
                        parentElement = activeProcess;
                    }
                    documentationParser.parseChildElement(xtr, parentElement, model);

                } else if (activeProcess == null && ELEMENT_TEXT_ANNOTATION.equals(xtr.getLocalName())) {
                    String elementId = xtr.getAttributeValue(null, ATTRIBUTE_ID);
                    TextAnnotation textAnnotation = (TextAnnotation) new TextAnnotationXMLConverter().convertXMLToElement(xtr, model);
                    textAnnotation.setId(elementId);
                    model.getGlobalArtifacts().add(textAnnotation);

                } else if (activeProcess == null && ELEMENT_ASSOCIATION.equals(xtr.getLocalName())) {
                    String elementId = xtr.getAttributeValue(null, ATTRIBUTE_ID);
                    Association association = (Association) new AssociationXMLConverter().convertXMLToElement(xtr, model);
                    association.setId(elementId);
                    model.getGlobalArtifacts().add(association);
                } else if (ELEMENT_EXTENSIONS.equals(xtr.getLocalName())) {
                    extensionElementsParser.parse(xtr, activeSubProcessList, activeProcess, model);

                } else if (ELEMENT_SUBPROCESS.equals(xtr.getLocalName())) {
                    subProcessParser.parse(xtr, activeSubProcessList, activeProcess);

                } else if (ELEMENT_TRANSACTION.equals(xtr.getLocalName())) {
                    subProcessParser.parse(xtr, activeSubProcessList, activeProcess);

                } else if (ELEMENT_DI_SHAPE.equals(xtr.getLocalName())) {
                    bpmnShapeParser.parse(xtr, model);

                } else if (ELEMENT_DI_EDGE.equals(xtr.getLocalName())) {
                    bpmnEdgeParser.parse(xtr, model);

                } else {

                    if (!activeSubProcessList.isEmpty() && ELEMENT_MULTIINSTANCE.equalsIgnoreCase(xtr.getLocalName())) {

                        multiInstanceParser.parseChildElement(xtr, activeSubProcessList.get(activeSubProcessList.size() - 1), model);

                    } else if (convertersToBpmnMap.containsKey(xtr.getLocalName())) {
                        if (activeProcess != null) {
                            BaseBpmnXMLConverter converter = convertersToBpmnMap.get(xtr.getLocalName());
                            converter.convertToBpmnModel(xtr, model, activeProcess, activeSubProcessList);
                        }
                    }
                }
            }

            for (Process process : model.getProcesses()) {
                for (Pool pool : model.getPools()) {
                    if (process.getId().equals(pool.getProcessRef())) {
                        pool.setExecutable(process.isExecutable());
                    }
                }
                processFlowElements(process.getFlowElements(), process);
            }

        } catch (XMLException e) {
            throw e;

        } catch (Exception e) {
            LOGGER.error("Error processing BPMN document", e);
            throw new XMLException("Error processing BPMN document", e);
        }
        return model;
    }

    private void processFlowElements(Collection<FlowElement> flowElementList, BaseElement parentScope) {
        for (FlowElement flowElement : flowElementList) {
            if (flowElement instanceof SequenceFlow) {
                SequenceFlow sequenceFlow = (SequenceFlow) flowElement;
                FlowNode sourceNode = getFlowNodeFromScope(sequenceFlow.getSourceRef(), parentScope);
                if (sourceNode != null) {
                    sourceNode.getOutgoingFlows().add(sequenceFlow);
                }
                FlowNode targetNode = getFlowNodeFromScope(sequenceFlow.getTargetRef(), parentScope);
                if (targetNode != null) {
                    targetNode.getIncomingFlows().add(sequenceFlow);
                }
            } else if (flowElement instanceof BoundaryEvent) {
                BoundaryEvent boundaryEvent = (BoundaryEvent) flowElement;
                FlowElement attachedToElement = getFlowNodeFromScope(boundaryEvent.getAttachedToRefId(), parentScope);
                if (attachedToElement != null) {
                    boundaryEvent.setAttachedToRef((Activity) attachedToElement);
                    ((Activity) attachedToElement).getBoundaryEvents().add(boundaryEvent);
                }
            } else if (flowElement instanceof SubProcess) {
                SubProcess subProcess = (SubProcess) flowElement;
                processFlowElements(subProcess.getFlowElements(), subProcess);
            }
        }
    }

    private FlowNode getFlowNodeFromScope(String elementId, BaseElement scope) {
        FlowNode flowNode = null;
        if (StringUtils.isNotEmpty(elementId)) {
            if (scope instanceof Process) {
                flowNode = (FlowNode) ((Process) scope).getFlowElement(elementId);
            } else if (scope instanceof SubProcess) {
                flowNode = (FlowNode) ((SubProcess) scope).getFlowElement(elementId);
            }
        }
        return flowNode;
    }

    public byte[] convertToXML(BpmnModel model) {
        return convertToXML(model, DEFAULT_ENCODING);
    }

    public byte[] convertToXML(BpmnModel model, String encoding) {
        try {

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

            XMLOutputFactory xof = XMLOutputFactory.newInstance();
            OutputStreamWriter out = new OutputStreamWriter(outputStream, encoding);

            XMLStreamWriter writer = xof.createXMLStreamWriter(out);
            XMLStreamWriter xtw = new IndentingXMLStreamWriter(writer);

            DefinitionsRootExport.writeRootElement(model, xtw, encoding);
            CollaborationExport.writePools(model, xtw);
            DataStoreExport.writeDataStores(model, xtw);
            SignalAndMessageDefinitionExport.writeSignalsAndMessages(model, xtw);

            for (Process process : model.getProcesses()) {

                if (process.getFlowElements().isEmpty() && process.getLanes().isEmpty()) {
                    // empty process, ignore it
                    continue;
                }

                ProcessExport.writeProcess(process, xtw);

                for (FlowElement flowElement : process.getFlowElements()) {
                    createXML(flowElement, model, xtw);
                }

                for (Artifact artifact : process.getArtifacts()) {
                    createXML(artifact, model, xtw);
                }

                // end process element
                xtw.writeEndElement();
            }

            BPMNDIExport.writeBPMNDI(model, xtw);

            // end definitions root element
            xtw.writeEndElement();
            xtw.writeEndDocument();

            xtw.flush();

            outputStream.close();

            xtw.close();

            return outputStream.toByteArray();

        } catch (Exception e) {
            LOGGER.error("Error writing BPMN XML", e);
            throw new XMLException("Error writing BPMN XML", e);
        }
    }

    private void createXML(FlowElement flowElement, BpmnModel model, XMLStreamWriter xtw) throws Exception {

        if (flowElement instanceof SubProcess) {

            SubProcess subProcess = (SubProcess) flowElement;
            if (flowElement instanceof Transaction) {
                xtw.writeStartElement(ELEMENT_TRANSACTION);
            } else {
                xtw.writeStartElement(ELEMENT_SUBPROCESS);
            }

            xtw.writeAttribute(ATTRIBUTE_ID, subProcess.getId());
            if (StringUtils.isNotEmpty(subProcess.getName())) {
                xtw.writeAttribute(ATTRIBUTE_NAME, subProcess.getName());
            } else {
                xtw.writeAttribute(ATTRIBUTE_NAME, "subProcess");
            }

            if (subProcess instanceof EventSubProcess) {
                xtw.writeAttribute(ATTRIBUTE_TRIGGERED_BY, ATTRIBUTE_VALUE_TRUE);

            } else if (subProcess instanceof Transaction == false) {
                if (subProcess.isAsynchronous()) {
                    BpmnXMLUtil.writeQualifiedAttribute(ATTRIBUTE_ACTIVITY_ASYNCHRONOUS, ATTRIBUTE_VALUE_TRUE, xtw);
                    if (subProcess.isNotExclusive()) {
                        BpmnXMLUtil.writeQualifiedAttribute(ATTRIBUTE_ACTIVITY_EXCLUSIVE, ATTRIBUTE_VALUE_FALSE, xtw);
                    }
                }
            }

            if (StringUtils.isNotEmpty(subProcess.getDocumentation())) {

                xtw.writeStartElement(ELEMENT_DOCUMENTATION);
                xtw.writeCharacters(subProcess.getDocumentation());
                xtw.writeEndElement();
            }

            boolean didWriteExtensionStartElement = ActivitiListenerExport.writeListeners(subProcess, false, xtw);

            didWriteExtensionStartElement = BpmnXMLUtil.writeExtensionElements(subProcess, didWriteExtensionStartElement, model.getNamespaces(), xtw);
            if (didWriteExtensionStartElement) {
                // closing extensions element
                xtw.writeEndElement();
            }

            MultiInstanceExport.writeMultiInstance(subProcess, xtw);

            for (FlowElement subElement : subProcess.getFlowElements()) {
                createXML(subElement, model, xtw);
            }

            for (Artifact artifact : subProcess.getArtifacts()) {
                createXML(artifact, model, xtw);
            }

            xtw.writeEndElement();

        } else {

            BaseBpmnXMLConverter converter = convertersToXMLMap.get(flowElement.getClass());

            if (converter == null) {
                throw new XMLException("No converter for " + flowElement.getClass() + " found");
            }

            converter.convertToXML(xtw, flowElement, model);
        }
    }

    private void createXML(Artifact artifact, BpmnModel model, XMLStreamWriter xtw) throws Exception {

        BaseBpmnXMLConverter converter = convertersToXMLMap.get(artifact.getClass());

        if (converter == null) {
            throw new XMLException("No converter for " + artifact.getClass() + " found");
        }

        converter.convertToXML(xtw, artifact, model);
    }
}