All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.effektif.workflow.api.bpmn.XmlElement Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2014 Effektif GmbH.
 *
 * 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 com.effektif.workflow.api.bpmn;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.effektif.workflow.api.json.JsonIgnore;

/** 
 * XML DOM structure that is jsonnable with Jackson
 * so that it can be serialized to/from JSON for REST service and database persistence.
 *
 * @author Tom Baeyens
 */
public class XmlElement {

  private static final Logger log = LoggerFactory.getLogger(XmlElement.class);

  /** prefix:localPart.  eg "e:subject" */
  public String name;

  /** Maps attribute names (prefix:localPart eg "e:type" or "type") to attribute values. */
  public Map attributes;
  
  /** Maps namespace URIs to prefixes; a null value represents the default namespace. */
  public XmlNamespaces namespaces;
  
  public List elements;
  public String text;
  
  /** not persisted in the db */
  @JsonIgnore
  public XmlElement parent;
  /** not persisted in the db */
  @JsonIgnore
  public String namespaceUri;

  // name ///////////////////////////////////////////////////////////////////////////

  /**
   * Clears the BPMN element name, used to clean up unparsed BPMN after BPMN import when the name is redundant.
   */
  public void clearName() {
    this.name = null;
  }

  /**
   * Simplifies the BPMN XML by empty collections of attributes, child elements and extension elements.
   */
  public void cleanEmptyElements() {
    // Remove empty attributes list.
    if (attributes != null && attributes.isEmpty()) {
      attributes = null;
    }
    if (elements != null) {
      // Recursively clean child elements.
      for (XmlElement childElement : elements) {
        childElement.cleanEmptyElements();
      }

      // Remove empty child elements list.
      if (elements.isEmpty()) {
        elements = null;
      }
    }
  }

  public void setName(String namespaceUri, String localPart) {
    this.name = getNamespacePrefix(namespaceUri) + localPart;
    this.namespaceUri = namespaceUri;
  }

  public boolean is(String namespaceUri, String localPart) {
    return (this.namespaceUri!= null && this.namespaceUri.equals(namespaceUri) && name.equals(localPart))
           || (name.equals(getNamespacePrefix(namespaceUri)+localPart));
  }

  public String getName() {
    return name;
  }

  public String getLocalBPMNName() {
    String localPart = this.name.contains(":") ? this.name.substring(this.name.indexOf(':') + 1) : this.name;

    return this.is("http://www.omg.org/spec/BPMN/20100524/MODEL", localPart) ? localPart : null;
  }

  // namespaces ///////////////////////////////////////////////////////////////////////////
  
  /** maps namespace uris to prefixes */
  public XmlNamespaces getNamespaces() {
    return namespaces;
  }

  public void addNamespace(String namespaceUri, String prefix) {
    if (namespaces == null) {
      namespaces = new XmlNamespaces();
    }
    if ("".equals(prefix)) {
      prefix = null;
    }
    try {
      namespaces.add(prefix, namespaceUri);
    } catch (URISyntaxException e) {
      log.error(String.format("Cannot add XML namespace for invalid URI %s: %s", namespaceUri, e.getMessage()));
    }
  }
  
  public boolean hasNamespace(String namespaceUri) {
    if (this.namespaces!=null && this.namespaces.hasNamespace(namespaceUri)) {
      return true;
    }
    if (parent!=null) {
      return parent.hasNamespace(namespaceUri);
    }
    return false;
  }

  protected String getNamespacePrefix(String namespaceUri) {
    if (this.namespaces != null && this.namespaces.hasNamespace(namespaceUri)) {
      String prefix = this.namespaces.getPrefix(namespaceUri);
      return prefix == null || prefix.isEmpty() ? "" : prefix + ":";
    }
    if (parent!=null) {
      return parent.getNamespacePrefix(namespaceUri);
    }
    return "";
  }

  // elements ///////////////////////////////////////////////////////////////////////////

  public List getElements() {
    return elements;
  }
  
  public List removeElements(String namespaceUri, String localPart) {
    List result = new ArrayList<>();
    if (this.elements != null) {
      Iterator iterator = this.elements.iterator();
      while (iterator.hasNext()) {
        XmlElement xmlElement = iterator.next();
        if (xmlElement.is(namespaceUri, localPart)) {
          iterator.remove();
          result.add(xmlElement);
        }
      }
    }
    return result;
  }
  
  public XmlElement removeElement(String namespaceUri, String localPart) {
    if (elements!=null) {
      Iterator iterator = elements.iterator();
      while (iterator.hasNext()) {
        XmlElement xmlElement = iterator.next();
        if (xmlElement.is(namespaceUri, localPart)) {
          iterator.remove();
          return xmlElement;
        }
      }
    }
    return null;
  }

  /**
   * Removes the specified element if it has no child elements (attributes are ignored).
   */
  public void removeEmptyElement(String namespaceUri, String localPart) {
    XmlElement element = getElement(namespaceUri, localPart);
    if (element != null) {
      List childElements = element.getElements();
      if (childElements == null || childElements.isEmpty()) {
        removeElement(namespaceUri, localPart);
      }
    }
  }

  /**
   * Returns the first element with the given name, or null if there isn’t one.
   */
  public XmlElement getElement(String namespaceUri, String localPart) {
    if (elements==null) {
      return null;
    }
    for (XmlElement childElement : elements) {
      if (childElement.is(namespaceUri, localPart)) {
        return childElement;
      }
    }
    return null;
  }

