View Javadoc
1   /*
2    *    Copyright 2006-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.mybatis.generator.internal;
17  
18  import static org.mybatis.generator.internal.util.messages.Messages.getString;
19  
20  import java.io.File;
21  import java.io.FileInputStream;
22  import java.io.IOException;
23  import java.io.InputStreamReader;
24  import java.io.StringReader;
25  import java.nio.charset.StandardCharsets;
26  import java.util.ArrayList;
27  import java.util.List;
28  
29  import javax.xml.XMLConstants;
30  import javax.xml.parsers.DocumentBuilder;
31  import javax.xml.parsers.DocumentBuilderFactory;
32  import javax.xml.parsers.ParserConfigurationException;
33  
34  import org.mybatis.generator.api.GeneratedXmlFile;
35  import org.mybatis.generator.config.MergeConstants;
36  import org.mybatis.generator.exception.ShellException;
37  import org.w3c.dom.Comment;
38  import org.w3c.dom.Document;
39  import org.w3c.dom.DocumentType;
40  import org.w3c.dom.Element;
41  import org.w3c.dom.NamedNodeMap;
42  import org.w3c.dom.Node;
43  import org.w3c.dom.NodeList;
44  import org.w3c.dom.Text;
45  import org.xml.sax.EntityResolver;
46  import org.xml.sax.InputSource;
47  import org.xml.sax.SAXException;
48  
49  /**
50   * This class handles the task of merging changes into an existing XML file.
51   *
52   * @author Jeff Butler
53   */
54  public class XmlFileMergerJaxp {
55      private XmlFileMergerJaxp() {
56      }
57  
58      private static class NullEntityResolver implements EntityResolver {
59          /**
60           * returns an empty reader. This is done so that the parser doesn't
61           * attempt to read a DTD. We don't need that support for the merge and
62           * it can cause problems on systems that aren't Internet connected.
63           */
64          @Override
65          public InputSource resolveEntity(String publicId, String systemId) {
66  
67              StringReader sr = new StringReader(""); //$NON-NLS-1$
68  
69              return new InputSource(sr);
70          }
71      }
72  
73      public static String getMergedSource(GeneratedXmlFile generatedXmlFile,
74              File existingFile) throws ShellException {
75  
76          try {
77              return getMergedSource(new InputSource(new StringReader(generatedXmlFile.getFormattedContent())),
78                  new InputSource(new InputStreamReader(new FileInputStream(existingFile), StandardCharsets.UTF_8)),
79                  existingFile.getName());
80          } catch (IOException | SAXException | ParserConfigurationException e) {
81              throw new ShellException(getString("Warning.13", //$NON-NLS-1$
82                      existingFile.getName()), e);
83          }
84      }
85  
86      public static String getMergedSource(InputSource newFile,
87              InputSource existingFile, String existingFileName) throws IOException, SAXException,
88              ParserConfigurationException, ShellException {
89  
90          DocumentBuilderFactory factory = DocumentBuilderFactory
91                  .newInstance();
92          factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
93          factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
94          factory.setExpandEntityReferences(false);
95          factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
96          DocumentBuilder builder = factory.newDocumentBuilder();
97          builder.setEntityResolver(new NullEntityResolver());
98  
99          Document existingDocument = builder.parse(existingFile);
100         Document newDocument = builder.parse(newFile);
101 
102         DocumentType newDocType = newDocument.getDoctype();
103         DocumentType existingDocType = existingDocument.getDoctype();
104 
105         if (!newDocType.getName().equals(existingDocType.getName())) {
106             throw new ShellException(getString("Warning.12", //$NON-NLS-1$
107                     existingFileName));
108         }
109 
110         Element existingRootElement = existingDocument.getDocumentElement();
111         Element newRootElement = newDocument.getDocumentElement();
112 
113         // reconcile the root element attributes -
114         // take all attributes from the new element and add to the existing
115         // element
116 
117         // remove all attributes from the existing root element
118         NamedNodeMap attributes = existingRootElement.getAttributes();
119         int attributeCount = attributes.getLength();
120         for (int i = attributeCount - 1; i >= 0; i--) {
121             Node node = attributes.item(i);
122             existingRootElement.removeAttribute(node.getNodeName());
123         }
124 
125         // add attributes from the new root node to the old root node
126         attributes = newRootElement.getAttributes();
127         attributeCount = attributes.getLength();
128         for (int i = 0; i < attributeCount; i++) {
129             Node node = attributes.item(i);
130             existingRootElement.setAttribute(node.getNodeName(), node
131                     .getNodeValue());
132         }
133 
134         // remove the old generated elements and any
135         // white space before the old nodes
136         List<Node> nodesToDelete = new ArrayList<>();
137         NodeList children = existingRootElement.getChildNodes();
138         int length = children.getLength();
139         for (int i = 0; i < length; i++) {
140             Node node = children.item(i);
141             if (isGeneratedNode(node)) {
142                 nodesToDelete.add(node);
143             } else if (isWhiteSpace(node)
144                     && isGeneratedNode(children.item(i + 1))) {
145                 nodesToDelete.add(node);
146             }
147         }
148 
149         for (Node node : nodesToDelete) {
150             existingRootElement.removeChild(node);
151         }
152 
153         // add the new generated elements
154         children = newRootElement.getChildNodes();
155         length = children.getLength();
156         Node firstChild = existingRootElement.getFirstChild();
157         for (int i = 0; i < length; i++) {
158             Node node = children.item(i);
159             // don't add the last node if it is only white space
160             if (i == length - 1 && isWhiteSpace(node)) {
161                 break;
162             }
163 
164             Node newNode = existingDocument.importNode(node, true);
165             if (firstChild == null) {
166                 existingRootElement.appendChild(newNode);
167             } else {
168                 existingRootElement.insertBefore(newNode, firstChild);
169             }
170         }
171 
172         // pretty print the result
173         return prettyPrint(existingDocument);
174     }
175 
176     private static String prettyPrint(Document document) throws ShellException {
177         DomWriter dw = new DomWriter();
178         return dw.toString(document);
179     }
180 
181     private static boolean isGeneratedNode(Node node) {
182         return node != null
183                 && node.getNodeType() == Node.ELEMENT_NODE
184                 && (isOldFormatNode(node) || isNewFormatNode(node));
185     }
186 
187     private static boolean isOldFormatNode(Node node) {
188         Element element = (Element) node;
189         String id = element.getAttribute("id"); //$NON-NLS-1$
190         if (id != null) {
191             return MergeConstants.idStartsWithPrefix(id);
192         }
193 
194         return false;
195     }
196 
197     private static boolean isNewFormatNode(Node node) {
198         // check for new node format - if the first non-whitespace node
199         // is an XML comment, and the comment includes
200         // one of the old element tags,
201         // then it is a generated node
202         NodeList children = node.getChildNodes();
203         int length = children.getLength();
204         for (int i = 0; i < length; i++) {
205             Node childNode = children.item(i);
206             if (childNode != null && childNode.getNodeType() == Node.COMMENT_NODE) {
207                 String commentData = ((Comment) childNode).getData();
208                 return MergeConstants.commentContainsTag(commentData);
209             }
210         }
211 
212         return false;
213     }
214 
215     private static boolean isWhiteSpace(Node node) {
216         boolean rc = false;
217 
218         if (node != null && node.getNodeType() == Node.TEXT_NODE) {
219             Text tn = (Text) node;
220             if (tn.getData().trim().length() == 0) {
221                 rc = true;
222             }
223         }
224 
225         return rc;
226     }
227 }