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