XMLIncludeTransformer.java

  1. /*
  2.  *    Copyright 2009-2024 the original author or authors.
  3.  *
  4.  *    Licensed under the Apache License, Version 2.0 (the "License");
  5.  *    you may not use this file except in compliance with the License.
  6.  *    You may obtain a copy of the License at
  7.  *
  8.  *       https://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  *    Unless required by applicable law or agreed to in writing, software
  11.  *    distributed under the License is distributed on an "AS IS" BASIS,
  12.  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  *    See the License for the specific language governing permissions and
  14.  *    limitations under the License.
  15.  */
  16. package org.apache.ibatis.builder.xml;

  17. import java.util.HashMap;
  18. import java.util.Map;
  19. import java.util.Optional;
  20. import java.util.Properties;

  21. import org.apache.ibatis.builder.BuilderException;
  22. import org.apache.ibatis.builder.IncompleteElementException;
  23. import org.apache.ibatis.builder.MapperBuilderAssistant;
  24. import org.apache.ibatis.parsing.PropertyParser;
  25. import org.apache.ibatis.parsing.XNode;
  26. import org.apache.ibatis.session.Configuration;
  27. import org.w3c.dom.NamedNodeMap;
  28. import org.w3c.dom.Node;
  29. import org.w3c.dom.NodeList;

  30. /**
  31.  * @author Frank D. Martinez [mnesarco]
  32.  */
  33. public class XMLIncludeTransformer {

  34.   private final Configuration configuration;
  35.   private final MapperBuilderAssistant builderAssistant;

  36.   public XMLIncludeTransformer(Configuration configuration, MapperBuilderAssistant builderAssistant) {
  37.     this.configuration = configuration;
  38.     this.builderAssistant = builderAssistant;
  39.   }

  40.   public void applyIncludes(Node source) {
  41.     Properties variablesContext = new Properties();
  42.     Properties configurationVariables = configuration.getVariables();
  43.     Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
  44.     applyIncludes(source, variablesContext, false);
  45.   }

  46.   /**
  47.    * Recursively apply includes through all SQL fragments.
  48.    *
  49.    * @param source
  50.    *          Include node in DOM tree
  51.    * @param variablesContext
  52.    *          Current context for static variables with values
  53.    */
  54.   private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
  55.     if ("include".equals(source.getNodeName())) {
  56.       Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
  57.       Properties toIncludeContext = getVariablesContext(source, variablesContext);
  58.       applyIncludes(toInclude, toIncludeContext, true);
  59.       if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
  60.         toInclude = source.getOwnerDocument().importNode(toInclude, true);
  61.       }
  62.       source.getParentNode().replaceChild(toInclude, source);
  63.       while (toInclude.hasChildNodes()) {
  64.         toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
  65.       }
  66.       toInclude.getParentNode().removeChild(toInclude);
  67.     } else if (source.getNodeType() == Node.ELEMENT_NODE) {
  68.       if (included && !variablesContext.isEmpty()) {
  69.         // replace variables in attribute values
  70.         NamedNodeMap attributes = source.getAttributes();
  71.         for (int i = 0; i < attributes.getLength(); i++) {
  72.           Node attr = attributes.item(i);
  73.           attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
  74.         }
  75.       }
  76.       NodeList children = source.getChildNodes();
  77.       for (int i = 0; i < children.getLength(); i++) {
  78.         applyIncludes(children.item(i), variablesContext, included);
  79.       }
  80.     } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
  81.         && !variablesContext.isEmpty()) {
  82.       // replace variables in text node
  83.       source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
  84.     }
  85.   }

  86.   private Node findSqlFragment(String refid, Properties variables) {
  87.     refid = PropertyParser.parse(refid, variables);
  88.     refid = builderAssistant.applyCurrentNamespace(refid, true);
  89.     try {
  90.       XNode nodeToInclude = configuration.getSqlFragments().get(refid);
  91.       return nodeToInclude.getNode().cloneNode(true);
  92.     } catch (IllegalArgumentException e) {
  93.       throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
  94.     }
  95.   }

  96.   private String getStringAttribute(Node node, String name) {
  97.     return node.getAttributes().getNamedItem(name).getNodeValue();
  98.   }

  99.   /**
  100.    * Read placeholders and their values from include node definition.
  101.    *
  102.    * @param node
  103.    *          Include node instance
  104.    * @param inheritedVariablesContext
  105.    *          Current context used for replace variables in new variables values
  106.    *
  107.    * @return variables context from include instance (no inherited values)
  108.    */
  109.   private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) {
  110.     Map<String, String> declaredProperties = null;
  111.     NodeList children = node.getChildNodes();
  112.     for (int i = 0; i < children.getLength(); i++) {
  113.       Node n = children.item(i);
  114.       if (n.getNodeType() == Node.ELEMENT_NODE) {
  115.         String name = getStringAttribute(n, "name");
  116.         // Replace variables inside
  117.         String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext);
  118.         if (declaredProperties == null) {
  119.           declaredProperties = new HashMap<>();
  120.         }
  121.         if (declaredProperties.put(name, value) != null) {
  122.           throw new BuilderException("Variable " + name + " defined twice in the same include definition");
  123.         }
  124.       }
  125.     }
  126.     if (declaredProperties == null) {
  127.       return inheritedVariablesContext;
  128.     }
  129.     Properties newProperties = new Properties();
  130.     newProperties.putAll(inheritedVariablesContext);
  131.     newProperties.putAll(declaredProperties);
  132.     return newProperties;
  133.   }
  134. }