Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.telenav.cactus.maven.refactoring.VersionUpdateFinder Maven / Gradle / Ivy
Go to download
API for manipulating versions consistently across large trees
of maven projects with an eye to ensuring the result is consistent
and buildable.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// © 2011-2022 Telenav, Inc.
//
// 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
//
// https://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.
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.telenav.cactus.maven.refactoring;
import com.mastfrog.function.state.Bool;
import com.mastfrog.util.preconditions.Exceptions;
import com.telenav.cactus.maven.model.MavenCoordinates;
import com.telenav.cactus.maven.model.Pom;
import com.telenav.cactus.maven.model.PomVersion;
import com.telenav.cactus.maven.model.VersionChange;
import com.telenav.cactus.maven.model.VersionChangeMagnitude;
import com.telenav.cactus.maven.model.VersionFlavor;
import com.telenav.cactus.maven.model.VersionFlavorChange;
import com.telenav.cactus.maven.model.published.PublishChecker;
import com.telenav.cactus.scope.ProjectFamily;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import static com.telenav.cactus.maven.model.PomVersion.mostCommonVersion;
import static com.telenav.cactus.maven.model.VersionChangeMagnitude.DOT;
import static com.telenav.cactus.maven.model.VersionFlavorChange.UNCHANGED;
import static com.telenav.cactus.maven.refactoring.PomRole.CONFIG;
import static com.telenav.cactus.maven.refactoring.PomRole.CONFIG_ROOT;
import static com.telenav.cactus.maven.refactoring.PropertyChange.propertyChange;
import static com.telenav.cactus.scope.ProjectFamily.familyOf;
/**
*
* @author timb
*/
class VersionUpdateFinder
{
final VersionChangeUpdatesCollector changes;
final PomCategorizer categories;
private final VersionIndicatingProperties potentialPropertyChanges;
private final Map familyVersionChanges;
private final Map syntheticFamilyVersionChanges = new HashMap<>();
private final Set completedFamilies = new HashSet<>();
private final SuperpomBumpPolicy superpomBumpPolicy;
private final VersionMismatchPolicy versionMismatchPolicy;
private final PublishChecker publishChecker;
VersionUpdateFinder(
VersionChangeUpdatesCollector changes,
PomCategorizer categories,
VersionIndicatingProperties potentialPropertyChanges,
Map familyVersionChanges,
SuperpomBumpPolicy superpomBumpPolicy,
VersionMismatchPolicy versionMismatchPolicy,
PublishChecker publishChecker)
{
this.changes = changes;
this.categories = categories;
this.potentialPropertyChanges = potentialPropertyChanges;
this.familyVersionChanges = familyVersionChanges;
this.superpomBumpPolicy = superpomBumpPolicy;
this.versionMismatchPolicy = versionMismatchPolicy;
this.publishChecker = publishChecker;
}
public void go()
{
boolean noFamilyChanges = familyVersionChanges.isEmpty();
Map mismatchOutcomes = null;
// Each round can cause new version changes to be added to the set
// of edits we're making, so iterate until nothing makes a change.
// In particular, if a parent pom change (likely a property edit)
// causes a cascading parent change through an entire family, we will
// detect that and synthesize an update to that family
do
{
if (noFamilyChanges && !changes.pomVersionChanges().isEmpty())
{
cascadeCurrentChanges();
}
// We will aggregate the version mismatch outcomes (from the
// VersionMismatchPolicy) so we can abort the build if we need to.
Map outcomes = findUpdates();
if (mismatchOutcomes == null)
{
mismatchOutcomes = outcomes;
}
else
{
mismatchOutcomes.putAll(outcomes);
}
}
while (changes.hasChanges());
pruneDuplicateVersions();
findSuperpomsThatNeedBumping();
// See if we need to abort and do so
StringBuilder abortMessage = new StringBuilder();
mismatchOutcomes.forEach((pom, outcome) ->
{
// It may have been resolved in a later round
if (versionMismatches().contains(pom))
{
switch (outcome)
{
// The version adjusting has already happened - we just
// need to collect any that will cause us to abort the build
case ABORT:
if (abortMessage.length() > 0)
{
abortMessage.append(", ");
}
abortMessage.append(pom);
}
}
});
if (abortMessage.length() > 0)
{
throw new IllegalStateException("Encountered version mismatches "
+ "with outcome ABORT: " + abortMessage);
}
}
private void findSuperpomsThatNeedBumping()
{
// Bugfix: With kivakit 1.6.2, we missed updating
// telenav-superpom-intermediate-bom and telenav-superpom
// because we were updating three families that did not include
// the superpom family "telenav" - we need to be sure that we
// capture cases where there is no choice but to publish a new
// superpom, because otherwise the release will fail (nastily - c.f.
// https://issues.sonatype.org/browse/OSSRH-82713
// )
if (publishChecker != null && superpomBumpPolicy.isBumpVersion())
{
Set alsoBump = new HashSet<>();
categories.eachPomWithRoleIn(pom ->
{
// 1. First see if we are already updating it; skip if so
if (changes.hasVersionUpdateFor(pom))
{
return;
}
Set possibleUpdates = new HashSet<>();
categories.childrenOf(pom).forEach(child ->
{
boolean alreadyChanging = changes.hasParentUpdateFor(
child);
if (!alreadyChanging)
{
possibleUpdates.add(child);
}
});
// 2. Then see if we are updating any poms it is the ancestor of
// and they are in the target families we're working on
Set allDescendants = new HashSet<>();
for (Pom child : possibleUpdates)
{
allDescendants.addAll(categories.descendantsOf(child));
}
boolean hasDescendantInTargetFamilies = false;
for (Pom desc : allDescendants)
{
if (familyVersionChanges.containsKey(familyOf(desc)))
{
hasDescendantInTargetFamilies = true;
break;
}
}
// 3. Then see if it is published and the published version differs
boolean needRepublish = false;
if (hasDescendantInTargetFamilies)
{
try
{
needRepublish = publishChecker.check(pom).differs();
}
catch (IOException | InterruptedException
| URISyntaxException ex)
{
Exceptions.chuck(ex);
}
}
// 4. If so, add a bump for it and cascade
if (needRepublish)
{
PomVersion oldVersion = pom.version();
oldVersion.updatedWith(superpomBumpPolicy
.minimalMagnitudeFor(
oldVersion), superpomBumpPolicy
.changeFor(oldVersion))
.ifPresent(newVersion ->
{
VersionChange.versionChange(oldVersion,
newVersion).ifPresent(versionChange ->
{
changes.changeParentVersion(pom,
pom,
versionChange);
cascadeChange(pom, versionChange);
});
});
}
}, PomRole.CONFIG, PomRole.CONFIG_ROOT);
}
}
private Map findUpdates()
{
collectPropertyChanges();
applyFamilyVersionChanges();
processVersionMismatches();
updateSyntheticFamilyVersionChanges();
return applyVersionMismatchPolicy();
}
void cascadeInitialPomChanges()
{
// If we have some specific pom changes we were handed that will have
// ripples, make them ripple now so we don't clobber them with family changes
pomVersionChanges().forEach((pom, vc) -> cascadeChange(pom, vc));
}
Map allFamilyChanges()
{
Map map = new HashMap<>(
familyVersionChanges);
map.putAll(syntheticFamilyVersionChanges);
return map;
}
private boolean updateSyntheticFamilyVersionChanges()
{
// If we have updated all of the poms in a given family, it is
// effectively
Bool changed = Bool.create();
for (ProjectFamily fam : categories.families())
{
if (familyVersionChanges.containsKey(fam) || syntheticFamilyVersionChanges
.containsKey(fam))
{
continue;
}
if (isFamilyEffectivelyUpdated(fam))
{
// Find the new project version we have been bumping to
Set changedPomsInFamily = changes.changedPomsInFamily(fam);
mostCommonVersion(changedPomsInFamily)
.ifPresent(mostCommonVersion -> // it will be present unless collection empty
{
// Find the old project version we have
mostCommonVersion(categories.pomsForFamily(fam))
.ifPresent(
oldVersion ->
{
// Create a new change
oldVersion.to(mostCommonVersion)
.ifPresent(newChange -> // if they are not the same
{
changed.set();
// Create a new effective family version change,
// and the next round will apply it
syntheticFamilyVersionChanges
.put(fam, newChange);
});
});
});
}
}
return changed.getAsBoolean();
}
private void collectPropertyChanges()
{
// Iterate all of the properties we know about, and add PropertyChange
// instances for any that represent things we need to change in poms
// the filter allows us to change.
categories.allPoms().forEach(pom ->
{
// Iterate all the properties that are in this pom:
this.potentialPropertyChanges.collectMatches(pom,
(role, versionProperty) ->
{
// A place where we can track whether or not somthing has
// changed, in which case, if we are modifying a pom, we
// must also update its version, which we will do at the end.
Bool changed = Bool.create();
// The property either points to a property or a family - figure
// out which, and look up the new version (if any) for it, and
// generate a PropertyChange for that property if the filter
// allows it.
if (role.isProject())
{
// A property like cactus.maven.plugin.version that references
// the version of a specific project
MavenCoordinates coords = (MavenCoordinates) versionProperty
.pointsTo();
// Find the project the property refers to
categories.pomFor(coords).ifPresent(
pomOfReVersionedProject ->
{
// Look up the new version of that project - we can skip
// looking up on the family here, as we will already have
// that
VersionChange change;
// If the pom has a tage of its own, we need
// to look in our set of version changes for poms
if (pomOfReVersionedProject.hasExplicitVersion())
{
change = pomVersionChanges().get(
pomOfReVersionedProject);
}
else
{
// Otherwise, any change we have will be a parent
// version change
//
// But since we might be explicitly adding a version,
// do look in the pom version changes first:
change = pomVersionChanges().get(
pomOfReVersionedProject);
if (change == null)
{
change = parentVersionChanges().get(
pomOfReVersionedProject);
}
}
if (change != null)
{
// Get the update version, which may be the old version
// from the change if the property represents the previous
// version of a library or family
PomVersion newValue = change.version(
role.isPrevious());
// It is possible that - for whatever reason - the property
// may already be set to the value we want. The optional
// returned by propertyChange() will be empty in that case,
// so we don't generate superfluous changes.
propertyChange(versionProperty, newValue)
.ifPresent(newPropertyChange ->
{
// Add our change (which may have already been added),
// setting the hasChanges flag in changes if the
// change is new
changed.set(changes.changeProperty(
pom,
newPropertyChange)
.isChange());
});
}
});
}
else
{
// We are targeting a project family
ProjectFamily family = (ProjectFamily) versionProperty
.pointsTo();
// Look up the version change for the family
VersionChange familyVersionChange = familyVersionChanges
.get(family);
// It is possible that the version was explicitly passed
// to one of the public methods on this class, so in that
// case, prefer the version tied to the specific project,
// in case that is different
if (pomVersionChanges().containsKey(pom))
{
// If this is a configuration superpom that is getting its
// version bumped independently of the family version, we do NOT
// want to accidentally set a property to that version instead
// of the right one
VersionChange pomChange = pomVersionChanges().get(pom);
if (pomChange.oldVersion().is(
versionProperty.oldValue()))
{
familyVersionChange = pomVersionChanges().get(pom);
}
else
{
// changes.addVersionMismatch(pom);
changed.set(true); // HUH?
}
}
if (familyVersionChange != null)
{
// Get our new version, which may be the previous version if that's
// what the property represents (e.g. cactus.prev.version for
// processing cactus libraries)
PomVersion newVersion = familyVersionChange.version(role
.isPrevious());
// Again, if this is not going to result in the property value
// being any different, then this will return empty() and we
// are done - the property could already be where we want to
// put it.
propertyChange(versionProperty, newVersion)
.ifPresent(newPropertyChange ->
{
// Set the property, and if this actually
// results in a change to our set of things
// to do, set the flag so we make sure to
// bump the version of the pom we are
// altering a property in
boolean reallyHaveAChange
= changes.changeProperty(pom,
newPropertyChange)
.isChange();
changed.set(reallyHaveAChange);
});
}
}
changed.ifTrue(() ->
{
// If we have a configuration superpom, it may be one that
// will show up as a version mismatch because it has its own
// versioning scheme. Take care of updating it now.
//
// If it has the normal family versioning scheme, then
// the code that applies version changes to everything
// in a family will take care of it.
if (categories.is(pom, CONFIG) || categories.is(pom,
CONFIG_ROOT))
{
ProjectFamily fam = familyOf(pom);
VersionChange vc = familyVersionChanges.get(fam);
if (vc != null && vc.oldVersion().equals(pom.version()))
{
// If the pom has the same current version as we expect
// for the whole family, just change to that version
if (changes.changePomVersion(pom, vc).isChange())
{
cascadeChange(pom, vc);
}
}
else
{
// We have a superpom with some different version. Apply
// the superpomBumpPolicy
Consumer newVersionConsumer = nue ->
{
pom.version().to(nue).ifPresent(
updatedPomVersion ->
{
if (changes.changePomVersion(pom,
updatedPomVersion).isChange())
{
// If we're changing a parent pom's version,
// walk all of its children and apply the
// appropriate parent change to them
cascadeChange(pom, updatedPomVersion);
}
});
};
if (vc != null && superpomBumpPolicy.isBumpVersion())
{
pom.version().updatedWith(
superpomBumpPolicy.magnitudeFor(vc),
superpomBumpPolicy.changeFor(
vc.newVersion()))
.ifPresent(newVersionConsumer);
}
else
{
if (superpomBumpPolicy.isBumpVersion())
{
pom.version().updatedWith(DOT, UNCHANGED)
.ifPresent(newVersionConsumer);
}
}
}
}
});
});
});
}
private void applyFamilyVersionChanges()
{
// Iterate all of the family version changes, and record them as
// applying to all poms in that family unless some other version change
// specific to that pom was already recorded.
allFamilyChanges().forEach((family, expectedVersionChange) ->
{
// No need to process a family more than once - it will not
// get any more complete than complete
if (completedFamilies.contains(family))
{
return;
}
// Go through all the poms
categories.eachPomInFamily(family, pom ->
{
// If we already have a change (perhaps from a caller adding
// an explicit version change), then skip this pom
if (!pomVersionChanges().containsKey(pom) && !versionMismatches()
.contains(pom))
{
// Check if the current version is what we expect - otherwise
// we will need to add a mismatch to possibly resolve with
// the policy, or in the next round
boolean match = expectedVersionChange.oldVersion()
.isVersionOf(pom);
if (!match)
{
// a mismatch - the version we're changing FROM is not
// what we are expecting it to be
changes.addVersionMismatch(pom);
}
else
if (match)
{
// If it has its own tag, change its version
if (pom.hasExplicitVersion())
{
changes.changePomVersion(pom,
expectedVersionChange);
}
else
{
Optional par = categories.parentOf(pom);
if (par.isPresent())
{
// Else the version comes from its parent so we
// will change that
VersionChangeUpdatesCollector.ChangeResult result = changes
.changeParentVersion(pom,
par.get(),
expectedVersionChange);
// If the filter blocks us bumping the parent version,
// we may need to add an explicit version to a pom
// that didn't have one so that it gets a new version even
// if we aren't allowed to touch the parent
if (result.isFiltered())
{
changes.changePomVersion(pom,
expectedVersionChange);
}
}
}
}
}
});
completedFamilies.add(family);
});
}
private void processVersionMismatches()
{
// Now see if we have version mismatches in poms which are superpoms -
// they have no modules and are "pom" packaging - these should just be
// bumped, as we allow superpoms to have their own versioning schemes
if (!versionMismatches().isEmpty() && superpomBumpPolicy.isBumpVersion())
{
eachVersionMismatch((pom, familyOrPomChange) ->
{
// We are only interested in poms that provide configuration
// information - i.e. parents of something
if (categories.is(pom, CONFIG)
|| categories.is(pom, CONFIG_ROOT))
{
VersionFlavorChange flavorChange = VersionFlavorChange.UNCHANGED;
VersionChangeMagnitude magnitudeChange = VersionChangeMagnitude.DOT;
// If we have a family change, compute a new version based on
// the kind of change (magnitude, flavor) it is making.
if (familyOrPomChange != null)
{
// Check that it is really caused by a family change.
// We will make requested changes consistent with the
// version request for that family; side-effect
// changes (a parent of some family not directly being
// modified changed, so we have to cascade parent pom
// version updates up through all of its children)
// should not mess with the suffix
if (familyVersionChanges.containsKey(familyOf(pom)))
{
flavorChange = superpomBumpPolicy.changeFor(
familyOrPomChange.newVersion());
magnitudeChange = superpomBumpPolicy.magnitudeFor(
familyOrPomChange);
}
}
// Compute our new version
PomVersion newVersion = pom.version().updatedWith(
magnitudeChange, flavorChange).get();
VersionChange vc = new VersionChange(pom.version(),
newVersion);
if (!pom.hasExplicitVersion())
{
// If the pom does not have its own tag, then
// we either need to add a version tag to it overriding
// that from its parent (if the parent will not be changed)
// or we need to update its parent's version (in which case
// we will pick up that this pom needs updating for the new
// parent version on the next round).
List parents = categories.parents(pom);
for (Pom par : parents)
{
if (par.version().equals(pom.version()))
{
if (par.hasExplicitVersion())
{
changes.changePomVersion(par, vc);
}
else
{
Optional parPar = categories.parentOf(
pom);
if (parPar.isPresent())
{
if (changes.changeParentVersion(par,
parPar.get(), vc)
.isFiltered())
{
// If the filter won't let us change the parent
// version, we need to insert a new version tag
// into the pom
changes.changePomVersion(pom, vc);
}
}
}
return true;
}
}
}
else
{
changes.changePomVersion(pom, vc);
return true;
}
return true;
}
return false;
});
}
}
private Map applyVersionMismatchPolicy()
{
// For any remaining mismatches, apply the mismatch policy to
// resolve them
Map outcomes = new HashMap<>();
eachVersionMismatch((pom, expectedChangeOrNull) ->
{
if (expectedChangeOrNull != null)
{
VersionMismatchPolicyOutcome outcome
= versionMismatchPolicy.mismatchEncountered(pom,
expectedChangeOrNull,
categories.rolesFor(pom));
outcomes.put(pom, outcome);
switch (outcome)
{
case ABORT:
// Leave it as a version mismatch by returning false
break;
case SKIP:
// Mark the mismatch resolved by returning true to
// remove it from the set of mismatches
return true;
case COERCE_TO_TARGET_VERSION:
// Just record a version change to the thing it's
// supposed to be
changes.changePomVersion(pom, expectedChangeOrNull);
return true;
case BUMP:
// Bump the version of it from whatever it is now, to
// that + 1, changing the same magnitude decimal as
// the version change for the family, and bringing it
// to whatever flavor the version change for the family
// uses.
VersionFlavor newFlavor
= expectedChangeOrNull.newVersion().flavor();
Optional res = pom.version().updatedWith(
VersionChangeMagnitude.DOT,
VersionFlavorChange.between(expectedChangeOrNull
.oldVersion().flavor(), newFlavor));
if (res.isPresent())
{
changes.changePomVersion(pom, new VersionChange(pom
.version(), res.get()));
}
// If res.isAbsent() then the mismatch is because the
// computed new version is what the pom is already at,
// so return true and remove it from the list of
// mismatches either way - it is inconsistent, but it is
// inconsistent because it is already at the version we
// are trying to bring it to. This can happen if there
// was a previous version update that failed somehow.
return true;
default:
throw new AssertionError(outcome);
}
}
return false;
});
return outcomes;
}
private void cascadeChange(Pom pom, VersionChange chg)
{
for (Pom kid : categories.childrenOf(pom))
{
if (!parentVersionChanges().containsKey(kid))
{
if (changes.changeParentVersion(kid, pom, chg).isChange())
{
cascadeChange(kid, chg);
}
}
}
}
/**
* Iterate all the version mismatches, passing each one's pom and the
* version (may be null) we are trying to change it to.
*
* @param c A biconsumer
*/
private void eachVersionMismatch(BiPredicate c)
{
// Need a defensive copy because we may not just remove from
// but add to the collection while iterating
for (Iterator it = new HashSet<>(versionMismatches()).iterator(); it
.hasNext();)
{
Pom pom = it.next();
// Something we called may have already triggered removal
if (!versionMismatches().contains(pom))
{
continue;
}
VersionChange change = this.pomVersionChanges().get(pom);
if (change == null)
{
change = this.familyVersionChanges.get(familyOf(pom));
}
if (change == null || c.test(pom, change))
{
changes.removeVersionMismatch(pom);
}
}
}
private boolean isFamilyEffectivelyUpdated(ProjectFamily family)
{
Map> javaProjects = categories
.projectsInFamilyWithRole(PomRole.JAVA);
Map> boms = categories.projectsInFamilyWithRole(
PomRole.BILL_OF_MATERIALS);
Set mix = new HashSet<>();
if (javaProjects.containsKey(family))
{
mix.addAll(javaProjects.get(family));
}
if (boms.containsKey(family))
{
mix.addAll(boms.get(family));
}
return changes.allChangedPoms().containsAll(mix);
}
private Map pomVersionChanges()
{
return changes.pomVersionChanges();
}
private Map parentVersionChanges()
{
return changes.parentVersionChanges();
}
private Set versionMismatches()
{
return changes.versionMismatches();
}
private void cascadeCurrentChanges()
{
// We need this for single superpom changes
HashMap ch = new HashMap<>(changes
.pomVersionChanges());
Set seen = new HashSet<>();
while (!ch.isEmpty())
{
ch.forEach((pom, ver) ->
{
seen.add(pom);
cascadeChange(pom, ver);
});
ch.putAll(changes.pomVersionChanges());
for (Pom p : seen)
{
ch.remove(p);
}
}
}
private void pruneDuplicateVersions()
{
new HashMap<>(changes.pomVersionChanges()).forEach((pom, verChange) ->
{
VersionChange parentChange = changes.parentVersionChanges().get(pom);
if (parentChange != null && verChange.newVersion().equals(
parentChange.version()))
{
categories.parentOf(pom).ifPresent(parentPom ->
{
if (!pom.hasExplicitVersion())
{
changes.removePomVersionChange(pom);
}
});
}
});
}
}