diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/api/PomHelper.java b/versions-common/src/main/java/org/codehaus/mojo/versions/api/PomHelper.java index 135f58fd9a..399db76add 100644 --- a/versions-common/src/main/java/org/codehaus/mojo/versions/api/PomHelper.java +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/api/PomHelper.java @@ -81,6 +81,7 @@ import org.apache.maven.project.ProjectBuildingResult; import org.apache.maven.shared.utils.io.IOUtil; import org.codehaus.mojo.versions.rewriting.MutableXMLStreamReader; +import org.codehaus.mojo.versions.rewriting.PropertyVersionInternal; import org.codehaus.mojo.versions.utils.ArtifactFactory; import org.codehaus.mojo.versions.utils.ModelNode; import org.codehaus.mojo.versions.utils.RegexUtils; @@ -224,59 +225,24 @@ public static Model getRawModel(Reader reader) throws IOException { */ public static boolean setPropertyVersion( MutableXMLStreamReader pom, String profileId, String property, String value) throws XMLStreamException { - class PropertyVersionInternal { - final Pattern propertyRegex = Pattern.compile( - (profileId == null ? "/project/properties/" : "/project/profiles/profile/properties/") - + RegexUtils.quote(property)); - final Pattern matchScopeRegex = profileId == null - ? Pattern.compile("/project/properties") - : Pattern.compile("/project/profiles/profile"); - final Pattern profileIdRegex = profileId == null ? null : Pattern.compile("/project/profiles/profile/id"); - - boolean setPropertyVersion(String path, boolean inMatchScope) throws XMLStreamException { - boolean replaced = false; - while (!replaced && pom.hasNext()) { - pom.next(); - if (pom.isStartElement()) { - String elementPath = path + "/" + pom.getLocalName(); - if (propertyRegex.matcher(elementPath).matches()) { - pom.mark(START); - } else if (matchScopeRegex.matcher(elementPath).matches()) { - // we're in a new match scope -> reset any previous partial matches - inMatchScope = profileId == null; - pom.clearMark(START); - pom.clearMark(END); - } else if (profileId != null - && profileIdRegex.matcher(elementPath).matches()) { - inMatchScope = - profileId.trim().equals(pom.getElementText().trim()); - } - - // getElementText could've pushed the pointer to END_ELEMENT - if (!pom.isEndElement()) { - // inMatchScope will not change in the child element - replaced = setPropertyVersion(elementPath, inMatchScope); - } - } else if (pom.isEndElement()) { - if (propertyRegex.matcher(path).matches()) { - pom.mark(END); - } else if (matchScopeRegex.matcher(path).matches()) { - if (inMatchScope && pom.hasMark(START) && pom.hasMark(END)) { - pom.replaceBetween(START, END, value); - replaced = true; - } - pom.clearMark(START); - pom.clearMark(END); - } - return replaced; - } - } - return replaced; - } - } + return setPropertyVersion(pom, profileId, property, value, false); + } - pom.rewind(); - return new PropertyVersionInternal().setPropertyVersion("", false); + /** + * Searches the pom re-defining the specified property to the specified version. + * + * @param pom The pom to modify. + * @param profileId The profile in which to modify the property. + * @param property The property to modify. + * @param value The new value of the property. + * @param insert Whether to insert a new property or just replace an existing one. + * @return true if a replacement was made. + * @throws XMLStreamException if somethinh went wrong. + */ + public static boolean setPropertyVersion( + MutableXMLStreamReader pom, String profileId, String property, String value, boolean insert) + throws XMLStreamException { + return new PropertyVersionInternal(pom, profileId, property, value).update(insert); } /** diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/rewriting/MutableXMLStreamReader.java b/versions-common/src/main/java/org/codehaus/mojo/versions/rewriting/MutableXMLStreamReader.java index 60d39c19e4..a0fef89164 100644 --- a/versions-common/src/main/java/org/codehaus/mojo/versions/rewriting/MutableXMLStreamReader.java +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/rewriting/MutableXMLStreamReader.java @@ -30,6 +30,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.apache.maven.shared.utils.io.IOUtil; import org.codehaus.stax2.LocationInfo; @@ -44,6 +46,7 @@ */ public class MutableXMLStreamReader extends StreamReader2Delegate implements AutoCloseable { private static final XMLInputFactory FACTORY = XMLInputFactory2.newInstance(); + private static final Pattern INDENTATION_PATTERN = Pattern.compile("^(\\s+)<", Pattern.MULTILINE); private StringBuilder source; @@ -386,4 +389,35 @@ public String toString() { return "MarkInfo[" + getStart() + ":" + getEnd() + "]"; } } + + /** + * Determine the line separator. + * + * @return line separator + */ + public String getLineSeparator() { + String text = getSource(); + if (text.contains("\r\n")) { + return "\r\n"; + } + if (text.contains("\r")) { + return "\r"; + } + return "\n"; + } + + /** + * Determine the indentation. + * + * @return indentation + */ + public String getIndentation() { + Pattern indentPattern = INDENTATION_PATTERN; + Matcher matcher = indentPattern.matcher(getSource()); + + if (matcher.find()) { + return matcher.group(1); + } + return ""; + } } diff --git a/versions-common/src/main/java/org/codehaus/mojo/versions/rewriting/PropertyVersionInternal.java b/versions-common/src/main/java/org/codehaus/mojo/versions/rewriting/PropertyVersionInternal.java new file mode 100644 index 0000000000..656b5ec8fd --- /dev/null +++ b/versions-common/src/main/java/org/codehaus/mojo/versions/rewriting/PropertyVersionInternal.java @@ -0,0 +1,380 @@ +package org.codehaus.mojo.versions.rewriting; + +/* + * Copyright MojoHaus and Contributors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import javax.xml.stream.XMLStreamException; + +/** + * Searches the pom re-defining or inserting the specified version property. + *

+ * This is an internal implementation class used by {@link org.codehaus.mojo.versions.api.PomHelper}. + * It should not be used directly by client code. + * + * @since 2.20.1 + */ +public class PropertyVersionInternal { + + private final MutableXMLStreamReader pom; + private final String profileId; + private final String propertyName; + private final String value; + private final String indentation; + private final String lineSeparator; + + private Template result = Template.NONE; + private boolean inProfileWithId; + + /** + * Marks. + */ + private enum Marks { + START, + END + } + + /** + * Represents the state of the parser. + */ + private enum State { + BEGIN_OF_STREAM, + PROJECT, + PROFILES, + PROFILE, + PROFILE_ID, + PROPERTIES, + PROPERTY, + PROJECT_END; + } + + /** + * Result template. + * + * Decides what to render between start and end marks. + */ + private enum Template { + /** + * Does not update anything. + */ + NONE { + @Override + boolean apply( + MutableXMLStreamReader pom, + String propertyName, + String value, + String indentation, + String lineSeparator) { + return false; + } + }, + /** + * Replaces an existing property value. + */ + VALUE { + @Override + boolean apply( + MutableXMLStreamReader pom, + String propertyName, + String value, + String indentation, + String lineSeparator) { + pom.replaceBetween(Marks.START, Marks.END, value); + return true; + } + }, + /** + * Inserts a missing property entry at the end of the {@code properties} container. + */ + PROJECT_PROPERTY { + @Override + boolean apply( + MutableXMLStreamReader pom, + String propertyName, + String value, + String indentation, + String lineSeparator) { + pom.replaceBetween( + Marks.START, + Marks.END, + String.format( + "%3$s<%1$s>%2$s%4$s%3$s", propertyName, value, indentation, lineSeparator)); + return true; + } + }, + /** + * Inserts a missing property entry at the end of the {@code profile}'s {@code properties} container. + */ + PROFILE_PROPERTY { + @Override + boolean apply( + MutableXMLStreamReader pom, + String propertyName, + String value, + String indentation, + String lineSeparator) { + pom.replaceBetween( + Marks.START, + Marks.END, + String.format( + "%3$s<%1$s>%2$s%4$s%3$s%3$s%3$s", + propertyName, value, indentation, lineSeparator)); + return true; + } + }, + /** + * Inserts a missing property container at the end of the {@code project} element. + */ + PROJECT_PROPERTIES { + @Override + boolean apply( + MutableXMLStreamReader pom, + String propertyName, + String value, + String indentation, + String lineSeparator) { + pom.replaceBetween( + Marks.START, + Marks.END, + String.format( + "%3$s%4$s%3$s%3$s<%1$s>%2$s%4$s%3$s%4$s", + propertyName, value, indentation, lineSeparator)); + return true; + } + }, + /** + * Inserts a missing property container at the end of the {@code profile} element. + */ + PROFILE_PROPERTIES { + @Override + boolean apply( + MutableXMLStreamReader pom, + String propertyName, + String value, + String indentation, + String lineSeparator) { + pom.replaceBetween( + Marks.START, + Marks.END, + String.format( + "%3$s%4$s%3$s%3$s%3$s%3$s<%1$s>%2$s%4$s%3$s%3$s%3$s%4$s%3$s%3$s", + propertyName, value, indentation, lineSeparator)); + return true; + } + }; + + /** + * Applies the template. + * + * @return {@code true} if the pom was modified + */ + abstract boolean apply( + MutableXMLStreamReader pom, + String propertyName, + String value, + String indentation, + String lineSeparator); + } + + /** + * Constructor. + * + * @param pom The pom to modify. + * @param profileId The profile in which to modify the property. + * @param propertyName The property to modify. + * @param value The new value of the property. + */ + public PropertyVersionInternal(MutableXMLStreamReader pom, String profileId, String propertyName, String value) { + this.pom = pom; + this.profileId = profileId; + this.propertyName = propertyName; + this.value = value; + this.indentation = pom.getIndentation(); + this.lineSeparator = pom.getLineSeparator(); + } + + /** + * Perform the actual update operation. + * + * @param insert Whether to insert a new property or just replace an existing one. + * @return {@code true} if the pom was modified + * @throws XMLStreamException if something went wrong. + */ + public boolean update(boolean insert) throws XMLStreamException { + pom.rewind(); + result = Template.NONE; + + State state = State.BEGIN_OF_STREAM; + while (result == Template.NONE && state != State.PROJECT_END && pom.hasNext()) { + pom.next(); + + if (pom.isStartElement()) { + state = handleStartElement(state, pom.getLocalName()); + } else if (pom.isEndElement()) { + state = handleEndElement(state, pom.getLocalName()); + } else { + if (state != State.PROPERTY) { + pom.mark(Marks.START); + } + } + } + + boolean replaced = false; + if (insert || result == Template.VALUE) { + replaced = result.apply(pom, propertyName, value, indentation, lineSeparator); + } + + pom.clearMark(Marks.START); + pom.clearMark(Marks.END); + return replaced; + } + + private State handleStartElement(State state, String name) throws XMLStreamException { + switch (state) { + case BEGIN_OF_STREAM: + if ("project".equals(name)) { + pom.mark(Marks.START); + return State.PROJECT; + } + break; + case PROJECT: + if (profileId == null) { + if ("properties".equals(name)) { + pom.mark(Marks.START); + return State.PROPERTIES; + } + } else { + if ("profiles".equals(name)) { + return State.PROFILES; + } + } + break; + case PROFILES: + if ("profile".equals(name)) { + return State.PROFILE; + } + break; + case PROFILE: + if ("id".equals(name)) { + return handleStartProfileId(); + } + if ("properties".equals(name) && inProfileWithId) { + pom.mark(Marks.START); + return State.PROPERTIES; + } + break; + case PROPERTIES: + if (propertyName.equals(name)) { + pom.mark(Marks.START); + return State.PROPERTY; + } + break; + default: + break; + } + + pom.skipElement(); + pom.mark(Marks.START); + return state; + } + + private State handleStartProfileId() throws XMLStreamException { + if (profileId.equals(pom.getElementText().trim())) { + inProfileWithId = true; + } + // getElementText could've pushed the pointer to END_ELEMENT + if (!pom.isEndElement()) { + return State.PROFILE_ID; + } + pom.mark(Marks.START); + return State.PROFILE; + } + + private State handleEndElement(State state, String name) { + switch (state) { + case PROJECT: + if ("project".equals(name)) { + return handleEndProject(); + } + break; + case PROFILES: + if ("profiles".equals(name)) { + return State.PROJECT; + } + break; + case PROFILE: + if ("profile".equals(name)) { + return handleEndProfile(); + } + break; + case PROFILE_ID: + if ("id".equals(name)) { + return State.PROFILE; + } + break; + case PROPERTIES: + if ("properties".equals(name)) { + return handleEndProperties(); + } + break; + case PROPERTY: + if (propertyName.equals(name)) { + return handleEndProperty(); + } + break; + default: + break; + } + return state; + } + + private State handleEndProject() { + if (result == Template.NONE && profileId == null) { + pom.mark(Marks.END); + result = Template.PROJECT_PROPERTIES; + } + return State.PROJECT_END; + } + + private State handleEndProfile() { + if (result == Template.NONE && inProfileWithId) { + pom.mark(Marks.END); + result = Template.PROFILE_PROPERTIES; + } + + inProfileWithId = false; + return State.PROFILES; + } + + private State handleEndProperties() { + if (profileId == null) { + if (result == Template.NONE) { + pom.mark(Marks.END); + result = Template.PROJECT_PROPERTY; + } + return State.PROJECT; + } + if (result == Template.NONE) { + pom.mark(Marks.END); + result = Template.PROFILE_PROPERTY; + } + return State.PROFILE; + } + + private State handleEndProperty() { + pom.mark(Marks.END); + result = Template.VALUE; + return State.PROPERTIES; + } +} diff --git a/versions-common/src/test/java/org/codehaus/mojo/versions/rewriting/PropertyVersionInternalTest.java b/versions-common/src/test/java/org/codehaus/mojo/versions/rewriting/PropertyVersionInternalTest.java new file mode 100644 index 0000000000..4962ed6634 --- /dev/null +++ b/versions-common/src/test/java/org/codehaus/mojo/versions/rewriting/PropertyVersionInternalTest.java @@ -0,0 +1,199 @@ +package org.codehaus.mojo.versions.rewriting; + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PropertyVersionInternalTest { + + @ParameterizedTest + @MethodSource("updatablePoms") + void testUpdate(String xml, String profileId) throws Exception { + MutableXMLStreamReader pom = new MutableXMLStreamReader( + new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), new File("pom.xml").toPath()); + + String oldVersion = "changeit"; + String newVersion = "1.2.345"; + boolean modified = new PropertyVersionInternal(pom, profileId, "myVersion", newVersion).update(true); + assertTrue(modified); + + String result = pom.getSource(); + + assertEquals(1, StringUtils.countMatches(result, "" + newVersion + "")); + assertEquals(0, StringUtils.countMatches(result, oldVersion)); + } + + static Stream updatablePoms() { + return Stream.of( + Arguments.of(simplePom(), null), + Arguments.of(simplePomWithPropertiesEmpty(), null), + Arguments.of(simplePomWithProperties(), null), + Arguments.of(simplePomWithPropertyExists(), null), + Arguments.of(simplePomWithPropertyNotEmpty(), null), + Arguments.of(simplePomWithProfile(), null), + Arguments.of(simplePomWithProfile(), "myProfile"), + Arguments.of(simplePomWithProfileAndPropertiesEmpty(), "myProfile"), + Arguments.of(simplePomWithProfileAndProperties(), "myProfile"), + Arguments.of(simplePomWithProfileAndPropertyExists(), "myProfile"), + Arguments.of(simplePomWithProfileAndPropertyNotEmpty(), "myProfile"), + Arguments.of(complexPomWithProfiles(), "myProfile")); + } + + static String simplePom() { + return ""; + } + + static String simplePomWithPropertiesEmpty() { + return "\n" + " \n" + " \n" + "\n"; + } + + static String simplePomWithProperties() { + return "\n" + + " \n" + + " someValue\n" + + " \n" + + "\n"; + } + + static String simplePomWithPropertyExists() { + return "\n" + + " \n" + + " someValue\n" + + " \n" + + " \n" + + "\n"; + } + + static String simplePomWithPropertyNotEmpty() { + return "\n" + + " \n" + + " someValue\n" + + " changeit\n" + + " \n" + + "\n"; + } + + static String simplePomWithProfile() { + return "\n" + + " \n" + + " \n" + + " myProfile\n" + + " \n" + + " \n" + + "\n"; + } + + static String simplePomWithProfileAndPropertiesEmpty() { + return "\n" + + " \n" + + " \n" + + " myProfile\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + } + + static String simplePomWithProfileAndProperties() { + return "\n" + + " \n" + + " \n" + + " myProfile\n" + + " \n" + + " someValue\n" + + " \n" + + " \n" + + " \n" + + "\n"; + } + + static String simplePomWithProfileAndPropertyExists() { + return "\n" + + " \n" + + " \n" + + " myProfile\n" + + " \n" + + " someValue\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + } + + static String simplePomWithProfileAndPropertyNotEmpty() { + return "\n" + + " \n" + + " \n" + + " myProfile\n" + + " \n" + + " someValue\n" + + " changeit\n" + + " \n" + + " \n" + + " \n" + + "\n"; + } + + static String complexPomWithProfiles() { + return "\n" + + " \n" + + " \n" + + " someValue\n" + + " mustNotChange\n" + + " \n\n" + + " \n" + + " " + + " \n\n" + + " \n" + + " \n" + + " \n" + + " someOtherProfile\n" + + " \n" + + " mustNotChange\n" + + " \n" + + " \n\n" + + " \n" + + " myProfile\n" + + " \n" + + " \n" + + " false\n" + + " \n" + + " \n" + + " someOtherValue\n" + + " changeit\n" + + " \n" + + " \n" + + " \n" + + "\n"; + } +} diff --git a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/SetPropertyMojo.java b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/SetPropertyMojo.java index 8561918291..51ea8bd065 100644 --- a/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/SetPropertyMojo.java +++ b/versions-maven-plugin/src/main/java/org/codehaus/mojo/versions/SetPropertyMojo.java @@ -96,6 +96,14 @@ public class SetPropertyMojo extends AbstractVersionsUpdaterMojo { @Parameter(property = "profileId") private String profileId = null; + /** + * Whether to insert a new version property or just replace an existing one. + * + * @since 2.21 + */ + @Parameter(property = "insert", defaultValue = "false") + private boolean insert; + /** * Whether to allow snapshots when searching for the latest version of an artifact. * @@ -201,7 +209,7 @@ private void update(MutableXMLStreamReader pom, List propertiesConfig, continue; } PomHelper.setPropertyVersion( - pom, profileToApply, currentProperty.getName(), defaultString(newVersionGiven)); + pom, profileToApply, currentProperty.getName(), defaultString(newVersionGiven), insert); } } diff --git a/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/SetPropertyMojoTest.java b/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/SetPropertyMojoTest.java index 11a70cbc3d..0dbaf115bc 100644 --- a/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/SetPropertyMojoTest.java +++ b/versions-maven-plugin/src/test/java/org/codehaus/mojo/versions/SetPropertyMojoTest.java @@ -240,4 +240,25 @@ private Model getModelForProfile( Paths.get(pomDir.toAbsolutePath().toString(), "pom.xml").toFile()); return model; } + + @Test + public void testSetNew() throws Exception { + String newVersion = UUID.randomUUID().toString(); + copyDir(Paths.get("src/test/resources/org/codehaus/mojo/set-property/issue-1268"), pomDir); + Path childPomDir = pomDir.resolve("child"); + SetPropertyMojo mojo = (SetPropertyMojo) mojoRule.lookupConfiguredMojo(childPomDir.toFile(), "set-property"); + + mojo.repositorySystem = mock(org.eclipse.aether.RepositorySystem.class); + when(mojo.repositorySystem.resolveVersionRange(any(), any(VersionRangeRequest.class))) + .then(i -> new VersionRangeResult(i.getArgument(1))); + + setVariableValueToObject(mojo, "newVersion", newVersion); + setVariableValueToObject(mojo, "property", "dummy-api-version"); + setVariableValueToObject(mojo, "insert", true); + mojo.execute(); + + Model model = PomHelper.getRawModel( + Paths.get(childPomDir.toAbsolutePath().toString(), "pom.xml").toFile()); + assertThat(model.getProperties().getProperty("dummy-api-version"), is(newVersion)); + } } diff --git a/versions-maven-plugin/src/test/resources/org/codehaus/mojo/set-property/issue-1268/child/pom.xml b/versions-maven-plugin/src/test/resources/org/codehaus/mojo/set-property/issue-1268/child/pom.xml new file mode 100644 index 0000000000..51848d0213 --- /dev/null +++ b/versions-maven-plugin/src/test/resources/org/codehaus/mojo/set-property/issue-1268/child/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + group + parent + 1.0 + + child + + + localhost + dummy-api + ${dummy-api-version} + + + diff --git a/versions-maven-plugin/src/test/resources/org/codehaus/mojo/set-property/issue-1268/pom.xml b/versions-maven-plugin/src/test/resources/org/codehaus/mojo/set-property/issue-1268/pom.xml new file mode 100644 index 0000000000..32af7ba082 --- /dev/null +++ b/versions-maven-plugin/src/test/resources/org/codehaus/mojo/set-property/issue-1268/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + group + parent + 1.0 + pom + + child + + + 2.0 + +