blob: 4ae3664454a2057b488c3726535bbaecd108cd3d [file] [log] [blame]
/*
* Copyright (c) 2010-2021 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.rt.dataobject.migration;
import static org.eclipse.scout.rt.platform.util.Assertions.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.eclipse.scout.rt.dataobject.DataObjectInventory;
import org.eclipse.scout.rt.dataobject.DoEntity;
import org.eclipse.scout.rt.dataobject.IDoEntity;
import org.eclipse.scout.rt.dataobject.ITypeVersion;
import org.eclipse.scout.rt.platform.ApplicationScoped;
import org.eclipse.scout.rt.platform.BEANS;
import org.eclipse.scout.rt.platform.CreateImmediately;
import org.eclipse.scout.rt.platform.exception.PlatformException;
import org.eclipse.scout.rt.platform.inventory.ClassInventory;
import org.eclipse.scout.rt.platform.inventory.IClassInfo;
import org.eclipse.scout.rt.platform.inventory.IClassInventory;
import org.eclipse.scout.rt.platform.namespace.INamespace;
import org.eclipse.scout.rt.platform.namespace.NamespaceVersion;
import org.eclipse.scout.rt.platform.namespace.NamespaceVersionedModel;
import org.eclipse.scout.rt.platform.namespace.NamespaceVersionedModel.VersionedItems;
import org.eclipse.scout.rt.platform.namespace.Namespaces;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
/**
* Inventory of data object structure migration related classes (namespaces, versions, migration handlers, context data
* classes).
*/
@ApplicationScoped
@CreateImmediately // apply validations at startup
public class DoStructureMigrationInventory {
protected final LinkedHashSet<String> m_namespaces = new LinkedHashSet<>();
protected final LinkedHashSet<NamespaceVersion> m_orderedVersions = new LinkedHashSet<>(); // ordered versions according to VersionedItemInventory
protected ByNamespaceVersionComparator m_comparator = null;
// only one migration handler per type version and type name can exist
protected final Map<NamespaceVersion, Map<String /* type name */, IDoStructureMigrationHandler>> m_migrationHandlers = new HashMap<>();
protected final Map<String /* type name */, Set<Class<? extends IDoStructureMigrationTargetContextData>>> m_doContextDataClassByTypeName = new HashMap<>();
// Contains subclasses (replaced data objects) of classes within DoStructureMigrationContextDataTarget#doEntityClasses too.
// This is a performance optimization so that lookup via getDoMigrationContextValues doesn't need to resolve parent classes.
protected final Map<Class<? extends IDoEntity>, Set<Class<? extends IDoStructureMigrationTargetContextData>>> m_doContextDataClassByDoEntityClass = new HashMap<>();
/**
* For each type name the versions with available migration handlers.
*/
protected final Map<String, List<NamespaceVersion>> m_typeNameVersions = new HashMap<>();
/**
* Based on current data from {@link DataObjectInventory}.
*/
protected final Map<String, NamespaceVersion> m_typeNameToCurrentTypeVersion = new HashMap<>();
/**
* @return All namespaces (sorted).
*/
protected List<INamespace> getAllNamespaces() {
return Namespaces.get().all();
}
/**
* @return All type versions.
*/
protected Collection<ITypeVersion> getAllTypeVersions() {
return BEANS.all(ITypeVersion.class);
}
/**
* @return All context data classes ({@link IDoStructureMigrationTargetContextData} annotated with
* {@link DoStructureMigrationContextDataTarget}).
*/
protected Collection<Class<? extends IDoStructureMigrationTargetContextData>> getAllContextDataClasses() {
//noinspection unchecked
return ClassInventory.get().getKnownAnnotatedTypes(DoStructureMigrationContextDataTarget.class)
.stream()
.filter(IClassInfo::isInstanciable)
.map(IClassInfo::resolveClass)
.filter(IDoStructureMigrationTargetContextData.class::isAssignableFrom)
.map(clazz -> (Class<? extends IDoStructureMigrationTargetContextData>) clazz)
.collect(Collectors.toList());
}
/**
* @return All migration handlers (sorted).
*/
protected List<IDoStructureMigrationHandler> getAllMigrationHandlers() {
return BEANS.all(IDoStructureMigrationHandler.class);
}
@PostConstruct
protected void init() {
// Collect namespaces
getAllNamespaces().stream().map(INamespace::getId).forEach(m_namespaces::add);
// Collect and build version model
NamespaceVersionedModel<ITypeVersion> model = createDefaultModel();
VersionedItems<ITypeVersion> items = model.getItems();
assertTrue(items.isValid(), "Type version model is not valid");
items.getItems().stream().map(ITypeVersion::getVersion).forEach(m_orderedVersions::add);
m_comparator = new ByNamespaceVersionComparator(new ArrayList<>(m_orderedVersions));
// Collect current type name/type versions
DataObjectInventory dataObjectInventory = BEANS.get(DataObjectInventory.class);
for (Entry<String, Class<? extends IDoEntity>> entry : dataObjectInventory.getTypeNameToClassMap().entrySet()) {
String typeName = entry.getKey();
NamespaceVersion typeVersion = dataObjectInventory.getTypeVersion(entry.getValue());
if (typeVersion != null) {
m_typeNameToCurrentTypeVersion.put(typeName, typeVersion);
}
}
// Collect IDataObjectDoMigrationContextValue
IClassInventory classInventory = ClassInventory.get();
for (Class<? extends IDoStructureMigrationTargetContextData> contextValueClass : getAllContextDataClasses()) {
DoStructureMigrationContextDataTarget annotation = contextValueClass.getAnnotation(DoStructureMigrationContextDataTarget.class);
if (annotation.typeNames() != null) {
for (String typeName : annotation.typeNames()) {
Set<Class<? extends IDoStructureMigrationTargetContextData>> contextDataClasses = m_doContextDataClassByTypeName.computeIfAbsent(typeName, k -> new HashSet<>());
contextDataClasses.add(contextValueClass);
}
}
if (annotation.doEntityClasses() != null) {
for (Class<? extends IDoEntity> doEntityClass : annotation.doEntityClasses()) {
assertFalse(doEntityClass == IDoEntity.class || doEntityClass == DoEntity.class, "{}: {} and {} are invalid do entity classes for the annotation {}",
contextValueClass, IDoEntity.class.getSimpleName(), DoEntity.class.getSimpleName(), DoStructureMigrationContextDataTarget.class.getSimpleName());
// class itself
m_doContextDataClassByDoEntityClass.computeIfAbsent(doEntityClass, k -> new HashSet<>()).add(contextValueClass);
// all known subclasses
//noinspection unchecked
classInventory.getAllKnownSubClasses(doEntityClass).stream()
.map(IClassInfo::resolveClass) // all subclasses of doEntityClass implement IDoEntity too, thus cast is okay
.forEach(clazz -> m_doContextDataClassByDoEntityClass.computeIfAbsent((Class<? extends IDoEntity>) clazz, k -> new HashSet<>()).add(contextValueClass));
}
}
}
// Collect migration handlers
Map<NamespaceVersion, Map<String /* type name */, List<IDoStructureMigrationHandler>>> migrationHandlersPerVersionAndTypeName = new HashMap<>();
Map<String, Set<NamespaceVersion>> unorderedTypeNameVersions = new HashMap<>();
for (IDoStructureMigrationHandler migrationHandler : getAllMigrationHandlers()) {
validateMigrationHandler(migrationHandler);
// Collect migration handlers
Map<String, List<IDoStructureMigrationHandler>> migrationHandlersPerTypeName = migrationHandlersPerVersionAndTypeName.computeIfAbsent(migrationHandler.toTypeVersion(), k -> new HashMap<>());
for (String typeName : migrationHandler.getTypeNames()) {
List<IDoStructureMigrationHandler> migrationHandlers = migrationHandlersPerTypeName.computeIfAbsent(typeName, k -> new ArrayList<>());
migrationHandlers.add(migrationHandler);
// there might be several migration handlers for the same type name and type version, thus use set to eliminate duplicates (sorting occurs afterwards)
Set<NamespaceVersion> versions = unorderedTypeNameVersions.computeIfAbsent(typeName, k -> new HashSet<>());
versions.add(migrationHandler.toTypeVersion());
}
}
validateMigrationHandlerUniqueness(migrationHandlersPerVersionAndTypeName);
// validation takes care that there is only one migration handler per type version and type name
migrationHandlersPerVersionAndTypeName.forEach((version, migrationHandlersPerTypeName) -> {
Map<String, IDoStructureMigrationHandler> migrationHandlerPerTypeName = new HashMap<>();
migrationHandlersPerTypeName.forEach((typeName, migrationHandlers) -> migrationHandlerPerTypeName.put(typeName, CollectionUtility.firstElement(migrationHandlers)));
m_migrationHandlers.put(version, migrationHandlerPerTypeName);
});
// Sort and add versions to m_typeNameVersions.
for (Entry<String, Set<NamespaceVersion>> entry : unorderedTypeNameVersions.entrySet()) {
List<NamespaceVersion> versions = new ArrayList<>(entry.getValue());
versions.sort(m_comparator);
m_typeNameVersions.put(entry.getKey(), versions);
}
}
protected void validateMigrationHandlerUniqueness(Map<NamespaceVersion, Map<String, List<IDoStructureMigrationHandler>>> migrationHandlersPerVersionAndTypeName) {
// Verify that there there is not more than one migration handler per type version and type name
StringBuilder duplicateBuilder = new StringBuilder();
for (Entry<NamespaceVersion, Map<String, List<IDoStructureMigrationHandler>>> versionEntry : migrationHandlersPerVersionAndTypeName.entrySet()) {
NamespaceVersion version = versionEntry.getKey();
for (Entry<String, List<IDoStructureMigrationHandler>> migrationHandlerEntry : versionEntry.getValue().entrySet()) {
if (migrationHandlerEntry.getValue().size() > 1) {
if (duplicateBuilder.length() > 0) {
duplicateBuilder.append("\n");
}
duplicateBuilder.append(migrationHandlerEntry.getKey());
duplicateBuilder.append("@");
duplicateBuilder.append(version.unwrap());
duplicateBuilder.append(": ");
duplicateBuilder.append(migrationHandlerEntry.getValue().stream().map(Object::getClass).map(Class::getSimpleName).collect(Collectors.joining(", ")));
}
}
}
if (duplicateBuilder.length() > 0) {
throw new PlatformException("Found multiple migration handlers for the same type version/type name:\n{}", duplicateBuilder.toString());
}
}
protected NamespaceVersionedModel<ITypeVersion> createDefaultModel() {
return NamespaceVersionedModel.<ITypeVersion> newBuilder()
.withNames(getAllNamespaces().stream().map(INamespace::getId).collect(Collectors.toList()))
.withItems(getAllTypeVersions())
.build();
}
/**
* Validations:
* <ul>
* <li>There are type names
* <li>Verifies that the target version ({@link IDoStructureMigrationHandler#toTypeVersion()}) of the migration
* handler are known versions
* <li>For all type names there is a type version as least as high as the target type version of the migration handler
* </ul>
*/
protected <T extends IDoStructureMigrationHandler> T validateMigrationHandler(T migrationHandler) {
// Check for missing type names
if (CollectionUtility.isEmpty(migrationHandler.getTypeNames())) {
throw new PlatformException("Migration handler {} doesn't have any type names", migrationHandler);
}
// Check for invalid type names
if (CollectionUtility.containsAny(migrationHandler.getTypeNames(), null, "")) {
throw new PlatformException("Migration handler {} has invalid type names (empty, null)", migrationHandler);
}
// Check for unknown toTypeVersion
NamespaceVersion toTypeVersion = migrationHandler.toTypeVersion();
if (!m_orderedVersions.contains(toTypeVersion)) {
throw new PlatformException("Unknown toTypeVersion value {}. Make sure that the type version value is registered within a {}", toTypeVersion, ITypeVersion.class.getSimpleName());
}
// The namespace of the previous type version of a data object must be equal to the namespace of the version of the migration handler, otherwise no migration handler
// will be called because DoStructureMigrationHelper#isMigrationApplicable will return false on different type version namespaces.
// This cannot be validated here because we don't have access to the previous type version.
validateDataObjectTypeVersion(migrationHandler);
return migrationHandler;
}
/**
* Validates that if there is a migration handler for a certain type version and type names, each data object must
* have at least this type version, otherwise the migration handler would set a higher version than the data object
* has (assuming that {@link IDoStructureMigrationHandler#toTypeVersion()} is the target type version).
* <p>
* This validation prevents that a migration handler is written but the type version on the data object class was not
* updated accordingly.
* <p>
* It's not an exact validation (best effort only).
*/
protected void validateDataObjectTypeVersion(IDoStructureMigrationHandler migrationHandler) {
NamespaceVersion typeVersionToUpdate = migrationHandler.toTypeVersion();
DataObjectInventory inventory = BEANS.get(DataObjectInventory.class);
for (String typeName : migrationHandler.getTypeNames()) {
Class<? extends IDoEntity> doEntityClass = inventory.fromTypeName(typeName);
if (doEntityClass == null) {
// Maybe an old migration handler where the type name was changed in the meantime. Skip it.
// When an old migration handler is involved the consistency of the type version should have been check then anyway, so it seems
// to be fine just skipping it.
continue;
}
// There might be false positives when in case of and old migration handler the type name was changed and a new data object got the same name later on.
// We ignore this scenario currently, maybe need to handle it in a future release (e.g. by only checking the migration handlers for the newest version?).
NamespaceVersion doEntityVersion = inventory.getTypeVersion(doEntityClass);
if (doEntityVersion == null) {
throw new PlatformException("Missing a type version (at least {}) for {} specified as type name in {}", typeVersionToUpdate, typeName, migrationHandler);
}
if (doEntityVersion.namespaceEquals(typeVersionToUpdate) && NamespaceVersion.compareVersion(doEntityVersion, typeVersionToUpdate) < 0) {
// Only compare if namespace is equal. If the DO is replaced the namespace is not equal anymore
throw new PlatformException("Entity do '{}' has specified a lower version than the migration handler '{}'. [entityDoVersion={}, migrationHandlerVersion={}]",
typeName, migrationHandler.getClass().getSimpleName(), doEntityVersion, typeVersionToUpdate);
}
}
}
/**
* For each type name {@link #findNextMigrationHandlerVersion(String, NamespaceVersion)} is called. The lowest version
* of all type names defines the starting point.
*
* @param typeNames
* Map from type name to version.
* @param toVersion
* <code>null</code> to return versions up to the newest version or a known type version to only return
* versions up to the provided version.
* @return List of versions for which migrations must be applied, might be an empty list (never <code>null</code>).
*/
public List<NamespaceVersion> getVersions(Map<String, NamespaceVersion> typeNames, NamespaceVersion toVersion) {
assertTrue(toVersion == null || m_orderedVersions.contains(toVersion), "toVersion '{}' is unknown", toVersion);
NamespaceVersion lowestNextVersion = null;
for (Entry<String, NamespaceVersion> entry : typeNames.entrySet()) {
NamespaceVersion nextVersion = findNextMigrationHandlerVersion(entry.getKey(), entry.getValue());
if (nextVersion == null) {
continue; // type name must not be considered (e.g. already current version)
}
if (lowestNextVersion == null || m_comparator.compare(lowestNextVersion, nextVersion) > 0) {
lowestNextVersion = nextVersion; // found a new lowest version
}
}
if (lowestNextVersion == null) {
// lowestNextVersion is null if there are no migration handlers available for the given type name/versions.
return Collections.emptyList();
}
List<NamespaceVersion> versions = new ArrayList<>(m_orderedVersions);
int lowerIndex = versions.indexOf(lowestNextVersion);
versions = versions.subList(lowerIndex, versions.size());
if (toVersion != null) {
// limit upper version if required
int upperIndex = versions.indexOf(toVersion);
if (upperIndex < 0) {
// toVersion is not part of version if and only if lower bound was already higher than toVersion -> no versions
return Collections.emptyList();
}
versions = versions.subList(0, upperIndex + 1); // inclusive
}
// remove all versions that don't have migration handlers
versions.removeIf(version -> !m_migrationHandlers.containsKey(version));
return new ArrayList<>(versions);
}
/**
* Determines the lowest possible version for which a migration handler could be triggered for the provided type
* name/version.
* <p>
* If the type name doesn't have the current version yet (based on {@link DataObjectInventory}, the returned version
* is the first version > provided version this type name is affected by a migration handler.
*
* @param version
* Type version of given type name (might be <code>null</code> if persisted without a type version yet)
* @return Lowest possible version or <code>null</code> if there will be no migration handler (already newest version
* or due to other reasons).
*/
protected NamespaceVersion findNextMigrationHandlerVersion(String typeName, NamespaceVersion version) {
// Example used within comments:
//
// - Type name "lorem.Migrationless" with current type version "lorem-2" (@TypeVersion)
// - Missing migration handler to "lorem-2"
//
// - Type name "lorem.MissingMigration" with current type version "lorem-3" (@TypeVersion)
// - Migration handler for type version "lorem-2"
// - Missing migration handler to "lorem-3"
//
// - Type name "lorem.Example" with current type version "lorem-3" (@TypeVersion)
// - Migration handler for type version "lorem-2"
// - Migration handler for type version "lorem-3"
//
// - Type name "lorem.Beta" (previously "lorem.Alpha" since "lorem-1") with current type version "lorem-4" (@TypeVersion)
// - Migration handler for type version "lorem-2" that acts on "lorem.Alpha" and renames it to "lorem.Beta"
// - Migration handler for type version "lorem-4" that acts on "lorem.Beta"
//
// - Type name "lorem.Two" (previously "lorem.One" since "lorem-1") with current type version "lorem-2" (@TypeVersion)
// - Migration handler for type version "lorem-2" that acts on "lorem.One" and renames it to "lorem.Two"
//
// - Type name "lorem.One" with current type version "lorem-6" (@TypeVersion) (introduced newly with "lorem-4", different data object, using same name as an old one until "lorem-2")
// - Migration handler for type version "lorem-5" that acts on "lorem.One"
// - Migration handler for type version "lorem-6" that acts on "lorem.One"
//
// - Type name "lorem.Switch" with current type version "ipsum-3" (@TypeVersion) (introduced with "lorem-5")
// - Migration handler for type version "lorem-6" that acts on "lorem.Switch" and uses "ipsum-2" as typeVersionToUpdate
// - Migration handler for type version "ipsum-3" that acts on "lorem.Switch"
//
// - Type name "ipsum.SwitchAndRename" with current type version "ipsum-3" (@TypeVersion) (introduced with "lorem.SwitchAndRename" in type version "lorem-5")
// - Migration handler for type version "lorem-6" that acts on "lorem.SwitchAndRename" and renames it to "ipsum.SwitchAndRename" and uses "ipsum-2" as typeVersionToUpdate
// - Migration handler for type version "ipsum-3" that acts on "ipsum.SwitchAndRename"
//
// - Known type versions "lorem-1", "lorem-2", "lorem-3", "lorem-4", "lorem-5", "lorem-6", "ipsum-2", "ipsum-3"
NamespaceVersion typeVersion = m_typeNameToCurrentTypeVersion.get(typeName);
if (typeVersion != null && typeVersion.equals(version)) {
// Already the current type version, thus no need to check for migration handlers.
return null; // Example: "lorem-3" (current type version)
}
// No type version maybe found for previously known type names due to renaming (example "lorem.Alpha").
List<NamespaceVersion> versions = m_typeNameVersions.get(typeName);
if (versions == null) {
// Despite not having the current type version, no migration handlers are available that would handle this type name (rare case, unknown/foreign type name, no migration to trigger).
return null; // Example: type name "other.Example" or "lorem.Migrationless"
}
if (version == null) {
// Persisted data object didn't have a type version yet -> start with first migration
// Example: type name "lorem.Example" without a type version
return CollectionUtility.firstElement(versions);
}
if (!m_orderedVersions.contains(version)) {
// The type version for this type name has a version that is unknown.
// If nothing went wrong with the corresponding data object when it was persisted, the type version could only be a newer one not yet known to this system (inserted into this system via e.g. import).
// This check is required because m_comparator must only be used with known type versions.
return null; // Example: "lorem.Example" with type version "lorem-7" (e.g. exported from a newer system)
}
// Search for the provided type version within the version having a migration handler for this type name.
int retVal = Collections.binarySearch(versions, version, m_comparator);
if (retVal >= 0) {
// Exact match found: look for the next migration handler for this type name
if (retVal + 1 == versions.size()) { // no more migration handlers
// Example: "lorem.MissingMigration" with type version "lorem-2" (invalid, no migration handler present)
// Example: "lorem.Alpha" with type version "lorem-2" (invalid, because should have been renamed to "lorem.Beta" when having "lorem-2")
return null;
}
// The version after that one is the next version that should run a migration handler for this type name.
return versions.get(retVal + 1); // Example: "lorem.Example" with type version "lorem-2" -> "lorem-3" | "lorem.One" with type version "lorem-5" -> "lorem-6"
}
// No exact match found, happens for example if there was no migration handler executed yet for this type name.
// binarySearch retVal = (-(insertion point) - 1)
int insertionPoint = -retVal - 1;
if (insertionPoint == versions.size()) { // all elements in the list are less than the specified key
// There is no newer version with migration handlers available for this type name but data object still has a different @TypeVersion annotation
// Two scenarios:
// - 1: a renamed type name
// - 2: an outdated @TypeVersion annotation and missing migration handler (example "lorem.Example" type version "lorem-4" but @TypeVersion is "lorem-3")
// Returning null would be invalid because there are other migration handlers that are relevant for the new type name.
// Thus return next from full list.
List<NamespaceVersion> orderedVersions = new ArrayList<>(m_orderedVersions);
int index = orderedVersions.indexOf(version);
if (index + 1 < orderedVersions.size()) {
// Example: "lorem.Alpha" with type version "lorem-1" -> "lorem-2" based on m_orderedVersions (first relevant migration handler for "lorem.Beta" is in "lorem-4")
// Example: "lorem.SwitchAndRename" with type version "lorem-5" -> "lorem-6" based on m_orderedVersions (first relevant migration handler for "ipsum.SwitchAndRename" is in "ipsum-3")
return orderedVersions.get(index + 1);
}
// If the current version is the last version in m_orderedVersions then something is wrong
return null; // Example: scenario 2
}
// insertionPoint: the index of the first element greater than the key
// Example: "lorem.Example" with type version "lorem-1" -> "lorem-2"
// Example: "lorem.One" with type version "lorem-1" -> "lorem-2"
// Example: "lorem.One" with type version "lorem-4" -> "lorem-5"
// Example: "lorem.Switch" with type version "lorem-5" -> "lorem-6"
// Example: "lorem.Switch" with type version "ipsum-2" -> "lorem-3"
// Example: "ipsum.SwitchAndRename" with type version "ipsum-2" -> "lorem-3"
return versions.get(insertionPoint);
}
public Map<String, IDoStructureMigrationHandler> getMigrationHandlers(NamespaceVersion version) {
assertNotNull(version, "version is required");
assertTrue(m_orderedVersions.contains(version), "version is unknown");
return m_migrationHandlers.computeIfAbsent(version, k -> Collections.emptyMap());
}
public Set<Class<? extends IDoStructureMigrationTargetContextData>> getDoMigrationContextValues(IDoEntity doEntity) {
assertNotNull(doEntity, "doEntity is required");
String typeName = BEANS.get(DoStructureMigrationHelper.class).getType(doEntity);
if (typeName != null) {
return m_doContextDataClassByTypeName.getOrDefault(typeName, Collections.emptySet());
}
else { // data object
return m_doContextDataClassByDoEntityClass.getOrDefault(doEntity.getClass(), Collections.emptySet());
}
}
/**
* Comparator for {@link NamespaceVersion} based on the total order of all versions according to the dependency model.
* <p>
* This comparator can only be used if the versions to compare are known, i.e. part of the ones provided in the
* constructor.
*/
protected static class ByNamespaceVersionComparator implements Comparator<NamespaceVersion> {
private final Map<NamespaceVersion, Integer> m_ordering;
/**
* @param versions
* Versions (<code>null</code> versions are not allowed).
*/
public ByNamespaceVersionComparator(List<NamespaceVersion> versions) {
m_ordering = createOrdering(versions);
}
protected static Map<NamespaceVersion, Integer> createOrdering(List<NamespaceVersion> versions) {
Map<NamespaceVersion, Integer> nameOrder = new HashMap<>();
for (int i = 0; i < versions.size(); i++) {
nameOrder.put(assertNotNull(versions.get(i)), i); // version must never be null
}
return nameOrder;
}
@Override
public int compare(NamespaceVersion o1, NamespaceVersion o2) {
// comparison only allowed for known versions, thus no versions is missing in ordering - no <null> handling
Integer s1 = assertNotNull(m_ordering.get(o1), "order for o1 ({}) is missing", o1.unwrap());
Integer s2 = assertNotNull(m_ordering.get(o2), "order for o2 ({}) is missing", o2.unwrap());
return s1.compareTo(s2);
}
}
}