  public XmlElement getOrCreateChildElement(String namespaceUri, String localPart) {
    return getOrCreateChildElement(namespaceUri, localPart, null);
  }

  /**
   * Returns either the first element with the given name, or the result of adding a new element if there wasn’t one.
   */
  public XmlElement getOrCreateChildElement(String namespaceUri, String localPart, Integer index) {
    XmlElement existingElement = getElement(namespaceUri, localPart);
    if (existingElement!=null) {
      return existingElement;
    }
    return createElement(namespaceUri, localPart, index);
  }
  
  public XmlElement createElement(String namespaceUri, String localPart) {
    return createElement(namespaceUri, localPart, null);
  }

  public XmlElement createElementFirst(String namespaceUri, String localPart) {
    return createElement(namespaceUri, localPart, 0);
  }

  public XmlElement createElement(String namespaceUri, String localPart, Integer index) {
    XmlElement element = new XmlElement();
    element.parent = this;
    element.namespaceUri = namespaceUri;
    addElement(element, index);
    element.setName(namespaceUri, localPart);
    return element;
  }

  public void addElement(XmlElement xmlElement) {
    addElement(xmlElement, null);
  }

  public void addElementFirst(XmlElement xmlElement) {
    addElement(xmlElement, 0);
  }

  public void addElement(XmlElement xmlElement, Integer index) {
    if (xmlElement!=null) {
      if (elements==null) {
        this.elements = new ArrayList<>();
      }
      if (index!=null) {
        this.elements.add(index, xmlElement);
      } else {
        this.elements.add(xmlElement);
      }
      xmlElement.parent = this;
    }
  }


  // attributes ///////////////////////////////////////////////////////////////////////////

  public void addAttribute(String namespaceUri, String localPart, Object value) {
    if (attributes==null) {
      attributes = new LinkedHashMap<>();
    }
    String attributeName = null; 
    if (this.namespaceUri!=null && this.namespaceUri.equals(namespaceUri)) {
      attributeName = localPart;
    } else {
      attributeName = getNamespacePrefix(namespaceUri)+localPart;
    }
    attributes.put(attributeName, escapeAttributeValue(value));
  }

  public String removeAttribute(String namespaceUri, String localPart) {
    if (attributes==null) {
      return null;
    }
    if ( this.namespaceUri.equals(namespaceUri)
         && attributes.containsKey(localPart) ) {
      return unescapeXml(attributes.remove(localPart));
    }
    String attributeName = getNamespacePrefix(namespaceUri)+localPart;
    return unescapeXml(attributes.remove(attributeName));
  }

  public String getAttribute(String namespaceUri, String localPart) {
    if (attributes==null) {
      return null;
    }
    if ( this.namespaceUri.equals(namespaceUri)
         && attributes.containsKey(localPart) ) {
      return unescapeXml(attributes.get(localPart));
    }
    String attributeName = getNamespacePrefix(namespaceUri)+localPart;
    return unescapeXml(attributes.get(attributeName));
  }

  public Map getAttributes() {
    return attributes;
  }
  
  // text ///////////////////////////////////////////////////////////////////////////

  private static Pattern specialCharacters = Pattern.compile("[<>&]");

  /**
   * Adds text to the current element’s text node, wrapping the text in CDATA section if necessary, instead of escaping.
   */
  public void addCDataText(String value) {
    if (value != null) {
      boolean containsSpecialCharacters = specialCharacters.matcher(value).find();
      String wrappedText = containsSpecialCharacters ? "" : value;
      this.text = this.text != null ? this.text + wrappedText : wrappedText;
    }
  }

  /**
   * Adds text to the current element’s text node, escaping XML special characters.
   */
  public void addText(Object value) {
    if (value != null) {
      String text = value.toString();
      if (!"".equals(text.trim())) {
        this.text = this.text != null ? this.text + escapeTextNode(text) : escapeTextNode(text);
      }
    }
  }

  public String getText() {
    return unescapeXml(text);
  }
  
  // other ///////////////////////////////////////////////////////////////////////////

  public boolean isEmpty() {
    if (attributes != null && !attributes.isEmpty()) {
      return false;
    }
    if (elements != null && !elements.isEmpty()) {
      return false;
    }
    if (text != null && !text.isEmpty()) {
      return false;
    }
    return true;
  }

  public boolean hasContent() {
    if (elements!=null && !elements.isEmpty()) {
      return false;
    }
    if (text!=null && !"".equals(text)) {
      return false;
    }
    return true;
  }

  private static String escapeTextNode(Object value) { return escapeXml(value, false); }
  private static String escapeAttributeValue(Object value) { return escapeXml(value, true); }

  private static String escapeXml(Object value, boolean replaceQuotes) {
    if (value == null) {
      return null;
    }
    String text = value.toString();
    String result = text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
    if (replaceQuotes) {
      result = result.replaceAll("\"", """).replaceAll("'", "'");
    }
    return result;
  }

  private static String unescapeXml(String value) {
    if (value == null) {
      return null;
    }
    return value.replaceAll(""", "\"").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">")
      .replaceAll("&", "&");
  }

  /**
   * Recursively fixes parent relationships, which are unset after deserialisation, such as when unparsed BPMN is read
   * from the database. If these parent relationships are not set, then the recursive namespace lookup fails.
   */
  public void setElementParents() {
    if (elements == null) {
      return;
    }
    for (XmlElement child : elements) {
      if (child.parent == null) {
        child.parent = this;
      }
      child.setElementParents();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy