org.flowable.cmmn.converter.CmmnXmlConverter.java Source code

Java tutorial

Introduction

Here is the source code for org.flowable.cmmn.converter.CmmnXmlConverter.java

Source

/* 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 org.flowable.cmmn.converter;

import org.apache.commons.lang3.StringUtils;
import org.flowable.cmmn.converter.exception.XMLException;
import org.flowable.cmmn.converter.export.CaseExport;
import org.flowable.cmmn.converter.export.CmmnDIExport;
import org.flowable.cmmn.converter.export.DefinitionsRootExport;
import org.flowable.cmmn.converter.export.StageExport;
import org.flowable.cmmn.model.Association;
import org.flowable.cmmn.model.BaseElement;
import org.flowable.cmmn.model.Case;
import org.flowable.cmmn.model.CaseElement;
import org.flowable.cmmn.model.CmmnDiEdge;
import org.flowable.cmmn.model.CmmnDiShape;
import org.flowable.cmmn.model.CmmnModel;
import org.flowable.cmmn.model.Criterion;
import org.flowable.cmmn.model.DecisionTask;
import org.flowable.cmmn.model.HasEntryCriteria;
import org.flowable.cmmn.model.HasExitCriteria;
import org.flowable.cmmn.model.PlanFragment;
import org.flowable.cmmn.model.PlanItem;
import org.flowable.cmmn.model.PlanItemDefinition;
import org.flowable.cmmn.model.ProcessTask;
import org.flowable.cmmn.model.Sentry;
import org.flowable.cmmn.model.SentryOnPart;
import org.flowable.cmmn.model.Stage;
import org.flowable.cmmn.model.Task;
import org.flowable.cmmn.model.TimerEventListener;
import org.flowable.engine.common.api.FlowableException;
import org.flowable.engine.common.api.io.InputStreamProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author Joram Barrez
 */
public class CmmnXmlConverter implements CmmnXmlConstants {

    protected static final Logger LOGGER = LoggerFactory.getLogger(CmmnXmlConverter.class);
    protected static final String XSD_LOCATION = "org/flowable/impl/cmmn/parser/CMMN11.xsd";
    protected static final String DEFAULT_ENCODING = "UTF-8";

    protected static Map<String, BaseCmmnXmlConverter> elementConverters = new HashMap<>();
    protected static Map<String, BaseCmmnXmlConverter> textConverters = new HashMap<>();

    protected ClassLoader classloader;

    static {
        addElementConverter(new DefinitionsXmlConverter());
        addElementConverter(new DocumentationXmlConverter());
        addElementConverter(new CaseXmlConverter());
        addElementConverter(new PlanModelXmlConverter());
        addElementConverter(new StageXmlConverter());
        addElementConverter(new MilestoneXmlConverter());
        addElementConverter(new TaskXmlConverter());
        addElementConverter(new HumanTaskXmlConverter());
        addElementConverter(new PlanItemXmlConverter());
        addElementConverter(new ItemControlXmlConverter());
        addElementConverter(new DefaultControlXmlConverter());
        addElementConverter(new RepetitionRuleXmlConverter());
        addElementConverter(new SentryXmlConverter());
        addElementConverter(new EntryCriterionXmlConverter());
        addElementConverter(new ExitCriterionXmlConverter());
        addElementConverter(new PlanItemOnPartXmlConverter());
        addElementConverter(new SentryIfPartXmlConverter());
        addElementConverter(new CaseTaskXmlConverter());
        addElementConverter(new ProcessXmlConverter());
        addElementConverter(new ProcessTaskXmlConverter());
        addElementConverter(new DecisionXmlConverter());
        addElementConverter(new DecisionTaskXmlConverter());
        addElementConverter(new TimerEventListenerXmlConverter());
        addElementConverter(new PlanItemStartTriggerXmlConverter());
        addElementConverter(new CmmnDiShapeXmlConverter());
        addElementConverter(new CmmnDiEdgeXmlConverter());
        addElementConverter(new CmmnDiBoundsXmlConverter());
        addElementConverter(new CmmnDiWaypointXmlConverter());

        addElementConverter(new FieldExtensionXmlConverter());
        addElementConverter(new FlowableHttpResponseHandlerXmlConverter());
        addElementConverter(new FlowableHttpRequestHandlerXmlConverter());

        addTextConverter(new StandardEventXmlConverter());
        addTextConverter(new ProcessRefExpressionXmlConverter());
        addTextConverter(new DecisionRefExpressionXmlConverter());
        addTextConverter(new ConditionXmlConverter());
        addTextConverter(new TimerExpressionXmlConverter());
    }

    public static void addElementConverter(BaseCmmnXmlConverter converter) {
        elementConverters.put(converter.getXMLElementName(), converter);
    }

    public static void addTextConverter(BaseCmmnXmlConverter converter) {
        textConverters.put(converter.getXMLElementName(), converter);
    }

    public CmmnModel convertToCmmnModel(InputStreamProvider inputStreamProvider) {
        return convertToCmmnModel(inputStreamProvider, true, true);
    }

    public CmmnModel convertToCmmnModel(InputStreamProvider inputStreamProvider, boolean validateSchema,
            boolean enableSafeBpmnXml) {
        return convertToCmmnModel(inputStreamProvider, validateSchema, enableSafeBpmnXml, DEFAULT_ENCODING);
    }

    public CmmnModel convertToCmmnModel(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);
        }

        if (encoding == null) {
            encoding = DEFAULT_ENCODING;
        }

        if (validateSchema) {
            try (InputStreamReader in = new InputStreamReader(inputStreamProvider.getInputStream(), encoding)) {
                if (!enableSafeBpmnXml) {
                    validateModel(inputStreamProvider);
                } else {
                    validateModel(xif.createXMLStreamReader(in));
                }
            } catch (UnsupportedEncodingException e) {
                throw new CmmnXMLException("The CMMN 1.1 xml is not properly encoded", e);
            } catch (XMLStreamException e) {
                throw new CmmnXMLException("Error while reading the CMMN 1.1 XML", e);
            } catch (Exception e) {
                throw new CmmnXMLException(e.getMessage(), e);
            }
        }
        // The input stream is closed after schema validation
        try (InputStreamReader in = new InputStreamReader(inputStreamProvider.getInputStream(), encoding)) {
            // XML conversion
            return convertToCmmnModel(xif.createXMLStreamReader(in));
        } catch (UnsupportedEncodingException e) {
            throw new CmmnXMLException("The CMMN 1.1 xml is not properly encoded", e);
        } catch (XMLStreamException e) {
            throw new CmmnXMLException("Error while reading the CMMN 1.1 XML", e);
        } catch (IOException e) {
            throw new CmmnXMLException(e.getMessage(), e);
        }
    }

    public CmmnModel convertToCmmnModel(XMLStreamReader xtr) {

        ConversionHelper conversionHelper = new ConversionHelper();
        conversionHelper.setCmmnModel(new CmmnModel());

        try {
            String currentXmlElement = null;
            while (xtr.hasNext()) {
                try {
                    xtr.next();
                } catch (Exception e) {
                    LOGGER.debug("Error reading CMMN XML document", e);
                    throw new CmmnXMLException("Error reading XML", e);
                }

                if (xtr.isStartElement()) {
                    currentXmlElement = xtr.getLocalName();
                    if (elementConverters.containsKey(currentXmlElement)) {
                        elementConverters.get(currentXmlElement).convertToCmmnModel(xtr, conversionHelper);
                    }

                } else if (xtr.isEndElement()) {
                    currentXmlElement = null;
                    if (elementConverters.containsKey(xtr.getLocalName())) {
                        elementConverters.get(xtr.getLocalName()).elementEnd(xtr, conversionHelper);
                    }

                } else if (xtr.isCharacters() && currentXmlElement != null) {
                    if (textConverters.containsKey(currentXmlElement)) {
                        textConverters.get(currentXmlElement).convertToCmmnModel(xtr, conversionHelper);
                    }

                }
            }

        } catch (CmmnXMLException e) {
            throw e;

        } catch (Exception e) {
            LOGGER.error("Error processing CMMN XML document", e);
            throw new CmmnXMLException("Error processing CMMN XML document", e);
        }

        // Post process all elements: add instances where a reference is used
        processCmmnElements(conversionHelper);
        return conversionHelper.getCmmnModel();
    }

    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(XSD_LOCATION));
        }

        if (schema == null) {
            schema = factory.newSchema(this.getClass().getClassLoader().getResource(XSD_LOCATION));
        }

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

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

    public byte[] convertToXML(CmmnModel 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);

            for (Case caseModel : model.getCases()) {

                if (caseModel.getPlanModel().getPlanItems().isEmpty()) {
                    // empty case, ignore it
                    continue;
                }

                CaseExport.writeCase(caseModel, xtw);

                Stage planModel = caseModel.getPlanModel();
                StageExport.writeStage(planModel, xtw);

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

            CmmnDIExport.writeCmmnDI(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 CMMN XML", e);
            throw new XMLException("Error writing CMMN XML", e);
        }
    }

    protected void processCmmnElements(ConversionHelper conversionHelper) {
        CmmnModel cmmnModel = conversionHelper.getCmmnModel();
        for (Case caze : cmmnModel.getCases()) {
            processPlanFragment(cmmnModel, caze.getPlanModel());
        }

        // CMMN doesn't mandate ids on many elements ... adding generated ids
        // to those elements as this makes the logic much easier
        ensureIds(conversionHelper.getPlanFragments(), "planFragment_");
        ensureIds(conversionHelper.getStages(), "stage_");
        ensureIds(conversionHelper.getEntryCriteria(), "entryCriterion_");
        ensureIds(conversionHelper.getExitCriteria(), "exitCriterion_");
        ensureIds(conversionHelper.getSentries(), "sentry_");
        ensureIds(conversionHelper.getSentryOnParts(), "onPart_");
        ensureIds(conversionHelper.getSentryIfParts(), "ifPart_");
        ensureIds(conversionHelper.getPlanItems(), "planItem_");
        ensureIds(conversionHelper.getPlanItemDefinitions(), "planItemDefinition_");

        // Now everything has an id, the map of all case elements can be filled
        for (Case caze : cmmnModel.getCases()) {
            for (CaseElement caseElement : conversionHelper.getCaseElements().get(caze)) {
                caze.getAllCaseElements().put(caseElement.getId(), caseElement);
            }
        }

        // set DI elements
        for (CmmnDiShape diShape : conversionHelper.getDiShapes()) {
            cmmnModel.addGraphicInfo(diShape.getCmmnElementRef(), diShape.getGraphicInfo());
        }

        for (CmmnDiEdge diEdge : conversionHelper.getDiEdges()) {
            Association association = new Association();
            association.setId(diEdge.getId());
            association.setSourceRef(diEdge.getCmmnElementRef());
            association.setTargetRef(diEdge.getTargetCmmnElementRef());

            String planItemSourceRef = null;
            PlanItem planItem = cmmnModel.findPlanItem(association.getSourceRef());
            if (planItem == null) {
                planItem = cmmnModel.findPlanItem(association.getTargetRef());
                planItemSourceRef = association.getTargetRef();
            } else {
                planItemSourceRef = association.getSourceRef();
            }

            if (planItem != null) {
                for (Criterion criterion : planItem.getEntryCriteria()) {
                    Sentry sentry = criterion.getSentry();
                    if (sentry.getOnParts().size() > 0) {
                        SentryOnPart sentryOnPart = sentry.getOnParts().get(0);
                        if (planItemSourceRef.equals(sentryOnPart.getSourceRef())) {
                            association.setTransitionEvent(sentryOnPart.getStandardEvent());
                        }
                    }
                }
            }

            cmmnModel.addAssociation(association);

            cmmnModel.addFlowGraphicInfoList(association.getId(), diEdge.getWaypoints());
        }
    }

    protected void processPlanFragment(CmmnModel cmmnModel, PlanFragment planFragment) {
        processPlanItems(cmmnModel, planFragment);
        processSentries(cmmnModel.getPrimaryCase().getPlanModel(), planFragment);

        if (planFragment instanceof Stage) {
            Stage stage = (Stage) planFragment;
            for (PlanItemDefinition planItemDefinition : stage.getPlanItemDefinitions()) {
                if (planItemDefinition instanceof PlanFragment) {
                    processPlanFragment(cmmnModel, (PlanFragment) planItemDefinition);
                }
            }

            if (!stage.getExitCriteria().isEmpty()) {
                resolveExitCriteriaSentry(stage);
            }
        }
    }

    protected void processPlanItems(CmmnModel cmmnModel, PlanFragment planFragment) {
        for (PlanItem planItem : planFragment.getPlanItems()) {

            Stage parentStage = planItem.getParentStage();
            PlanItemDefinition planItemDefinition = parentStage.findPlanItemDefinition(planItem.getDefinitionRef());
            if (planItemDefinition == null) {
                throw new FlowableException("No matching plan item definition found for reference "
                        + planItem.getDefinitionRef() + " of plan item " + planItem.getId());
            }
            planItem.setPlanItemDefinition(planItemDefinition);

            if (!planItem.getEntryCriteria().isEmpty()) {
                resolveEntryCriteria(planItem);
            }

            if (!planItem.getExitCriteria().isEmpty()) {
                boolean exitCriteriaAllowed = true;
                if (planItemDefinition instanceof Task) {
                    Task task = (Task) planItemDefinition;
                    if (!task.isBlocking() && StringUtils.isEmpty(task.getBlockingExpression())) {
                        exitCriteriaAllowed = false;
                    }
                }

                if (exitCriteriaAllowed) {
                    resolveExitCriteriaSentry(planItem);
                } else {
                    LOGGER.warn("Ignoring exit criteria on plan item {}", planItem.getId());
                    planItem.getExitCriteria().clear();
                }
            }

            if (planItemDefinition instanceof PlanFragment) {
                processPlanFragment(cmmnModel, (PlanFragment) planItemDefinition);

            } else if (planItemDefinition instanceof ProcessTask) {
                ProcessTask processTask = (ProcessTask) planItemDefinition;
                if (processTask.getProcessRef() != null) {
                    org.flowable.cmmn.model.Process process = cmmnModel.getProcessById(processTask.getProcessRef());
                    if (process != null) {
                        processTask.setProcess(process);
                    }
                }

            } else if (planItemDefinition instanceof DecisionTask) {
                DecisionTask decisionTask = (DecisionTask) planItemDefinition;
                if (decisionTask.getDecisionRef() != null) {
                    org.flowable.cmmn.model.Decision decision = cmmnModel
                            .getDecisionById(decisionTask.getDecisionRef());
                    if (decision != null) {
                        decisionTask.setDecision(decision);
                    }
                }

            } else if (planItemDefinition instanceof TimerEventListener) {
                TimerEventListener timerEventListener = (TimerEventListener) planItemDefinition;
                String sourceRef = timerEventListener.getTimerStartTriggerSourceRef();
                PlanItem startTriggerPlanItem = timerEventListener.getParentStage()
                        .findPlanItemInPlanFragmentOrUpwards(sourceRef);
                if (startTriggerPlanItem != null) {
                    timerEventListener.setTimerStartTriggerPlanItem(startTriggerPlanItem);

                    // Although the CMMN spec does not categorize the timer start trigger as an entry criterion,
                    // it is exposed as such to the engine as there is no difference in handling it vs a real criterion
                    // which means no special care will need to be taken in the core engine operations

                    Criterion criterion = new Criterion();
                    criterion.setEntryCriterion(true);

                    SentryOnPart sentryOnPart = new SentryOnPart();
                    sentryOnPart.setSourceRef(startTriggerPlanItem.getId());
                    sentryOnPart.setSource(startTriggerPlanItem);
                    sentryOnPart.setStandardEvent(timerEventListener.getTimerStartTriggerStandardEvent());

                    Sentry sentry = new Sentry();
                    sentry.addSentryOnPart(sentryOnPart);

                    criterion.setSentry(sentry);
                    planItem.addEntryCriterion(criterion);
                }
            }

        }

    }

    protected void resolveEntryCriteria(HasEntryCriteria hasEntryCriteria) {
        for (Criterion entryCriterion : hasEntryCriteria.getEntryCriteria()) {
            Sentry sentry = entryCriterion.getParent().findSentry(entryCriterion.getSentryRef());
            if (sentry != null) {
                entryCriterion.setSentry(sentry);
            } else {
                throw new FlowableException("No sentry found for reference " + entryCriterion.getSentryRef()
                        + " of entry criterion " + entryCriterion.getId());
            }
        }
    }

    protected void resolveExitCriteriaSentry(HasExitCriteria hasExitCriteria) {
        for (Criterion exitCriterion : hasExitCriteria.getExitCriteria()) {
            Sentry sentry = exitCriterion.getParent().findSentry(exitCriterion.getSentryRef());
            if (sentry != null) {
                exitCriterion.setSentry(sentry);
            } else {
                throw new FlowableException("No sentry found for reference " + exitCriterion.getSentryRef()
                        + " of exit criterion " + exitCriterion.getId());
            }
        }
    }

    protected void processSentries(Stage planModelStage, PlanFragment planFragment) {
        for (Sentry sentry : planFragment.getSentries()) {
            for (SentryOnPart onPart : sentry.getOnParts()) {
                PlanItem planItem = planModelStage.findPlanItemInPlanFragmentOrDownwards(onPart.getSourceRef());
                if (planItem != null) {
                    onPart.setSource(planItem);
                } else {
                    throw new FlowableException("Could not resolve on part source reference "
                            + onPart.getSourceRef() + " of sentry " + sentry.getId());
                }
            }
        }
    }

    protected void ensureIds(List<? extends BaseElement> elements, String idPrefix) {
        Map<String, BaseElement> elementsWithId = new HashMap<>();
        Set<BaseElement> baseElementsWithoutId = new HashSet<>();
        for (BaseElement baseElement : elements) {
            if (baseElement.getId() != null) {
                elementsWithId.put(baseElement.getId(), baseElement);
            } else {
                baseElementsWithoutId.add(baseElement);
            }
        }

        if (!baseElementsWithoutId.isEmpty()) {
            int counter = 1;
            for (BaseElement baseElement : baseElementsWithoutId) {
                String id = idPrefix + counter++;
                while (elementsWithId.containsKey(id)) {
                    id = idPrefix + counter++;
                }

                baseElement.setId(id);
                elementsWithId.put(id, baseElement);
            }
        }
    }

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

    public static Map<String, BaseCmmnXmlConverter> getConvertersToCmmnModelMap() {
        return elementConverters;
    }

    public static void setConvertersToCmmnModelMap(Map<String, BaseCmmnXmlConverter> convertersToCmmnModelMap) {
        CmmnXmlConverter.elementConverters = convertersToCmmnModelMap;
    }

}