View Javadoc
1   /*
2    *    Copyright 2009-2023 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  
18  import java.util.HashMap;
19  import java.util.Map;
20  import java.util.Optional;
21  import java.util.Properties;
22  
23  import org.apache.ibatis.builder.BuilderException;
24  import org.apache.ibatis.builder.IncompleteElementException;
25  import org.apache.ibatis.builder.MapperBuilderAssistant;
26  import org.apache.ibatis.parsing.PropertyParser;
27  import org.apache.ibatis.parsing.XNode;
28  import org.apache.ibatis.session.Configuration;
29  import org.w3c.dom.NamedNodeMap;
30  import org.w3c.dom.Node;
31  import org.w3c.dom.NodeList;
32  
33  /**
34   * @author Frank D. Martinez [mnesarco]
35   */
36  public class XMLIncludeTransformer {
37  
38    private final Configuration configuration;
39    private final MapperBuilderAssistant builderAssistant;
40  
41    public XMLIncludeTransformer(Configuration configuration, MapperBuilderAssistant builderAssistant) {
42      this.configuration = configuration;
43      this.builderAssistant = builderAssistant;
44    }
45  
46    public void applyIncludes(Node source) {
47      Properties variablesContext = new Properties();
48      Properties configurationVariables = configuration.getVariables();
49      Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll);
50      applyIncludes(source, variablesContext, false);
51    }
52  
53    /**
54     * Recursively apply includes through all SQL fragments.
55     *
56     * @param source
57     *          Include node in DOM tree
58     * @param variablesContext
59     *          Current context for static variables with values
60     */
61    private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
62      if ("include".equals(source.getNodeName())) {
63        Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
64        Properties toIncludeContext = getVariablesContext(source, variablesContext);
65        applyIncludes(toInclude, toIncludeContext, true);
66        if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
67          toInclude = source.getOwnerDocument().importNode(toInclude, true);
68        }
69        source.getParentNode().replaceChild(toInclude, source);
70        while (toInclude.hasChildNodes()) {
71          toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
72        }
73        toInclude.getParentNode().removeChild(toInclude);
74      } else if (source.getNodeType() == Node.ELEMENT_NODE) {
75        if (included && !variablesContext.isEmpty()) {
76          // replace variables in attribute values
77          NamedNodeMap attributes = source.getAttributes();
78          for (int i = 0; i < attributes.getLength(); i++) {
79            Node attr = attributes.item(i);
80            attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
81          }
82        }
83        NodeList children = source.getChildNodes();
84        for (int i = 0; i < children.getLength(); i++) {
85          applyIncludes(children.item(i), variablesContext, included);
86        }
87      } else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
88          && !variablesContext.isEmpty()) {
89        // replace variables in text node
90        source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
91      }
92    }
93  
94    private Node findSqlFragment(String refid, Properties variables) {
95      refid = PropertyParser.parse(refid, variables);
96      refid = builderAssistant.applyCurrentNamespace(refid, true);
97      try {
98        XNode nodeToInclude = configuration.getSqlFragments().get(refid);
99        return nodeToInclude.getNode().cloneNode(true);
100     } catch (IllegalArgumentException e) {
101       throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e);
102     }
103   }
104 
105   private String getStringAttribute(Node node, String name) {
106     return node.getAttributes().getNamedItem(name).getNodeValue();
107   }
108 
109   /**
110    * Read placeholders and their values from include node definition.
111    *
112    * @param node
113    *          Include node instance
114    * @param inheritedVariablesContext
115    *          Current context used for replace variables in new variables values
116    *
117    * @return variables context from include instance (no inherited values)
118    */
119   private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) {
120     Map<String, String> declaredProperties = null;
121     NodeList children = node.getChildNodes();
122     for (int i = 0; i < children.getLength(); i++) {
123       Node n = children.item(i);
124       if (n.getNodeType() == Node.ELEMENT_NODE) {
125         String name = getStringAttribute(n, "name");
126         // Replace variables inside
127         String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext);
128         if (declaredProperties == null) {
129           declaredProperties = new HashMap<>();
130         }
131         if (declaredProperties.put(name, value) != null) {
132           throw new BuilderException("Variable " + name + " defined twice in the same include definition");
133         }
134       }
135     }
136     if (declaredProperties == null) {
137       return inheritedVariablesContext;
138     }
139     Properties newProperties = new Properties();
140     newProperties.putAll(inheritedVariablesContext);
141     newProperties.putAll(declaredProperties);
142     return newProperties;
143   }
144 }