View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.shared.release.transform.jdom2;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.StringReader;
24  import java.io.StringWriter;
25  import java.io.Writer;
26  import java.util.Iterator;
27  import java.util.regex.Matcher;
28  import java.util.regex.Pattern;
29  
30  import org.apache.maven.model.Model;
31  import org.apache.maven.project.MavenProject;
32  import org.apache.maven.shared.release.ReleaseExecutionException;
33  import org.apache.maven.shared.release.config.ReleaseDescriptor;
34  import org.apache.maven.shared.release.transform.ModelETL;
35  import org.apache.maven.shared.release.util.ReleaseUtil;
36  import org.codehaus.plexus.util.WriterFactory;
37  import org.jdom2.CDATA;
38  import org.jdom2.Comment;
39  import org.jdom2.Document;
40  import org.jdom2.Element;
41  import org.jdom2.JDOMException;
42  import org.jdom2.Namespace;
43  import org.jdom2.filter.ContentFilter;
44  import org.jdom2.filter.ElementFilter;
45  import org.jdom2.input.SAXBuilder;
46  import org.jdom2.output.Format;
47  import org.jdom2.output.XMLOutputter;
48  
49  /**
50   * JDOM2 implementation for extracting, transform, loading the Model (pom.xml)
51   *
52   * @author Robert Scholte
53   * @since 3.0
54   */
55  public class JDomModelETL implements ModelETL {
56      private ReleaseDescriptor releaseDescriptor;
57  
58      private MavenProject project;
59  
60      private Document document;
61  
62      private String intro = null;
63      private String outtro = null;
64  
65      private String ls = ReleaseUtil.LS;
66  
67      /**
68       * <p>Setter for the field <code>ls</code>.</p>
69       *
70       * @param ls a {@link java.lang.String} object
71       */
72      public void setLs(String ls) {
73          this.ls = ls;
74      }
75  
76      /**
77       * <p>Setter for the field <code>releaseDescriptor</code>.</p>
78       *
79       * @param releaseDescriptor a {@link org.apache.maven.shared.release.config.ReleaseDescriptor} object
80       */
81      public void setReleaseDescriptor(ReleaseDescriptor releaseDescriptor) {
82          this.releaseDescriptor = releaseDescriptor;
83      }
84  
85      /**
86       * <p>Setter for the field <code>project</code>.</p>
87       *
88       * @param project a {@link org.apache.maven.project.MavenProject} object
89       */
90      public void setProject(MavenProject project) {
91          this.project = project;
92      }
93  
94      @Override
95      public void extract(File pomFile) throws ReleaseExecutionException {
96          try {
97              String content = ReleaseUtil.readXmlFile(pomFile, ls);
98              // we need to eliminate any extra whitespace inside elements, as JDOM2 will nuke it
99              content = content.replaceAll("<([^!][^>]*?)\\s{2,}([^>]*?)>", "<$1 $2>");
100             content = content.replaceAll("(\\s{2,})/>", "$1 />");
101 
102             SAXBuilder builder = new SAXBuilder();
103             document = builder.build(new StringReader(content));
104 
105             // Normalize line endings to platform's style (XML processors like JDOM2 normalize line endings to "\n" as
106             // per section 2.11 of the XML spec)
107             normaliseLineEndings(document);
108 
109             // rewrite DOM as a string to find differences, since text outside the root element is not tracked
110             StringWriter w = new StringWriter();
111             Format format = Format.getRawFormat();
112             format.setLineSeparator(ls);
113             XMLOutputter out = new XMLOutputter(format);
114             out.output(document.getRootElement(), w);
115 
116             int index = content.indexOf(w.toString());
117             if (index >= 0) {
118                 intro = content.substring(0, index);
119                 outtro = content.substring(index + w.toString().length());
120             } else {
121                 /*
122                  * NOTE: Due to whitespace, attribute reordering or entity expansion the above indexOf test can easily
123                  * fail. So let's try harder. Maybe some day, when JDOM2 offers a StaxBuilder and this builder employes
124                  * XMLInputFactory2.P_REPORT_PROLOG_WHITESPACE, this whole mess can be avoided.
125                  */
126                 // CHECKSTYLE_OFF: LocalFinalVariableName
127                 final String SPACE = "\\s++";
128                 final String XML = "<\\?(?:(?:[^\"'>]++)|(?:\"[^\"]*+\")|(?:'[^\']*+'))*+>";
129                 final String INTSUB = "\\[(?:(?:[^\"'\\]]++)|(?:\"[^\"]*+\")|(?:'[^\']*+'))*+\\]";
130                 final String DOCTYPE =
131                         "<!DOCTYPE(?:(?:[^\"'\\[>]++)|(?:\"[^\"]*+\")|(?:'[^\']*+')|(?:" + INTSUB + "))*+>";
132                 final String PI = XML;
133                 final String COMMENT = "<!--(?:[^-]|(?:-[^-]))*+-->";
134 
135                 final String INTRO =
136                         "(?:(?:" + SPACE + ")|(?:" + XML + ")|(?:" + DOCTYPE + ")|(?:" + COMMENT + ")|(?:" + PI + "))*";
137                 final String OUTRO = "(?:(?:" + SPACE + ")|(?:" + COMMENT + ")|(?:" + PI + "))*";
138                 final String POM = "(?s)(" + INTRO + ")(.*?)(" + OUTRO + ")";
139                 // CHECKSTYLE_ON: LocalFinalVariableName
140 
141                 Matcher matcher = Pattern.compile(POM).matcher(content);
142                 if (matcher.matches()) {
143                     intro = matcher.group(1);
144                     outtro = matcher.group(matcher.groupCount());
145                 }
146             }
147         } catch (JDOMException | IOException e) {
148             throw new ReleaseExecutionException("Error reading POM: " + e.getMessage(), e);
149         }
150     }
151 
152     @Override
153     public void transform() {}
154 
155     @Override
156     public void load(File targetFile) throws ReleaseExecutionException {
157         writePom(targetFile, document, releaseDescriptor, project.getModelVersion(), intro, outtro);
158     }
159 
160     @Override
161     public Model getModel() {
162         return new JDomModel(document);
163     }
164 
165     private void normaliseLineEndings(Document document) {
166         for (Iterator<?> i = document.getDescendants(new ContentFilter(ContentFilter.COMMENT)); i.hasNext(); ) {
167             Comment c = (Comment) i.next();
168             c.setText(ReleaseUtil.normalizeLineEndings(c.getText(), ls));
169         }
170         for (Iterator<?> i = document.getDescendants(new ContentFilter(ContentFilter.CDATA)); i.hasNext(); ) {
171             CDATA c = (CDATA) i.next();
172             c.setText(ReleaseUtil.normalizeLineEndings(c.getText(), ls));
173         }
174     }
175 
176     private void writePom(
177             File pomFile,
178             Document document,
179             ReleaseDescriptor releaseDescriptor,
180             String modelVersion,
181             String intro,
182             String outtro)
183             throws ReleaseExecutionException {
184         Element rootElement = document.getRootElement();
185 
186         if (releaseDescriptor.isAddSchema()) {
187             Namespace pomNamespace = Namespace.getNamespace("", "http://maven.apache.org/POM/" + modelVersion);
188             rootElement.setNamespace(pomNamespace);
189             Namespace xsiNamespace = Namespace.getNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
190             rootElement.addNamespaceDeclaration(xsiNamespace);
191 
192             if (rootElement.getAttribute("schemaLocation", xsiNamespace) == null) {
193                 rootElement.setAttribute(
194                         "schemaLocation",
195                         "http://maven.apache.org/POM/" + modelVersion + " https://maven.apache.org/xsd/maven-"
196                                 + modelVersion + ".xsd",
197                         xsiNamespace);
198             }
199 
200             // the empty namespace is considered equal to the POM namespace, so match them up to avoid extra xmlns=""
201             ElementFilter elementFilter = new ElementFilter(Namespace.getNamespace(""));
202             for (Iterator<?> i = rootElement.getDescendants(elementFilter); i.hasNext(); ) {
203                 Element e = (Element) i.next();
204                 e.setNamespace(pomNamespace);
205             }
206         }
207 
208         try (Writer writer = WriterFactory.newXmlWriter(pomFile)) {
209             if (intro != null) {
210                 writer.write(intro);
211             }
212 
213             Format format = Format.getRawFormat();
214             format.setLineSeparator(ls);
215             XMLOutputter out = new XMLOutputter(format);
216             out.output(document.getRootElement(), writer);
217 
218             if (outtro != null) {
219                 writer.write(outtro);
220             }
221         } catch (IOException e) {
222             throw new ReleaseExecutionException("Error writing POM: " + e.getMessage(), e);
223         }
224     }
225 }