diff --git a/waltz-data/src/main/java/org/finos/waltz/data/assessment_rating/AssessmentRatingDao.java b/waltz-data/src/main/java/org/finos/waltz/data/assessment_rating/AssessmentRatingDao.java index 6ae00c5173..b63220edd5 100644 --- a/waltz-data/src/main/java/org/finos/waltz/data/assessment_rating/AssessmentRatingDao.java +++ b/waltz-data/src/main/java/org/finos/waltz/data/assessment_rating/AssessmentRatingDao.java @@ -228,6 +228,15 @@ public List findByGenericSelector(GenericSelector genericSelec } + public int deleteByGenericSelector(GenericSelector genericSelector) { + return dsl + .deleteFrom(ar) + .where(ar.ENTITY_KIND.eq(genericSelector.kind().name())) + .and(ar.ENTITY_ID.in(genericSelector.selector())) + .execute(); + } + + public boolean store(SaveAssessmentRatingCommand command) { checkNotNull(command, "command cannot be null"); AssessmentRatingRecord record = COMMAND_TO_RECORD_MAPPER.apply(command); diff --git a/waltz-data/src/main/java/org/finos/waltz/data/entity_named_note/EntityNamedNoteDao.java b/waltz-data/src/main/java/org/finos/waltz/data/entity_named_note/EntityNamedNoteDao.java index 920a395d36..c2fa69894f 100644 --- a/waltz-data/src/main/java/org/finos/waltz/data/entity_named_note/EntityNamedNoteDao.java +++ b/waltz-data/src/main/java/org/finos/waltz/data/entity_named_note/EntityNamedNoteDao.java @@ -18,6 +18,7 @@ package org.finos.waltz.data.entity_named_note; +import org.finos.waltz.data.GenericSelector; import org.finos.waltz.schema.tables.records.EntityNamedNoteRecord; import org.finos.waltz.model.EntityKind; import org.finos.waltz.model.EntityReference; @@ -138,4 +139,12 @@ public Set findByNoteTypeExtIdAndEntityReference(String noteTyp .and(ENTITY_NAMED_NOTE.ENTITY_ID.eq(entityReference.id())) .fetchSet(TO_DOMAIN_MAPPER); } + + public int deleteByParentSelector(GenericSelector selector) { + return dsl + .deleteFrom(ENTITY_NAMED_NOTE) + .where(ENTITY_NAMED_NOTE.ENTITY_ID.in(selector.selector()) + .and(ENTITY_NAMED_NOTE.ENTITY_KIND.eq(selector.kind().name()))) + .execute(); + } } diff --git a/waltz-data/src/main/java/org/finos/waltz/data/entity_relationship/EntityRelationshipDao.java b/waltz-data/src/main/java/org/finos/waltz/data/entity_relationship/EntityRelationshipDao.java index 859a4d459f..079873beb9 100644 --- a/waltz-data/src/main/java/org/finos/waltz/data/entity_relationship/EntityRelationshipDao.java +++ b/waltz-data/src/main/java/org/finos/waltz/data/entity_relationship/EntityRelationshipDao.java @@ -25,9 +25,12 @@ import org.finos.waltz.model.EntityReference; import org.finos.waltz.model.ImmutableEntityReference; import org.finos.waltz.model.entity_relationship.*; +import org.finos.waltz.schema.Tables; import org.finos.waltz.schema.tables.records.EntityRelationshipRecord; import org.jooq.*; import org.jooq.impl.DSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -48,6 +51,7 @@ @Repository public class EntityRelationshipDao { + private static final Logger LOG = LoggerFactory.getLogger(EntityRelationshipDao.class); private static final List POSSIBLE_ENTITIES = newArrayList( EntityKind.APPLICATION, @@ -319,4 +323,103 @@ public int removeAll(long groupId, List changeInitiativeIds) { .and(ENTITY_RELATIONSHIP.ID_B.in(changeInitiativeIds)) .execute(); } + + + public void migrateEntityRelationships(EntityReference sourceReference, EntityReference targetReference, String userId) { + + dsl.transaction(ctx -> { + + DSLContext tx = ctx.dsl(); + + LOG.info("Migrating entity relationships from source: {}/{} to target: {}/{}", + sourceReference.kind().prettyName(), + sourceReference.id(), + targetReference.kind().prettyName(), + targetReference.id()); + + Condition measurableIsA = Tables.ENTITY_RELATIONSHIP.ID_A.eq(sourceReference.id()).and(Tables.ENTITY_RELATIONSHIP.KIND_A.eq(sourceReference.kind().name())); + Condition measurableIsB = Tables.ENTITY_RELATIONSHIP.ID_B.eq(sourceReference.id()).and(Tables.ENTITY_RELATIONSHIP.KIND_B.eq(sourceReference.kind().name())); + + SelectOrderByStep> allowedKindAUpdates = selectKindARelationshipsThatCanBeAdded(sourceReference, targetReference); + + int kindARelsUpdated = tx + .update(Tables.ENTITY_RELATIONSHIP) + .set(Tables.ENTITY_RELATIONSHIP.ID_A, targetReference.id()) + .set(Tables.ENTITY_RELATIONSHIP.LAST_UPDATED_AT, DateTimeUtilities.nowUtcTimestamp()) + .set(Tables.ENTITY_RELATIONSHIP.LAST_UPDATED_BY, userId) + .from(allowedKindAUpdates) + .where(measurableIsA) + .and(Tables.ENTITY_RELATIONSHIP.KIND_B.eq(allowedKindAUpdates.field(Tables.ENTITY_RELATIONSHIP.KIND_B)) + .and(Tables.ENTITY_RELATIONSHIP.ID_B.eq(allowedKindAUpdates.field(Tables.ENTITY_RELATIONSHIP.ID_B)) + .and(Tables.ENTITY_RELATIONSHIP.RELATIONSHIP.eq(allowedKindAUpdates.field(Tables.ENTITY_RELATIONSHIP.RELATIONSHIP))))) + .execute(); + + SelectOrderByStep> allowedKindBUpdates = selectKindBRelationshipsThatCanBeAdded(sourceReference, targetReference); + + int kindBRelsUpdated = tx + .update(Tables.ENTITY_RELATIONSHIP) + .set(Tables.ENTITY_RELATIONSHIP.ID_B, targetReference.id()) + .set(Tables.ENTITY_RELATIONSHIP.LAST_UPDATED_AT, DateTimeUtilities.nowUtcTimestamp()) + .set(Tables.ENTITY_RELATIONSHIP.LAST_UPDATED_BY, userId) + .from(allowedKindBUpdates) + .where(measurableIsB) + .and(Tables.ENTITY_RELATIONSHIP.KIND_A.eq(allowedKindBUpdates.field(Tables.ENTITY_RELATIONSHIP.KIND_A)) + .and(Tables.ENTITY_RELATIONSHIP.ID_A.eq(allowedKindBUpdates.field(Tables.ENTITY_RELATIONSHIP.ID_A)) + .and(Tables.ENTITY_RELATIONSHIP.RELATIONSHIP.eq(allowedKindBUpdates.field(Tables.ENTITY_RELATIONSHIP.RELATIONSHIP))))) + .execute(); + + int entityRelsRemoved = tx + .deleteFrom(Tables.ENTITY_RELATIONSHIP) + .where(measurableIsA.or(measurableIsB)) + .execute(); + + LOG.info("Migrated {} relationships from source: {}/{} to target: {}/{}", + kindARelsUpdated + kindBRelsUpdated, + sourceReference.kind().prettyName(), + sourceReference.id(), + targetReference.kind().prettyName(), + targetReference.id()); + + LOG.info("Removed {} relationships that could not be migrated from source: {}/{} to target: {}/{} as a relationship already exists", + entityRelsRemoved, + sourceReference.kind().prettyName(), + sourceReference.id(), + targetReference.kind().prettyName(), + targetReference.id()); + }); + } + + private SelectOrderByStep> selectKindARelationshipsThatCanBeAdded(EntityReference source, EntityReference target) { + + SelectConditionStep> targets = DSL + .select(Tables.ENTITY_RELATIONSHIP.ID_B, Tables.ENTITY_RELATIONSHIP.KIND_B, Tables.ENTITY_RELATIONSHIP.RELATIONSHIP) + .from(Tables.ENTITY_RELATIONSHIP) + .where(Tables.ENTITY_RELATIONSHIP.ID_A.eq(target.id()) + .and(Tables.ENTITY_RELATIONSHIP.KIND_A.eq(target.kind().name()))); + + SelectConditionStep> migrations = DSL + .select(Tables.ENTITY_RELATIONSHIP.ID_B, Tables.ENTITY_RELATIONSHIP.KIND_B, Tables.ENTITY_RELATIONSHIP.RELATIONSHIP) + .from(Tables.ENTITY_RELATIONSHIP) + .where(Tables.ENTITY_RELATIONSHIP.ID_A.eq(source.id()) + .and(Tables.ENTITY_RELATIONSHIP.KIND_A.eq(source.kind().name()))); + + return migrations.except(targets); + } + + private SelectOrderByStep> selectKindBRelationshipsThatCanBeAdded(EntityReference source, EntityReference target) { + + SelectConditionStep> targets = DSL + .select(Tables.ENTITY_RELATIONSHIP.ID_A, Tables.ENTITY_RELATIONSHIP.KIND_A, Tables.ENTITY_RELATIONSHIP.RELATIONSHIP) + .from(Tables.ENTITY_RELATIONSHIP) + .where(Tables.ENTITY_RELATIONSHIP.ID_B.eq(target.id()) + .and(Tables.ENTITY_RELATIONSHIP.KIND_B.eq(target.kind().name()))); + + SelectConditionStep> migrations = DSL + .select(Tables.ENTITY_RELATIONSHIP.ID_A, Tables.ENTITY_RELATIONSHIP.KIND_A, Tables.ENTITY_RELATIONSHIP.RELATIONSHIP) + .from(Tables.ENTITY_RELATIONSHIP) + .where(Tables.ENTITY_RELATIONSHIP.ID_B.eq(source.id()) + .and(Tables.ENTITY_RELATIONSHIP.KIND_B.eq(source.kind().name()))); + + return migrations.except(targets); + } } diff --git a/waltz-data/src/main/java/org/finos/waltz/data/measurable/MeasurableDao.java b/waltz-data/src/main/java/org/finos/waltz/data/measurable/MeasurableDao.java index 8aedd15efb..531869b66c 100644 --- a/waltz-data/src/main/java/org/finos/waltz/data/measurable/MeasurableDao.java +++ b/waltz-data/src/main/java/org/finos/waltz/data/measurable/MeasurableDao.java @@ -19,7 +19,6 @@ package org.finos.waltz.data.measurable; -import org.finos.waltz.schema.tables.records.MeasurableRecord; import org.finos.waltz.common.DateTimeUtilities; import org.finos.waltz.data.FindEntityReferencesByIdSelector; import org.finos.waltz.model.EntityKind; @@ -27,6 +26,7 @@ import org.finos.waltz.model.EntityReference; import org.finos.waltz.model.measurable.ImmutableMeasurable; import org.finos.waltz.model.measurable.Measurable; +import org.finos.waltz.schema.tables.records.MeasurableRecord; import org.jooq.*; import org.jooq.impl.DSL; import org.slf4j.Logger; @@ -41,15 +41,15 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import static org.finos.waltz.data.JooqUtilities.summarizeResults; -import static org.finos.waltz.schema.Tables.*; -import static org.finos.waltz.schema.tables.EntityHierarchy.ENTITY_HIERARCHY; -import static org.finos.waltz.schema.tables.Measurable.MEASURABLE; import static java.util.Optional.ofNullable; import static org.finos.waltz.common.Checks.checkNotNull; import static org.finos.waltz.common.EnumUtilities.readEnum; import static org.finos.waltz.common.StringUtilities.mkSafe; import static org.finos.waltz.data.JooqUtilities.TO_ENTITY_REFERENCE; +import static org.finos.waltz.data.JooqUtilities.summarizeResults; +import static org.finos.waltz.schema.Tables.*; +import static org.finos.waltz.schema.tables.EntityHierarchy.ENTITY_HIERARCHY; +import static org.finos.waltz.schema.tables.Measurable.MEASURABLE; @Repository @@ -229,6 +229,31 @@ public boolean updateParentId(Long measurableId, Long destinationId, String user .execute() == 1; } + public boolean moveChildren(Long measurableId, Long targetId, String userId) { + + if (targetId == null) { + throw new IllegalArgumentException("Cannot move children without specifying a new target"); + } + + LOG.info("Moving children from measurable: {} to {}", + measurableId, + targetId); + + Select> destinationExtId = DSL + .select(MEASURABLE.EXTERNAL_ID) + .from(MEASURABLE) + .where(MEASURABLE.ID.eq(targetId)); + + return dsl + .update(MEASURABLE) + .set(MEASURABLE.PARENT_ID, targetId) + .set(MEASURABLE.EXTERNAL_PARENT_ID, destinationExtId) + .set(MEASURABLE.LAST_UPDATED_AT, DateTimeUtilities.nowUtcTimestamp()) + .set(MEASURABLE.LAST_UPDATED_BY, userId) + .where(MEASURABLE.PARENT_ID.eq(measurableId)) + .execute() == 1; + } + public List findByCategoryId(Long categoryId) { return dsl diff --git a/waltz-data/src/main/java/org/finos/waltz/data/measurable_rating/MeasurableRatingDao.java b/waltz-data/src/main/java/org/finos/waltz/data/measurable_rating/MeasurableRatingDao.java index f78681d577..8bc293877f 100644 --- a/waltz-data/src/main/java/org/finos/waltz/data/measurable_rating/MeasurableRatingDao.java +++ b/waltz-data/src/main/java/org/finos/waltz/data/measurable_rating/MeasurableRatingDao.java @@ -18,6 +18,7 @@ package org.finos.waltz.data.measurable_rating; +import org.finos.waltz.common.DateTimeUtilities; import org.finos.waltz.common.exception.NotFoundException; import org.finos.waltz.data.InlineSelectFieldFactory; import org.finos.waltz.data.JooqUtilities; @@ -35,6 +36,8 @@ import org.jooq.*; import org.jooq.impl.DSL; import org.jooq.lambda.tuple.Tuple2; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -55,8 +58,7 @@ import static org.finos.waltz.common.SetUtilities.union; import static org.finos.waltz.common.StringUtilities.firstChar; import static org.finos.waltz.common.StringUtilities.notEmpty; -import static org.finos.waltz.schema.Tables.MEASURABLE_CATEGORY; -import static org.finos.waltz.schema.Tables.USER_ROLE; +import static org.finos.waltz.schema.Tables.*; import static org.finos.waltz.schema.tables.Application.APPLICATION; import static org.finos.waltz.schema.tables.Measurable.MEASURABLE; import static org.finos.waltz.schema.tables.MeasurableRating.MEASURABLE_RATING; @@ -65,14 +67,16 @@ @Repository public class MeasurableRatingDao { + private static final Logger LOG = LoggerFactory.getLogger(MeasurableRatingDao.class); + private static final Condition APP_JOIN_CONDITION = APPLICATION.ID.eq(MEASURABLE_RATING.ENTITY_ID) .and(MEASURABLE_RATING.ENTITY_KIND.eq(EntityKind.APPLICATION.name())); private static final Field ENTITY_NAME_FIELD = InlineSelectFieldFactory.mkNameField( - MEASURABLE_RATING.ENTITY_ID, - MEASURABLE_RATING.ENTITY_KIND, - newArrayList(EntityKind.values())) - .as("entity_name"); + MEASURABLE_RATING.ENTITY_ID, + MEASURABLE_RATING.ENTITY_KIND, + newArrayList(EntityKind.values())) + .as("entity_name"); private static final Field ENTITY_LIFECYCLE_FIELD = InlineSelectFieldFactory.mkEntityLifecycleField( MEASURABLE_RATING.ENTITY_ID, @@ -382,4 +386,314 @@ public Set calculateAmendedAllocationOperations(Set operat } } + + /** + * Takes a source measurable and will move all ratings, decommission dates, replacement applications and allocations to the target measurable where possible. + * If a value already exists on the target the migration is ignored, or in the case of allocations, aggregated. + * + * @param measurableId the source measurable from which to migrate data + * @param targetId the target measurable to inherit the data (where possible) + * @param userId the user responsible for the change + */ + public void migrateRatings(Long measurableId, Long targetId, String userId) { + + if (targetId == null) { + throw new IllegalArgumentException("Cannot migrate ratings without specifying a new target"); + } + + LOG.info("Migrating ratings from measurable: {} to {}", + measurableId, + targetId); + + int sharedRatingCount = getSharedRatingsCount(measurableId, targetId); + int sharedDecomCount = getSharedDecommsCount(measurableId, targetId); + + dsl.transaction(ctx -> { + + DSLContext tx = ctx.dsl(); + + // RATINGS + + SelectOrderByStep> allowableMigrations = selectRatingsThatCanBeModified(measurableId, targetId); + // Do not update the measurable where an existing mapping already exists + + SelectConditionStep> ratingsToInsert = DSL + .select(Tables.MEASURABLE_RATING.ENTITY_ID, + Tables.MEASURABLE_RATING.ENTITY_KIND, + DSL.val(targetId), + Tables.MEASURABLE_RATING.RATING, + Tables.MEASURABLE_RATING.DESCRIPTION, + Tables.MEASURABLE_RATING.LAST_UPDATED_AT, + Tables.MEASURABLE_RATING.LAST_UPDATED_BY, + Tables.MEASURABLE_RATING.PROVENANCE, + Tables.MEASURABLE_RATING.IS_READONLY) + .from(Tables.MEASURABLE_RATING) + .innerJoin(allowableMigrations).on(Tables.MEASURABLE_RATING.ENTITY_ID.eq(allowableMigrations.field(Tables.MEASURABLE_RATING.ENTITY_ID)) + .and(Tables.MEASURABLE_RATING.ENTITY_KIND.eq(allowableMigrations.field(Tables.MEASURABLE_RATING.ENTITY_KIND)))) + .where(Tables.MEASURABLE_RATING.MEASURABLE_ID.eq(measurableId)); + + int migratedRatings = tx + .insertInto(Tables.MEASURABLE_RATING) + .columns(Tables.MEASURABLE_RATING.ENTITY_ID, + Tables.MEASURABLE_RATING.ENTITY_KIND, + Tables.MEASURABLE_RATING.MEASURABLE_ID, + Tables.MEASURABLE_RATING.RATING, + Tables.MEASURABLE_RATING.DESCRIPTION, + Tables.MEASURABLE_RATING.LAST_UPDATED_AT, + Tables.MEASURABLE_RATING.LAST_UPDATED_BY, + Tables.MEASURABLE_RATING.PROVENANCE, + Tables.MEASURABLE_RATING.IS_READONLY) + .select(ratingsToInsert) + .execute(); + + if (migratedRatings > 0) { + writeChangeLogForMerge( + tx, + targetId, + EntityKind.MEASURABLE_RATING, + Operation.UPDATE, + format("Migrated %d ratings from measurable: %d to %d", migratedRatings, measurableId, targetId), + userId); + } + + if (sharedRatingCount > 0) { + writeChangeLogForMerge( + tx, + targetId, + EntityKind.MEASURABLE_RATING, + Operation.REMOVE, + format("Failed to migrate %d ratings from measurable: %d to %d due to existing ratings on the target", sharedRatingCount, measurableId, targetId), + userId); + } + + // DECOMMS + + SelectOrderByStep> allowableDecomns = selectDecommsThatCanBeModified(measurableId, targetId); + + int migratedDecoms = tx + .update(MEASURABLE_RATING_PLANNED_DECOMMISSION) + .set(MEASURABLE_RATING_PLANNED_DECOMMISSION.MEASURABLE_ID, targetId) + .from(allowableDecomns) + .where(MEASURABLE_RATING_PLANNED_DECOMMISSION.ENTITY_ID.eq(allowableDecomns.field(Tables.MEASURABLE_RATING.ENTITY_ID)) + .and(MEASURABLE_RATING_PLANNED_DECOMMISSION.ENTITY_KIND.eq(allowableDecomns.field(Tables.MEASURABLE_RATING.ENTITY_KIND)) + .and(MEASURABLE_RATING_PLANNED_DECOMMISSION.MEASURABLE_ID.eq(measurableId)))) + .execute(); + + if (migratedDecoms > 0) { + writeChangeLogForMerge( + tx, + targetId, + EntityKind.MEASURABLE_RATING_PLANNED_DECOMMISSION, + Operation.UPDATE, + format("Migrated %d decomms from measurable: %d to %d", migratedDecoms, measurableId, targetId), + userId); + } + + if (sharedDecomCount > 0) { + writeChangeLogForMerge( + tx, + targetId, + EntityKind.MEASURABLE_RATING_PLANNED_DECOMMISSION, + Operation.REMOVE, + format("Failed to migrate %d decomms from measurable: %d to %d due to existing decomms on the target", sharedDecomCount, measurableId, targetId), + userId); + } + + + // ALLOCATIONS + + + SelectOrderByStep> updateableAllocs = selectAllocsThatCanBeModified(measurableId, targetId); + SelectHavingStep> mergableAllocs = selectAllocsToBeUpdated(measurableId, targetId); + + int migratedAllocs = tx + .update(ALLOCATION) + .set(ALLOCATION.MEASURABLE_ID, targetId) + .from(updateableAllocs) + .where(ALLOCATION.ENTITY_ID.eq(updateableAllocs.field(ALLOCATION.ENTITY_ID)) + .and(ALLOCATION.ENTITY_KIND.eq(updateableAllocs.field(ALLOCATION.ENTITY_KIND)) + .and(ALLOCATION.MEASURABLE_ID.eq(measurableId)))) + .execute(); + + int mergedAllocs = tx + .update(ALLOCATION) + .set(ALLOCATION.ALLOCATION_PERCENTAGE, mergableAllocs.field("allocation_percentage", Integer.class)) + .from(mergableAllocs) + .where(ALLOCATION.ALLOCATION_SCHEME_ID.eq(mergableAllocs.field(ALLOCATION.ALLOCATION_SCHEME_ID)) + .and(ALLOCATION.ENTITY_ID.eq(mergableAllocs.field(ALLOCATION.ENTITY_ID)) + .and(ALLOCATION.ENTITY_KIND.eq(mergableAllocs.field(ALLOCATION.ENTITY_KIND)) + .and(ALLOCATION.MEASURABLE_ID.eq(targetId))))) + .execute(); + + if (migratedAllocs > 0) { + writeChangeLogForMerge( + tx, + targetId, + EntityKind.ALLOCATION, + Operation.UPDATE, + format("Migrated %d allocations from measurable: %d to %d", migratedAllocs, measurableId, targetId), + userId); + } + + if (mergedAllocs > 0) { + writeChangeLogForMerge( + tx, + targetId, + EntityKind.ALLOCATION, + Operation.UPDATE, + format("Merged %d allocations from measurable: %d to %d where there was an existing allocation on the target", mergedAllocs, measurableId, targetId), + userId); + } + + LOG.info(format("Migrated %d ratings, %d decomms, %d/%d allocations (migrated/merged) from measurable: %d to %d", + migratedRatings, + migratedDecoms, + migratedAllocs, + mergedAllocs, + measurableId, + targetId)); + + int removedRatings = tx + .deleteFrom(Tables.MEASURABLE_RATING) + .where(Tables.MEASURABLE_RATING.MEASURABLE_ID.eq(measurableId)) + .execute(); + + if (removedRatings > 0) { + writeChangeLogForMerge( + tx, + targetId, + EntityKind.ALLOCATION, + Operation.UPDATE, + format("Removed %d ratings from measurable: %d where they could not be migrated due to an existing rating on the target", removedRatings, measurableId, targetId), + userId); + } + + // allocations, decomms and replacements are automatically cleared up via cascade delete on fk + LOG.info("Removed {} measurable ratings and any associated allocations, planned decommissions and replacement applications after migration", removedRatings); + }); + } + + + private void writeChangeLogForMerge(DSLContext tx, Long measurableId, EntityKind childKind, Operation operation, String message, String userId) { + tx + .insertInto(CHANGE_LOG) + .columns(CHANGE_LOG.PARENT_KIND, + CHANGE_LOG.PARENT_ID, + CHANGE_LOG.MESSAGE, + CHANGE_LOG.USER_ID, + CHANGE_LOG.SEVERITY, + CHANGE_LOG.CREATED_AT, + CHANGE_LOG.CHILD_KIND, + CHANGE_LOG.OPERATION) + .values(EntityKind.MEASURABLE.name(), + measurableId, + message, + userId, + Severity.INFORMATION.name(), + DateTimeUtilities.nowUtcTimestamp(), + childKind.name(), + operation.name()) + .execute(); + } + + + private SelectOrderByStep> selectRatingsThatCanBeModified(Long measurableId, Long targetId) { + + SelectConditionStep> targets = mkEntitySelectForMeasurable(targetId); + SelectConditionStep> migrations = mkEntitySelectForMeasurable(measurableId); + + return migrations.except(targets); + } + + private SelectConditionStep> mkEntitySelectForMeasurable(Long measurableId) { + return DSL + .select(Tables.MEASURABLE_RATING.ENTITY_ID, Tables.MEASURABLE_RATING.ENTITY_KIND) + .from(Tables.MEASURABLE_RATING) + .where(Tables.MEASURABLE_RATING.MEASURABLE_ID.eq(measurableId)); + } + + public int getSharedRatingsCount(Long measurableId, Long targetId) { + + SelectConditionStep> targets = mkEntitySelectForMeasurable(targetId); + SelectConditionStep> migrations = mkEntitySelectForMeasurable(measurableId); + + SelectOrderByStep> sharedRatings = migrations.intersect(targets); + + return dsl.fetchCount(sharedRatings); + } + + public int getSharedDecommsCount(Long measurableId, Long targetId) { + + SelectConditionStep> targets = mkEntitySelectForDecomm(targetId); + SelectConditionStep> migrations = mkEntitySelectForDecomm(measurableId); + + SelectOrderByStep> sharedDecomms = migrations.intersect(targets); + + return dsl.fetchCount(sharedDecomms); + } + + + private SelectOrderByStep> selectDecommsThatCanBeModified(Long measurableId, Long targetId) { + + SelectConditionStep> targets = mkEntitySelectForDecomm(targetId); + SelectConditionStep> migrations = mkEntitySelectForDecomm(measurableId); + + return migrations.except(targets); + } + + private SelectConditionStep> mkEntitySelectForDecomm(Long measurableId) { + return DSL + .select(MEASURABLE_RATING_PLANNED_DECOMMISSION.ENTITY_ID, MEASURABLE_RATING_PLANNED_DECOMMISSION.ENTITY_KIND) + .from(MEASURABLE_RATING_PLANNED_DECOMMISSION) + .where(MEASURABLE_RATING_PLANNED_DECOMMISSION.MEASURABLE_ID.eq(measurableId)); + } + + + private SelectOrderByStep> selectAllocsThatCanBeModified(Long measurableId, Long targetId) { + + SelectConditionStep> targets = DSL + .select(ALLOCATION.ENTITY_ID, ALLOCATION.ENTITY_KIND) + .from(ALLOCATION) + .where(ALLOCATION.MEASURABLE_ID.eq(targetId)); + + SelectConditionStep> migrations = DSL + .select(ALLOCATION.ENTITY_ID, ALLOCATION.ENTITY_KIND) + .from(ALLOCATION) + .where(ALLOCATION.MEASURABLE_ID.eq(measurableId)); + + return migrations.except(targets); + } + + private SelectOrderByStep> selectAllocsToBeSummed(Long measurableId, Long targetId) { + + SelectConditionStep> targets = DSL + .select(ALLOCATION.ALLOCATION_SCHEME_ID, ALLOCATION.ENTITY_ID, ALLOCATION.ENTITY_KIND) + .from(ALLOCATION) + .where(ALLOCATION.MEASURABLE_ID.eq(targetId)); + + SelectConditionStep> migrations = DSL + .select(ALLOCATION.ALLOCATION_SCHEME_ID, ALLOCATION.ENTITY_ID, ALLOCATION.ENTITY_KIND) + .from(ALLOCATION) + .where(ALLOCATION.MEASURABLE_ID.eq(measurableId)); + + return migrations.intersect(targets); + } + + private SelectHavingStep> selectAllocsToBeUpdated(Long measurableId, Long targetId) { + + SelectOrderByStep> valuesToBeSummed = selectAllocsToBeSummed(measurableId, targetId); + + return DSL + .select(ALLOCATION.ALLOCATION_SCHEME_ID, + ALLOCATION.ENTITY_ID, + ALLOCATION.ENTITY_KIND, + DSL.cast(DSL.sum(ALLOCATION.ALLOCATION_PERCENTAGE), Integer.class).as("allocation_percentage")) + .from(ALLOCATION) + .innerJoin(valuesToBeSummed).on(ALLOCATION.ALLOCATION_SCHEME_ID.eq(valuesToBeSummed.field(ALLOCATION.ALLOCATION_SCHEME_ID)) + .and(ALLOCATION.ENTITY_KIND.eq(valuesToBeSummed.field(ALLOCATION.ENTITY_KIND)) + .and(ALLOCATION.ENTITY_ID.eq(valuesToBeSummed.field(ALLOCATION.ENTITY_ID))))) + .where(ALLOCATION.MEASURABLE_ID.in(targetId, measurableId)) + .groupBy(ALLOCATION.ALLOCATION_SCHEME_ID, ALLOCATION.ENTITY_ID, ALLOCATION.ENTITY_KIND); + } + } diff --git a/waltz-model/src/main/java/org/finos/waltz/model/EntityKind.java b/waltz-model/src/main/java/org/finos/waltz/model/EntityKind.java index 4822a06033..44e45b71a5 100644 --- a/waltz-model/src/main/java/org/finos/waltz/model/EntityKind.java +++ b/waltz-model/src/main/java/org/finos/waltz/model/EntityKind.java @@ -23,6 +23,7 @@ public enum EntityKind { ACTOR("Actor"), AGGREGATE_OVERLAY_DIAGRAM("Aggregate Overlay Diagram"), AGGREGATE_OVERLAY_DIAGRAM_INSTANCE("Aggregate Overlay Diagram Instance"), + ALLOCATION("Allocation"), ALLOCATION_SCHEME("Allocation scheme"), APPLICATION("Application"), APP_GROUP("Application group"), diff --git a/waltz-model/src/main/java/org/finos/waltz/model/taxonomy_management/TaxonomyChangeImpact.java b/waltz-model/src/main/java/org/finos/waltz/model/taxonomy_management/TaxonomyChangeImpact.java index 10ee927677..eb0a01d767 100644 --- a/waltz-model/src/main/java/org/finos/waltz/model/taxonomy_management/TaxonomyChangeImpact.java +++ b/waltz-model/src/main/java/org/finos/waltz/model/taxonomy_management/TaxonomyChangeImpact.java @@ -33,7 +33,9 @@ public abstract class TaxonomyChangeImpact implements DescriptionProvider { public abstract Severity severity(); + public abstract String description(); - public abstract Set impactedReferences(); + + public abstract int impactCount(); } diff --git a/waltz-model/src/main/java/org/finos/waltz/model/taxonomy_management/TaxonomyChangePreview.java b/waltz-model/src/main/java/org/finos/waltz/model/taxonomy_management/TaxonomyChangePreview.java index fd12429fdb..fdca6c1f8d 100644 --- a/waltz-model/src/main/java/org/finos/waltz/model/taxonomy_management/TaxonomyChangePreview.java +++ b/waltz-model/src/main/java/org/finos/waltz/model/taxonomy_management/TaxonomyChangePreview.java @@ -32,7 +32,6 @@ public abstract class TaxonomyChangePreview { public abstract TaxonomyChangeCommand command(); public abstract List impacts(); - @Nullable public abstract String errorMessage(); } diff --git a/waltz-ng/client/measurable/components/change-control/measurable-change-control.html b/waltz-ng/client/measurable/components/change-control/measurable-change-control.html index bbb92bc1a4..84f963606d 100644 --- a/waltz-ng/client/measurable/components/change-control/measurable-change-control.html +++ b/waltz-ng/client/measurable/components/change-control/measurable-change-control.html @@ -266,6 +266,44 @@

+ + + + + + diff --git a/waltz-ng/client/measurable/components/change-control/measurable-change-control.js b/waltz-ng/client/measurable/components/change-control/measurable-change-control.js index 3f81ca95ca..f70e914f2b 100644 --- a/waltz-ng/client/measurable/components/change-control/measurable-change-control.js +++ b/waltz-ng/client/measurable/components/change-control/measurable-change-control.js @@ -71,13 +71,15 @@ function controller($scope, const vm = initialiseData(this, initialState); function mkCmd(params = {}) { + const paramProcessor = vm.selectedOperation.paramProcessor || _.identity; + const processedParam = paramProcessor(params); return { changeType: vm.selectedOperation.code, changeDomain: toEntityRef(vm.changeDomain), primaryReference: toEntityRef(vm.measurable), - params: paramProcessor(params), + params: processedParam, createdBy: vm.userName, lastUpdatedBy: vm.userName }; @@ -88,7 +90,7 @@ function controller($scope, } function mkPreviewCmd() { - return mkCmd(); + return mkCmd(vm.commandParams); } function calcPreview() { @@ -270,6 +272,45 @@ function controller($scope, should be used with care`, color: "#b40400", options: [ + { + name: "Merge", + code: "MERGE", + icon: "code-fork", + description: "Merges this item with another and all of it's children will be migrated", + onShow: () => { + resetForm(); + calcPreview(); + }, + paramProcessor: (d) => _.isEmpty(d) + ? {} + : ({ + targetId: d.target.id, + targetName: d.target.name + }), + onReset: () => { + vm.commandParams.target = null; + vm.submitDisabled = true; + }, + onChange: (target) => { + if (target === null) { + toasts.warning("Must have selected a arget to merge, ignoring...."); + vm.commandParams.target = null; + vm.submitDisabled = true; + } else if (target.id === vm.measurable.id) { + toasts.warning("Cannot merge onto yourself, ignoring...."); + vm.commandParams.target = null; + vm.submitDisabled = true; + } else if (vm.measurable.concrete && !target.concrete) { + toasts.warning("Cannot migrate to a non-concrete node, ignoring...."); + vm.commandParams.target = null; + vm.submitDisabled = true; + } else { + vm.commandParams.target = target; + vm.submitDisabled = false; + calcPreview(); + } + } + }, { name: "Remove", code: "REMOVE", diff --git a/waltz-ng/client/taxonomy-management/components/taxonomy-change-command-preview/taxonomy-change-command-preview.html b/waltz-ng/client/taxonomy-management/components/taxonomy-change-command-preview/taxonomy-change-command-preview.html index e8a02d5aa5..76964fcd41 100644 --- a/waltz-ng/client/taxonomy-management/components/taxonomy-change-command-preview/taxonomy-change-command-preview.html +++ b/waltz-ng/client/taxonomy-management/components/taxonomy-change-command-preview/taxonomy-change-command-preview.html @@ -40,7 +40,7 @@ + ng-bind="impact.impactCount"> diff --git a/waltz-service/src/main/java/org/finos/waltz/service/assessment_rating/AssessmentRatingService.java b/waltz-service/src/main/java/org/finos/waltz/service/assessment_rating/AssessmentRatingService.java index 1df1d16b1e..5e0da51388 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/assessment_rating/AssessmentRatingService.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/assessment_rating/AssessmentRatingService.java @@ -34,7 +34,6 @@ import org.finos.waltz.model.rating.RatingSchemeItem; import org.finos.waltz.service.changelog.ChangeLogService; import org.finos.waltz.service.permission.permission_checker.AssessmentRatingPermissionChecker; -import org.jooq.DSLContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -103,6 +102,13 @@ public List findByTargetKindForRelatedSelector(EntityKind targ } + public int deleteByAssessmentRatingRelatedSelector(EntityKind targetKind, + IdSelectionOptions selectionOptions) { + GenericSelector genericSelector = genericSelectorFactory.applyForKind(targetKind, selectionOptions); + return assessmentRatingDao.deleteByGenericSelector(genericSelector); + } + + public List findByDefinitionId(long definitionId) { return assessmentRatingDao.findByDefinitionId(definitionId); diff --git a/waltz-service/src/main/java/org/finos/waltz/service/entity_named_note/EntityNamedNoteService.java b/waltz-service/src/main/java/org/finos/waltz/service/entity_named_note/EntityNamedNoteService.java index 7a42650065..245a715241 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/entity_named_note/EntityNamedNoteService.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/entity_named_note/EntityNamedNoteService.java @@ -18,13 +18,12 @@ package org.finos.waltz.service.entity_named_note; +import org.finos.waltz.data.GenericSelector; +import org.finos.waltz.data.GenericSelectorFactory; +import org.finos.waltz.model.*; import org.finos.waltz.service.changelog.ChangeLogService; import org.finos.waltz.data.entity_named_note.EntityNamedNoteDao; import org.finos.waltz.data.entity_named_note.EntityNamedNoteTypeDao; -import org.finos.waltz.model.EntityReference; -import org.finos.waltz.model.Operation; -import org.finos.waltz.model.Severity; -import org.finos.waltz.model.UserTimestamp; import org.finos.waltz.model.changelog.ChangeLog; import org.finos.waltz.model.changelog.ImmutableChangeLog; import org.finos.waltz.model.entity_named_note.EntityNamedNodeType; @@ -44,7 +43,7 @@ public class EntityNamedNoteService { private final EntityNamedNoteDao entityNamedNoteDao; private final EntityNamedNoteTypeDao entityNamedNodeTypeDao; private final ChangeLogService changeLogService; - + private final GenericSelectorFactory genericSelectorFactory = new GenericSelectorFactory(); @Autowired public EntityNamedNoteService(EntityNamedNoteDao entityNamedNoteDao, @@ -128,6 +127,12 @@ public boolean remove(EntityReference ref, String username) { return rc; } + public int deleteByNamedNoteParentSelector(IdSelectionOptions selectionOptions) { + GenericSelector selector = genericSelectorFactory.apply(selectionOptions); + return entityNamedNoteDao + .deleteByParentSelector(selector); + } + private void logMsg(EntityReference ref, String username, Operation op, String msg) { ChangeLog logEntry = ImmutableChangeLog diff --git a/waltz-service/src/main/java/org/finos/waltz/service/entity_relationship/EntityRelationshipService.java b/waltz-service/src/main/java/org/finos/waltz/service/entity_relationship/EntityRelationshipService.java index a01d94db17..59fe5cdbfb 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/entity_relationship/EntityRelationshipService.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/entity_relationship/EntityRelationshipService.java @@ -114,4 +114,8 @@ public int deleteForGenericEntitySelector(IdSelectionOptions selectionOptions) { GenericSelector selector = genericSelectorFactory.apply(selectionOptions); return entityRelationshipDao.deleteForGenericEntitySelector(selector); } + + public void migrateEntityRelationships(EntityReference sourceReference, EntityReference targetReference, String userId) { + entityRelationshipDao.migrateEntityRelationships(sourceReference, targetReference, userId); + } } diff --git a/waltz-service/src/main/java/org/finos/waltz/service/measurable/MeasurableService.java b/waltz-service/src/main/java/org/finos/waltz/service/measurable/MeasurableService.java index 29b7cafbfa..65d89a8857 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/measurable/MeasurableService.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/measurable/MeasurableService.java @@ -249,4 +249,7 @@ public void writeAuditMessage(Long measurableId, String userId, String msg) { .build()); } + public boolean moveChildren(Long measurableId, Long targetMeasurableId, String userId) { + return measurableDao.moveChildren(measurableId, targetMeasurableId, userId); + } } \ No newline at end of file diff --git a/waltz-service/src/main/java/org/finos/waltz/service/measurable_rating/MeasurableRatingService.java b/waltz-service/src/main/java/org/finos/waltz/service/measurable_rating/MeasurableRatingService.java index 66ee40f38f..b303a71b55 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/measurable_rating/MeasurableRatingService.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/measurable_rating/MeasurableRatingService.java @@ -288,4 +288,16 @@ private void checkRatingIsAllowable(SaveMeasurableRatingCommand command) { public boolean checkRatingExists(SaveMeasurableRatingCommand command) { return measurableRatingDao.checkRatingExists(command); } + + public void migrateRatings(Long measurableId, Long targetMeasurableId, String userId) { + measurableRatingDao.migrateRatings(measurableId, targetMeasurableId, userId); + } + + public int getSharedRatingsCount(Long measurableId, Long targetMeasurableId) { + return measurableRatingDao.getSharedRatingsCount(measurableId, targetMeasurableId); + } + + public int getSharedDecommsCount(Long measurableId, Long targetMeasurableId) { + return measurableRatingDao.getSharedDecommsCount(measurableId, targetMeasurableId); + } } diff --git a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/TaxonomyManagementUtilities.java b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/TaxonomyManagementUtilities.java index 1bbea791d0..d1be2ab553 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/TaxonomyManagementUtilities.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/TaxonomyManagementUtilities.java @@ -37,6 +37,7 @@ import static java.lang.String.format; import static org.finos.waltz.common.Checks.*; +import static org.finos.waltz.common.CollectionUtilities.any; import static org.finos.waltz.common.SetUtilities.fromCollection; import static org.finos.waltz.common.SetUtilities.minus; import static org.finos.waltz.model.IdSelectionOptions.mkOpts; @@ -70,8 +71,8 @@ public static void validateMeasurablesInCategory(MeasurableService measurableSer public static Measurable validateMeasurableInCategory(MeasurableService measurableService, - long measurableId, - long categoryId) { + long measurableId, + long categoryId) { Measurable measurable = measurableService.getById(measurableId); checkNotNull( @@ -90,6 +91,36 @@ public static Measurable validateMeasurableInCategory(MeasurableService measurab return measurable; } + public static void validateTargetNotChild(MeasurableService measurableService, + Measurable measurable, + Measurable targetMeasurable) { + + List children = measurableService.findByMeasurableIdSelector(mkOpts( + measurable.entityReference(), + HierarchyQueryScope.CHILDREN)); + + checkFalse( + any(children, d -> d.equals(targetMeasurable)), + format("Target measurable [%s / %d] is a child of measurable [%s / %d]", + targetMeasurable.name(), + targetMeasurable.id().get(), + measurable.name(), + measurable.id().get())); + } + + + public static void validateConcreteMergeAllowed(Measurable measurable, + Measurable targetMeasurable) { + + checkFalse( + measurable.concrete() && !targetMeasurable.concrete(), + format("Measurable [%s / %d] is concrete but target measurable [%s / %d] is abstract", + measurable.name(), + measurable.id().get(), + targetMeasurable.name(), + targetMeasurable.id().get())); + } + public static Set findCurrentRatingMappings(MeasurableRatingService measurableRatingService, TaxonomyChangeCommand cmd) { @@ -106,24 +137,24 @@ public static Set findCurrentRatingMappings(MeasurableRatingSer * Optionally add an impact to the given preview and return it. * Whether to add the impact is determined by the presence of references. * - * @param preview The preview builder to update - * @param refs Set of references, if empty no impact will be added to the preview - * @param severity Severity of the impact - * @param msg Description of the impact + * @param preview The preview builder to update + * @param impactCount Count of the records affected by this change + * @param severity Severity of the impact + * @param msg Description of the impact * @return The preview builder for convenience */ public static ImmutableTaxonomyChangePreview.Builder addToPreview(ImmutableTaxonomyChangePreview.Builder preview, - Set refs, + int impactCount, Severity severity, String msg) { - return refs.isEmpty() - ? preview - : preview + return impactCount == 0 + ? preview + : preview .addImpacts(ImmutableTaxonomyChangeImpact.builder() - .impactedReferences(refs) - .description(msg) - .severity(severity) - .build()); + .impactCount(impactCount) + .description(msg) + .severity(severity) + .build()); } diff --git a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/MergeMeasurableCommandProcessor.java b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/MergeMeasurableCommandProcessor.java new file mode 100644 index 0000000000..1703983799 --- /dev/null +++ b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/MergeMeasurableCommandProcessor.java @@ -0,0 +1,161 @@ +/* + * Waltz - Enterprise Architecture + * Copyright (C) 2016, 2017, 2018, 2019 Waltz open source project + * See README.md for more information + * + * 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 + * + */ + +package org.finos.waltz.service.taxonomy_management.processors; + +import org.finos.waltz.common.Checks; +import org.finos.waltz.common.DateTimeUtilities; +import org.finos.waltz.common.SetUtilities; +import org.finos.waltz.model.EntityKind; +import org.finos.waltz.model.HierarchyQueryScope; +import org.finos.waltz.model.IdSelectionOptions; +import org.finos.waltz.model.measurable.Measurable; +import org.finos.waltz.model.taxonomy_management.*; +import org.finos.waltz.service.measurable.MeasurableService; +import org.finos.waltz.service.taxonomy_management.TaxonomyCommandProcessor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Set; + +import static java.lang.String.format; +import static org.finos.waltz.common.Checks.checkNotNull; +import static org.finos.waltz.model.IdSelectionOptions.mkOpts; +import static org.finos.waltz.service.taxonomy_management.TaxonomyManagementUtilities.*; + +@Service +public class MergeMeasurableCommandProcessor implements TaxonomyCommandProcessor { + + private final MeasurableService measurableService; + private final TaxonomyManagementHelper taxonomyManagementHelper; + + @Autowired + public MergeMeasurableCommandProcessor(MeasurableService measurableService, + TaxonomyManagementHelper taxonomyManagementHelper) { + checkNotNull(measurableService, "measurableService cannot be null"); + checkNotNull(taxonomyManagementHelper, "taxonomyManagementHelper cannot be null"); + this.taxonomyManagementHelper = taxonomyManagementHelper; + this.measurableService = measurableService; + } + + + @Override + public Set supportedTypes() { + return SetUtilities.asSet(TaxonomyChangeType.MERGE); + } + + + @Override + public EntityKind domain() { + return EntityKind.MEASURABLE_CATEGORY; + } + + + public TaxonomyChangePreview preview(TaxonomyChangeCommand cmd) { + Measurable primaryRef = validate(cmd); + + ImmutableTaxonomyChangePreview.Builder previewBuilder = ImmutableTaxonomyChangePreview + .builder() + .command(ImmutableTaxonomyChangeCommand + .copyOf(cmd) + .withPrimaryReference(primaryRef.entityReference())); + + IdSelectionOptions opts = IdSelectionOptions.mkOpts(cmd.primaryReference(), HierarchyQueryScope.EXACT); + + taxonomyManagementHelper.previewBookmarkRemovals(previewBuilder, opts); + taxonomyManagementHelper.previewInvolvementRemovals(previewBuilder, opts); + taxonomyManagementHelper.previewEntityNamedNoteRemovals(previewBuilder, opts); + taxonomyManagementHelper.previewAssessmentRemovals(previewBuilder, opts); + taxonomyManagementHelper.previewFlowDiagramRemovals(previewBuilder, opts); + taxonomyManagementHelper.previewChildNodeMigrations(previewBuilder, opts); + + Long target = getTarget(cmd); + + if (target != null) { + taxonomyManagementHelper.previewRatingMigrations(previewBuilder, opts.entityReference().id(), target); + taxonomyManagementHelper.previewDecommMigrations(previewBuilder, opts.entityReference().id(), target); + } + + ImmutableTaxonomyChangePreview preview = previewBuilder.build(); + + return preview; + } + + + public TaxonomyChangeCommand apply(TaxonomyChangeCommand cmd, String userId) { + + Measurable measurableToMerge = validate(cmd); + Long target = getTarget(cmd); + checkNotNull(target, "Target cannot be null when migrating a measurable"); + + String targetName = getTargetName(cmd); + IdSelectionOptions selectionOptions = mkOpts(measurableToMerge.entityReference(), HierarchyQueryScope.EXACT); // children are migrated, do not want to delete their data + + taxonomyManagementHelper.migrateMeasurable(selectionOptions, target, userId); + + int removedBookmarks = taxonomyManagementHelper.removeBookmarks(selectionOptions); + int removedInvolvements = taxonomyManagementHelper.removeInvolvements(selectionOptions); + int removedNotes = taxonomyManagementHelper.removeNamedNotes(selectionOptions); + int removedDiagrams = taxonomyManagementHelper.removeFlowDiagrams(selectionOptions); + int removedAssessments = taxonomyManagementHelper.removeAssessments(EntityKind.MEASURABLE, selectionOptions); + + String message = format("Merged measurable: %s [%d] into target: %s [%d] - Removed: %d bookmarks; %d involvements; %d notes; %d flow diagram relationships; %d assessments", + measurableToMerge.name(), + measurableToMerge.id().get(), + targetName, + target, + removedBookmarks, + removedInvolvements, + removedNotes, + removedDiagrams, + removedAssessments); + + measurableService.writeAuditMessage(target, userId, message); + + return ImmutableTaxonomyChangeCommand + .copyOf(cmd) + .withLastUpdatedAt(DateTimeUtilities.nowUtc()) + .withLastUpdatedBy(userId) + .withStatus(TaxonomyChangeLifecycleStatus.EXECUTED); + } + + + private Measurable validate(TaxonomyChangeCommand cmd) { + doBasicValidation(cmd); + long categoryId = cmd.changeDomain().id(); + Measurable measurable = validateMeasurableInCategory(measurableService, cmd.primaryReference().id(), categoryId); + Long targetId = getTarget(cmd); + if (targetId != null) { + Measurable target = validateMeasurableInCategory(measurableService, targetId, categoryId); + validateTargetNotChild(measurableService, measurable, target); + validateConcreteMergeAllowed(measurable, target); + } + return measurable; + } + + + private Long getTarget(TaxonomyChangeCommand cmd) { + return cmd.paramAsLong("targetId", null); + } + + + private String getTargetName(TaxonomyChangeCommand cmd) { + return cmd.param("targetName"); + } + +} diff --git a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/RemoveMeasurableCommandProcessor.java b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/RemoveMeasurableCommandProcessor.java index a2c088d810..ecbfa03ad5 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/RemoveMeasurableCommandProcessor.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/RemoveMeasurableCommandProcessor.java @@ -51,34 +51,16 @@ @Service public class RemoveMeasurableCommandProcessor implements TaxonomyCommandProcessor { - private final BookmarkService bookmarkService; - private final EntityRelationshipService entityRelationshipService; - private final FlowDiagramEntityService flowDiagramEntityService; - private final InvolvementService involvementService; - private final MeasurableRatingService measurableRatingService; private final MeasurableService measurableService; - + private final TaxonomyManagementHelper taxonomyManagementHelper; @Autowired - public RemoveMeasurableCommandProcessor(BookmarkService bookmarkService, - EntityRelationshipService entityRelationshipService, - FlowDiagramEntityService flowDiagramEntityService, - InvolvementService involvementService, - MeasurableRatingService measurableRatingService, - MeasurableService measurableService) { - checkNotNull(bookmarkService, "bookmarkService cannot be null"); - checkNotNull(entityRelationshipService, "entityRelationshipService cannot be null"); - checkNotNull(flowDiagramEntityService, "flowDiagramEntityService cannot be null"); - checkNotNull(involvementService, "involvementService cannot be null"); - checkNotNull(measurableRatingService, "measurableRatingService cannot be null"); + public RemoveMeasurableCommandProcessor(MeasurableService measurableService, + TaxonomyManagementHelper taxonomyManagementHelper) { checkNotNull(measurableService, "measurableService cannot be null"); - this.bookmarkService = bookmarkService; - this.entityRelationshipService = entityRelationshipService; - this.flowDiagramEntityService = flowDiagramEntityService; - this.involvementService = involvementService; - this.measurableRatingService = measurableRatingService; this.measurableService = measurableService; + this.taxonomyManagementHelper = taxonomyManagementHelper; } @@ -106,110 +88,34 @@ public TaxonomyChangePreview preview(TaxonomyChangeCommand cmd) { IdSelectionOptions selectionOptions = mkOpts(cmd.primaryReference(), HierarchyQueryScope.CHILDREN); - previewChildNodeRemovals(preview, selectionOptions); - previewAppMappingRemovals(preview, selectionOptions); - previewBookmarkRemovals(preview, selectionOptions); - previewInvolvementRemovals(preview, selectionOptions); - previewFlowDiagramRemovals(preview, selectionOptions); - previewEntityRelationships(preview, selectionOptions); + taxonomyManagementHelper.previewChildNodeRemovals(preview, selectionOptions); + taxonomyManagementHelper.previewAppMappingRemovals(preview, selectionOptions); + taxonomyManagementHelper.previewBookmarkRemovals(preview, selectionOptions); + taxonomyManagementHelper.previewInvolvementRemovals(preview, selectionOptions); + taxonomyManagementHelper.previewFlowDiagramRemovals(preview, selectionOptions); + taxonomyManagementHelper.previewEntityRelationships(preview, selectionOptions); // TODO: entitySvgDiagrams, roadmapScenarios return preview.build(); } - - private void previewEntityRelationships(ImmutableTaxonomyChangePreview.Builder preview, - IdSelectionOptions options) { - Set refs = entityRelationshipService - .findForGenericEntitySelector(options) - .stream() - .flatMap(rel -> Stream.of(rel.a(), rel.b())) - .filter(ref -> !ref.equals(options.entityReference())) - .collect(Collectors.toSet()); - - addToPreview( - preview, - refs, - Severity.WARNING, - "Entity Relationships will be removed"); - } - - - private void previewFlowDiagramRemovals(ImmutableTaxonomyChangePreview.Builder preview, - IdSelectionOptions selectionOptions) { - - Set refs = flowDiagramEntityService - .findForEntitySelector(selectionOptions) - .stream() - .map(FlowDiagramEntity::entityReference) - .collect(Collectors.toSet()); - - addToPreview( - preview, - refs, - Severity.WARNING, - "Relationships to flow diagrams will be removed"); - - } - - - private void previewInvolvementRemovals(ImmutableTaxonomyChangePreview.Builder preview, - IdSelectionOptions selectionOptions) { - addToPreview( - preview, - map(involvementService.findByGenericEntitySelector(selectionOptions), Involvement::entityReference), - Severity.ERROR, - "Involvements (links to people) associated to this item (or it's children) will be removed"); - } - - - private void previewBookmarkRemovals(ImmutableTaxonomyChangePreview.Builder preview, - IdSelectionOptions selectionOptions) { - Set bookmarks = bookmarkService.findByBookmarkIdSelector(selectionOptions); - addToPreview( - preview, - map(bookmarks, Bookmark::entityReference), - Severity.ERROR, - "Bookmarks associated to this item (or it's children) will be removed"); - } - - - private void previewAppMappingRemovals(ImmutableTaxonomyChangePreview.Builder preview, - IdSelectionOptions selectionOptions) { - List ratings = measurableRatingService.findByMeasurableIdSelector(selectionOptions); - addToPreview( - preview, - map(ratings, MeasurableRating::entityReference), - Severity.ERROR, - "Application ratings associated to this item (or it's children) will be removed"); - } - - - private void previewChildNodeRemovals(ImmutableTaxonomyChangePreview.Builder preview, - IdSelectionOptions selectionOptions) { - Set childRefs = map(measurableService.findByMeasurableIdSelector(selectionOptions), Measurable::entityReference); - addToPreview( - preview, - minus(childRefs, asSet(selectionOptions.entityReference())), - Severity.ERROR, - "This node has child nodes which will also be removed"); - } - - public TaxonomyChangeCommand apply(TaxonomyChangeCommand cmd, String userId) { doBasicValidation(cmd); Measurable measurable = validatePrimaryMeasurable(measurableService, cmd); IdSelectionOptions selectionOptions = mkOpts(cmd.primaryReference(), HierarchyQueryScope.CHILDREN); - removeBookmarks(selectionOptions); - removeInvolvements(selectionOptions); - removeAppMappings(selectionOptions); - removeMeasurables(selectionOptions); - removeFlowDiagrams(selectionOptions); - removeEntityRelationshipsDiagrams(selectionOptions); - + taxonomyManagementHelper.removeBookmarks(selectionOptions); + taxonomyManagementHelper.removeInvolvements(selectionOptions); + taxonomyManagementHelper.removeAppMappings(selectionOptions); + taxonomyManagementHelper.removeMeasurables(selectionOptions); + taxonomyManagementHelper.removeFlowDiagrams(selectionOptions); + taxonomyManagementHelper.removeEntityRelationshipsDiagrams(selectionOptions); + taxonomyManagementHelper.removeAssessments(EntityKind.MEASURABLE, selectionOptions); + taxonomyManagementHelper.removeNamedNotes(selectionOptions); + + String message = String.format("Measurable %s has been removed", measurable.name()); Optional measurableId = measurable.parentId().isPresent() ? measurable.parentId() @@ -218,40 +124,13 @@ public TaxonomyChangeCommand apply(TaxonomyChangeCommand cmd, String userId) { // TODO: entitySvgDiagrams, roadmapScenarios - return ImmutableTaxonomyChangeCommand.copyOf(cmd) + return ImmutableTaxonomyChangeCommand + .copyOf(cmd) .withStatus(TaxonomyChangeLifecycleStatus.EXECUTED) .withLastUpdatedBy(userId) .withLastUpdatedAt(DateTimeUtilities.nowUtc()); } - private int removeEntityRelationshipsDiagrams(IdSelectionOptions selectionOptions) { - return entityRelationshipService.deleteForGenericEntitySelector(selectionOptions); - } - - - private int removeFlowDiagrams(IdSelectionOptions selectionOptions) { - return flowDiagramEntityService.deleteForEntitySelector(selectionOptions); - } - - - private int removeMeasurables(IdSelectionOptions selectionOptions) { - return measurableService.deleteByIdSelector(selectionOptions); - } - - - private int removeAppMappings(IdSelectionOptions selectionOptions) { - return measurableRatingService.deleteByMeasurableIdSelector(selectionOptions); - } - - - private int removeInvolvements(IdSelectionOptions selectionOptions) { - return involvementService.deleteByGenericEntitySelector(selectionOptions); - } - - - private int removeBookmarks(IdSelectionOptions selectionOptions) { - return bookmarkService.deleteByBookmarkIdSelector(selectionOptions); - } } diff --git a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/TaxonomyManagementHelper.java b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/TaxonomyManagementHelper.java new file mode 100644 index 0000000000..e81e6ea1c1 --- /dev/null +++ b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/TaxonomyManagementHelper.java @@ -0,0 +1,266 @@ +package org.finos.waltz.service.taxonomy_management.processors; + +import org.finos.waltz.model.*; +import org.finos.waltz.model.assessment_rating.AssessmentRating; +import org.finos.waltz.model.bookmark.Bookmark; +import org.finos.waltz.model.entity_named_note.EntityNamedNote; +import org.finos.waltz.model.entity_relationship.EntityRelationship; +import org.finos.waltz.model.flow_diagram.FlowDiagramEntity; +import org.finos.waltz.model.involvement.Involvement; +import org.finos.waltz.model.measurable.Measurable; +import org.finos.waltz.model.measurable_rating.MeasurableRating; +import org.finos.waltz.model.taxonomy_management.ImmutableTaxonomyChangePreview; +import org.finos.waltz.service.assessment_rating.AssessmentRatingService; +import org.finos.waltz.service.bookmark.BookmarkService; +import org.finos.waltz.service.entity_named_note.EntityNamedNoteService; +import org.finos.waltz.service.entity_relationship.EntityRelationshipService; +import org.finos.waltz.service.flow_diagram.FlowDiagramEntityService; +import org.finos.waltz.service.involvement.InvolvementService; +import org.finos.waltz.service.measurable.MeasurableService; +import org.finos.waltz.service.measurable_rating.MeasurableRatingService; +import org.finos.waltz.service.measurable_rating_planned_decommission.MeasurableRatingPlannedDecommissionService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static org.finos.waltz.common.Checks.checkNotNull; +import static org.finos.waltz.common.SetUtilities.*; +import static org.finos.waltz.model.EntityReference.mkRef; +import static org.finos.waltz.service.taxonomy_management.TaxonomyManagementUtilities.addToPreview; + +@Service +public class TaxonomyManagementHelper { + + private static final Logger LOG = LoggerFactory.getLogger(TaxonomyManagementHelper.class); + + private final BookmarkService bookmarkService; + private final EntityRelationshipService entityRelationshipService; + private final FlowDiagramEntityService flowDiagramEntityService; + private final InvolvementService involvementService; + private final MeasurableRatingService measurableRatingService; + private final MeasurableService measurableService; + private final EntityNamedNoteService entityNamedNoteService; + private final AssessmentRatingService assessmentRatingService; + + @Autowired + public TaxonomyManagementHelper(BookmarkService bookmarkService, + EntityRelationshipService entityRelationshipService, + FlowDiagramEntityService flowDiagramEntityService, + InvolvementService involvementService, + MeasurableRatingService measurableRatingService, + MeasurableService measurableService, + EntityNamedNoteService entityNamedNoteService, + AssessmentRatingService assessmentRatingService) { + + checkNotNull(bookmarkService, "bookmarkService cannot be null"); + checkNotNull(entityRelationshipService, "entityRelationshipService cannot be null"); + checkNotNull(flowDiagramEntityService, "flowDiagramEntityService cannot be null"); + checkNotNull(involvementService, "involvementService cannot be null"); + checkNotNull(measurableRatingService, "measurableRatingService cannot be null"); + checkNotNull(measurableService, "measurableService cannot be null"); + checkNotNull(entityNamedNoteService, "entityNamedNoteService cannot be null"); + checkNotNull(assessmentRatingService, "assessmentRatingService cannot be null"); + + this.assessmentRatingService = assessmentRatingService; + this.bookmarkService = bookmarkService; + this.entityNamedNoteService = entityNamedNoteService; + this.entityRelationshipService = entityRelationshipService; + this.flowDiagramEntityService = flowDiagramEntityService; + this.involvementService = involvementService; + this.measurableRatingService = measurableRatingService; + this.measurableService = measurableService; + } + + + // PREVIEWS + + public void previewEntityRelationships(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions options) { + + Collection rels = entityRelationshipService.findForGenericEntitySelector(options); + + addToPreview( + preview, + rels.size(), + Severity.WARNING, + "Entity Relationships will be removed"); + } + + + public void previewFlowDiagramRemovals(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions selectionOptions) { + + List diagrams = flowDiagramEntityService.findForEntitySelector(selectionOptions); + + addToPreview( + preview, + diagrams.size(), + Severity.WARNING, + "Relationships to flow diagrams will be removed"); + + } + + public void previewInvolvementRemovals(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions selectionOptions) { + Collection involvements = involvementService.findByGenericEntitySelector(selectionOptions); + addToPreview( + preview, + involvements.size(), + Severity.ERROR, + "Involvements (links to people) associated to this item (or it's children) will be removed"); + } + + public void previewEntityNamedNoteRemovals(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions selectionOptions) { + + List notes = entityNamedNoteService.findByEntityReference(selectionOptions.entityReference()); + addToPreview( + preview, + notes.size(), + Severity.ERROR, + "Entity named notes associated to this item will be removed"); + } + + + public void previewAssessmentRemovals(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions selectionOptions) { + + List assessmentRatings = assessmentRatingService.findByTargetKindForRelatedSelector(EntityKind.MEASURABLE, selectionOptions); + addToPreview( + preview, + assessmentRatings.size(), + Severity.ERROR, + "Assessments associated to this item will be removed"); + } + + + public void previewBookmarkRemovals(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions selectionOptions) { + Set bookmarks = bookmarkService.findByBookmarkIdSelector(selectionOptions); + addToPreview( + preview, + bookmarks.size(), + Severity.ERROR, + "Bookmarks associated to this item will be removed"); + } + + + public void previewAppMappingRemovals(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions selectionOptions) { + List ratings = measurableRatingService.findByMeasurableIdSelector(selectionOptions); + addToPreview( + preview, + ratings.size(), + Severity.ERROR, + "Application ratings associated to this item (or it's children) will be removed"); + } + + + public void previewChildNodeRemovals(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions selectionOptions) { + Set childRefs = map(measurableService.findByMeasurableIdSelector(selectionOptions), Measurable::entityReference); + addToPreview( + preview, + minus(childRefs, asSet(selectionOptions.entityReference())).size(), + Severity.ERROR, + "This node has child nodes which will also be removed"); + } + + public void previewChildNodeMigrations(ImmutableTaxonomyChangePreview.Builder preview, + IdSelectionOptions selectionOptions) { + + ImmutableIdSelectionOptions hierarchySelector = ImmutableIdSelectionOptions.copyOf(selectionOptions).withScope(HierarchyQueryScope.CHILDREN); + + Set childRefs = map(measurableService.findByMeasurableIdSelector(hierarchySelector), Measurable::entityReference); + + addToPreview( + preview, + minus(childRefs, asSet(selectionOptions.entityReference())).size(), + Severity.WARNING, + "This node has child nodes which will be migrated to the new target"); + } + + public void previewRatingMigrations(ImmutableTaxonomyChangePreview.Builder preview, + Long measurableId, + Long targetId) { + + int sharedRatingsCount = measurableRatingService.getSharedRatingsCount(measurableId, targetId); + + addToPreview( + preview, + sharedRatingsCount, + Severity.WARNING, + "This node has measurable ratings that cannot be migrated due to an existing rating on the target"); + } + + public void previewDecommMigrations(ImmutableTaxonomyChangePreview.Builder preview, + Long measurableId, + Long targetId) { + + int sharedDecommsCount = measurableRatingService.getSharedDecommsCount(measurableId, targetId); + + addToPreview( + preview, + sharedDecommsCount, + Severity.WARNING, + "This node has measurable rating planned decommission dates that cannot be migrated due to an existing decom on the target rating"); + } + + + // ACTIONS + + public void migrateMeasurable(IdSelectionOptions selectionOptions, Long targetId, String userId) { + + EntityReference sourceMeasurable = selectionOptions.entityReference(); + EntityReference targetMeasurable = mkRef(EntityKind.MEASURABLE, targetId); + + measurableService.moveChildren(sourceMeasurable.id(), targetId, userId); + measurableRatingService.migrateRatings(sourceMeasurable.id(), targetId, userId); + entityRelationshipService.migrateEntityRelationships(sourceMeasurable, targetMeasurable, userId); + measurableService.deleteByIdSelector(selectionOptions); + } + + public int removeEntityRelationshipsDiagrams(IdSelectionOptions selectionOptions) { + return entityRelationshipService.deleteForGenericEntitySelector(selectionOptions); + } + + + public int removeFlowDiagrams(IdSelectionOptions selectionOptions) { + return flowDiagramEntityService.deleteForEntitySelector(selectionOptions); + } + + + public int removeMeasurables(IdSelectionOptions selectionOptions) { + return measurableService.deleteByIdSelector(selectionOptions); + } + + + public int removeAppMappings(IdSelectionOptions selectionOptions) { + return measurableRatingService.deleteByMeasurableIdSelector(selectionOptions); + } + + + public int removeInvolvements(IdSelectionOptions selectionOptions) { + return involvementService.deleteByGenericEntitySelector(selectionOptions); + } + + + public int removeBookmarks(IdSelectionOptions selectionOptions) { + return bookmarkService.deleteByBookmarkIdSelector(selectionOptions); + } + + + public int removeAssessments(EntityKind targetKind, IdSelectionOptions selectionOptions) { + return assessmentRatingService.deleteByAssessmentRatingRelatedSelector(targetKind, selectionOptions); + } + + public int removeNamedNotes(IdSelectionOptions selectionOptions) { + return entityNamedNoteService.deleteByNamedNoteParentSelector(selectionOptions); + } + +} diff --git a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableConcreteFlagCommandProcessor.java b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableConcreteFlagCommandProcessor.java index 30a60d8489..4d547453be 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableConcreteFlagCommandProcessor.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableConcreteFlagCommandProcessor.java @@ -83,7 +83,7 @@ public TaxonomyChangePreview preview(TaxonomyChangeCommand cmd) { if (!newValue) { addToPreview( preview, - findCurrentRatingMappings(measurableRatingService, cmd), + findCurrentRatingMappings(measurableRatingService, cmd).size(), Severity.WARNING, "Current app mappings exist to item, these will be invalid when the item becomes non-concrete"); } diff --git a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableDescriptionCommandProcessor.java b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableDescriptionCommandProcessor.java index 19d79fa867..746df2e6ca 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableDescriptionCommandProcessor.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableDescriptionCommandProcessor.java @@ -34,6 +34,7 @@ import static org.finos.waltz.common.Checks.checkNotNull; import static org.finos.waltz.common.SetUtilities.asSet; +import static org.finos.waltz.service.taxonomy_management.TaxonomyManagementUtilities.findCurrentRatingMappings; @Service public class UpdateMeasurableDescriptionCommandProcessor implements TaxonomyCommandProcessor { @@ -79,10 +80,10 @@ public TaxonomyChangePreview preview(TaxonomyChangeCommand cmd) { } TaxonomyManagementUtilities.addToPreview( - preview, - TaxonomyManagementUtilities.findCurrentRatingMappings(measurableRatingService, cmd), - Severity.INFORMATION, - "Current app mappings exist to item, these may be misleading if the description change alters the meaning of this item"); + preview, + findCurrentRatingMappings(measurableRatingService, cmd).size(), + Severity.INFORMATION, + "Current app mappings exist to item, these may be misleading if the description change alters the meaning of this item"); return preview.build(); diff --git a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableNameCommandProcessor.java b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableNameCommandProcessor.java index fbd6ebdd03..006f341fd8 100644 --- a/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableNameCommandProcessor.java +++ b/waltz-service/src/main/java/org/finos/waltz/service/taxonomy_management/processors/UpdateMeasurableNameCommandProcessor.java @@ -84,10 +84,10 @@ public TaxonomyChangePreview preview(TaxonomyChangeCommand cmd) { } addToPreview( - preview, - findCurrentRatingMappings(measurableRatingService, cmd), - Severity.INFORMATION, - "Current app mappings exist to item, these may be misleading if the name change alters the meaning of this item"); + preview, + findCurrentRatingMappings(measurableRatingService, cmd).size(), + Severity.INFORMATION, + "Current app mappings exist to item, these may be misleading if the name change alters the meaning of this item"); return preview.build(); }