diff --git a/src/main/java/de/bonndan/nivio/input/AppearanceProcessor.java b/src/main/java/de/bonndan/nivio/input/AppearanceProcessor.java index 716d135bd..3999d630d 100644 --- a/src/main/java/de/bonndan/nivio/input/AppearanceProcessor.java +++ b/src/main/java/de/bonndan/nivio/input/AppearanceProcessor.java @@ -25,12 +25,15 @@ public AppearanceProcessor(ProcessLog processLog, IconService iconService) { this.iconService = iconService; } - public void process(LandscapeDescription input, Landscape landscape) { + @Override + public ProcessingChangelog process(LandscapeDescription input, Landscape landscape) { Optional logo = Optional.ofNullable(landscape.getConfig().getBranding().getMapLogo()); logo.ifPresent(s -> setLandscapeLogo(landscape, s)); landscape.getGroupItems().forEach(group -> group.getItems().forEach(item -> setItemAppearance(group, item))); + + return new ProcessingChangelog(); } private void setItemAppearance(Group group, Item item) { diff --git a/src/main/java/de/bonndan/nivio/input/DiffProcessor.java b/src/main/java/de/bonndan/nivio/input/DiffProcessor.java index 9cca5cdbf..1a05cc4e9 100644 --- a/src/main/java/de/bonndan/nivio/input/DiffProcessor.java +++ b/src/main/java/de/bonndan/nivio/input/DiffProcessor.java @@ -24,9 +24,9 @@ protected DiffProcessor(ProcessLog processLog) { } @Override - public void process(LandscapeDescription input, Landscape landscape) { + public ProcessingChangelog process(LandscapeDescription input, Landscape landscape) { Set existingItems = landscape.getItems().all(); - + ProcessingChangelog changelog = new ProcessingChangelog(); //insert new ones List newItems = added(input.getItemDescriptions().all(), existingItems, landscape); Set inLandscape = new HashSet<>(); @@ -34,6 +34,7 @@ public void process(LandscapeDescription input, Landscape landscape) { newItems.forEach( newItem -> { processLog.info(String.format("Creating new item %s in env %s", newItem.getIdentifier(), input.getIdentifier())); + changelog.addEntry(newItem, ProcessingChangelog.ChangeType.CREATED); inLandscape.add(ItemFactory.fromDescription(newItem, landscape)); } ); @@ -59,9 +60,14 @@ public void process(LandscapeDescription input, Landscape landscape) { } } - processLog.info("Updating item " + item.getIdentifier() + " in landscape " + input.getIdentifier()); + processLog.info(String.format("Updating item %s in landscape %s", item.getIdentifier(), input.getIdentifier())); + Item newWithAssignedValues = ItemFactory.assignAll(item, description); + inLandscape.add(newWithAssignedValues); - inLandscape.add(ItemFactory.assignAll(item, description)); + List changes = item.getChanges(newWithAssignedValues); + if (!changes.isEmpty()) { + changelog.addEntry(newWithAssignedValues, ProcessingChangelog.ChangeType.UPDATED, String.join("; ", changes)); + } } ); @@ -72,6 +78,7 @@ public void process(LandscapeDescription input, Landscape landscape) { List toDelete = getUnreferenced(input, inLandscape, existingItems, processLog); toDelete.forEach(item -> { processLog.info(String.format("Removing item %s from landscape", item)); + changelog.addEntry(item, ProcessingChangelog.ChangeType.DELETED); landscape.getGroup(item.getGroup()).ifPresent(group -> { boolean removed = group.removeItem(item); if (!removed) { @@ -79,6 +86,8 @@ public void process(LandscapeDescription input, Landscape landscape) { } }); }); + + return changelog; } private List getUnreferenced( @@ -92,7 +101,9 @@ private List getUnreferenced( return new ArrayList<>(); } - return removed(kept, all); + List removed = removed(kept, all); + logger.info("Removing " + removed.size() + " sources in env " + landscapeDescription.getIdentifier()); + return removed; } /** @@ -116,6 +127,8 @@ static List removed(Collection items, Collection itemDescripti /** * Returns all elements which are not in the second list * + * @return + * */ static List added(Collection itemDescriptions, Collection existingItems, Landscape landscape) { return itemDescriptions.stream() diff --git a/src/main/java/de/bonndan/nivio/input/GroupProcessor.java b/src/main/java/de/bonndan/nivio/input/GroupProcessor.java index 2479f9469..991a946b9 100644 --- a/src/main/java/de/bonndan/nivio/input/GroupProcessor.java +++ b/src/main/java/de/bonndan/nivio/input/GroupProcessor.java @@ -25,16 +25,26 @@ protected GroupProcessor(ProcessLog processLog) { super(processLog); } - public void process(LandscapeDescription input, Landscape landscape) { + public ProcessingChangelog process(LandscapeDescription input, Landscape landscape) { + ProcessingChangelog changelog = new ProcessingChangelog(); List> specs = getSpecs(input.getConfig().getGroupBlacklist()); input.getGroups().forEach((identifier, groupDescription) -> { Group g = GroupFactory.createFromDescription(identifier, landscape.getIdentifier(), groupDescription); if (!isBlacklisted(g.getIdentifier(), specs)) { - processLog.info("Adding or updating group " + g.getIdentifier()); - landscape.addGroup(g); + + Optional existing = landscape.getGroup(g.getIdentifier()); + Group added = landscape.addGroup(g); + if (existing.isEmpty()) { + processLog.info("Adding group " + g.getIdentifier()); + changelog.addEntry(added, ProcessingChangelog.ChangeType.CREATED); + } else { + processLog.info("Updating group " + g.getIdentifier()); + String updates = String.join("; ", existing.get().getChanges(added)); + changelog.addEntry(added, ProcessingChangelog.ChangeType.UPDATED, updates); + } } else { processLog.info("Ignoring blacklisted group " + g.getIdentifier()); } @@ -50,7 +60,9 @@ public void process(LandscapeDescription input, Landscape landscape) { if (!isBlacklisted(group, specs)) { if (!landscape.getGroups().containsKey(group)) { - landscape.addGroup(GroupFactory.createFromDescription(group, landscape.getIdentifier(), null)); + Group fromDescription = GroupFactory.createFromDescription(group, landscape.getIdentifier(), null); + changelog.addEntry(fromDescription, ProcessingChangelog.ChangeType.CREATED, String.format("Reference by item %s", item)); + landscape.addGroup(fromDescription); } } else { processLog.info("Removing item " + item.getIdentifier() + " because in blacklisted group " + group); @@ -72,6 +84,8 @@ public void process(LandscapeDescription input, Landscape landscape) { } throw new RuntimeException(String.format("item group '%s' not found.", item.getGroup())); }); + + return changelog; } private List> getSpecs(List blacklist) { diff --git a/src/main/java/de/bonndan/nivio/input/Indexer.java b/src/main/java/de/bonndan/nivio/input/Indexer.java index b82083409..da829c108 100644 --- a/src/main/java/de/bonndan/nivio/input/Indexer.java +++ b/src/main/java/de/bonndan/nivio/input/Indexer.java @@ -51,23 +51,26 @@ public void index(final LandscapeDescription input) { }); try { - runResolvers(input, landscape); + ProcessingChangelog processingChangelog = runResolvers(input, landscape); landscapeRepo.save(landscape); + eventPublisher.publishEvent(new ProcessingFinishedEvent(input, landscape, processingChangelog)); + landscape.getLog().info("Reindexed landscape " + input.getIdentifier()); + } catch (ProcessingException e) { final String msg = "Error while reindexing landscape " + input.getIdentifier(); landscape.getLog().warn(msg, e); eventPublisher.publishEvent(new ProcessingErrorEvent(input.getFullyQualifiedIdentifier(), e)); - return; } - - eventPublisher.publishEvent(new ProcessingFinishedEvent(input, landscape)); - landscape.getLog().info("Reindexed landscape " + input.getIdentifier()); } - private void runResolvers(LandscapeDescription input, Landscape landscape) { + private ProcessingChangelog runResolvers(LandscapeDescription input, Landscape landscape) { + //a detailed textual log ProcessLog logger = landscape.getLog(); + //a structured log on component level + ProcessingChangelog changelog = new ProcessingChangelog(); + // read all input sources new SourceReferencesResolver(formatFactory, logger, eventPublisher).resolve(input); @@ -97,19 +100,21 @@ private void runResolvers(LandscapeDescription input, Landscape landscape) { new GroupQueryResolver(logger).resolve(input); // compare landscape against input, add and remove items - new DiffProcessor(logger).process(input, landscape); + changelog.merge(new DiffProcessor(logger).process(input, landscape)); // assign items to groups, add missing groups - new GroupProcessor(logger).process(input, landscape); + changelog.merge(new GroupProcessor(logger).process(input, landscape)); // create relations between items - new ItemRelationProcessor(logger).process(input, landscape); + changelog.merge(new ItemRelationProcessor(logger).process(input, landscape)); // ensures that item have a resolved icon in the api new AppearanceProcessor(logger, iconService).process(input, landscape); // this step must be final or very late to include all item modifications landscape.getItems().indexForSearch(); + + return changelog; } } \ No newline at end of file diff --git a/src/main/java/de/bonndan/nivio/input/ItemRelationProcessor.java b/src/main/java/de/bonndan/nivio/input/ItemRelationProcessor.java index cc24ee43a..bb3e87af8 100644 --- a/src/main/java/de/bonndan/nivio/input/ItemRelationProcessor.java +++ b/src/main/java/de/bonndan/nivio/input/ItemRelationProcessor.java @@ -4,9 +4,9 @@ import de.bonndan.nivio.input.dto.RelationDescription; import de.bonndan.nivio.model.*; import de.bonndan.nivio.search.ItemMatcher; +import org.apache.commons.collections.CollectionUtils; -import java.util.Iterator; -import java.util.Optional; +import java.util.*; /** * Creates {@link Relation}s between {@link Item}s. @@ -18,69 +18,134 @@ protected ItemRelationProcessor(ProcessLog processLog) { } @Override - public void process(LandscapeDescription input, Landscape landscape) { - input.getItemDescriptions().all().forEach(serviceDescription -> { - Item origin = landscape.getItems().pick(serviceDescription); - if (!input.isPartial()) { - processLog.debug(String.format("Clearing relations of %s", origin)); - origin.getRelations().clear(); //delete all relations on full update - } - }); + public ProcessingChangelog process(LandscapeDescription input, Landscape landscape) { + + ProcessingChangelog changelog = new ProcessingChangelog(); + List processed = new ArrayList<>(); input.getItemDescriptions().all().forEach(itemDescription -> { Item origin = landscape.getItems().pick(itemDescription); + List affected = new ArrayList<>(); for (RelationDescription relationDescription : itemDescription.getRelations()) { - Optional update = update(relationDescription, landscape, origin); - update.ifPresent(relation -> { - origin.getRelations().remove(relation); - origin.getRelations().add(relation); - if (relation.getSource() == origin) { - relation.getTarget().getRelations().remove(relation); - relation.getTarget().getRelations().add(relation); - } else { - relation.getSource().getRelations().remove(relation); - relation.getSource().getRelations().add(relation); - } - }); + + if (!isValid(relationDescription, landscape)) { + continue; + } + + Optional current = getCurrentRelation(relationDescription, landscape, origin); + current.ifPresentOrElse( + (relation) -> updateRelation(changelog, processed, affected, relationDescription, relation), + () -> createRelation(landscape, changelog, processed, origin, affected, relationDescription)); } + + affected.forEach(relation -> assignToBothEnds(origin, relation)); + Collection toDelete = CollectionUtils.subtract(origin.getRelations(), affected); + toDelete.stream() + .filter(relation -> !processed.contains(relation)) + .filter(relation -> origin.equals(relation.getSource())) + .filter(relation -> !input.isPartial()) + .forEach(relation -> { + removeFromBothEnds(origin, relation); + processLog.info(String.format("Removing relation between %s and %s", relation.getSource(), relation.getTarget())); + changelog.addEntry(relation, ProcessingChangelog.ChangeType.DELETED, null); + }); }); + + return changelog; } - private Optional update(RelationDescription relationDescription, Landscape landscape, Item origin) { + private void updateRelation(ProcessingChangelog changelog, List processed, List affected, RelationDescription relationDescription, Relation relation) { + Relation update = update(relationDescription, relation); + affected.add(update); + processed.add(update); + processLog.info(String.format("Updating relation between %s and %s", update.getSource(), update.getTarget())); + List changes = relation.getChanges(update); + if (!changes.isEmpty()) { + changelog.addEntry(update, ProcessingChangelog.ChangeType.UPDATED, String.join(";", changes)); + } + } + + private void createRelation(Landscape landscape, ProcessingChangelog changelog, List processed, Item origin, List affected, RelationDescription relationDescription) { + Relation created = create(relationDescription, landscape); + affected.add(created); + processed.add(created); + processLog.info(String.format(origin + ": Adding relation between %s and %s", created.getSource(), created.getTarget())); + changelog.addEntry(created, ProcessingChangelog.ChangeType.CREATED, null); + } + + private boolean isValid(RelationDescription relationDescription, Landscape landscape) { Optional source = findBy(relationDescription.getSource(), landscape); if (source.isEmpty()) { processLog.warn(String.format("Relation source %s not found", relationDescription.getSource())); - return Optional.empty(); + return false; } Optional target = findBy(relationDescription.getTarget(), landscape); if (target.isEmpty()) { processLog.warn(String.format("Relation target %s not found", relationDescription.getTarget())); - return Optional.empty(); + return false; } + return true; + } + + private void assignToBothEnds(Item origin, Relation relation) { + removeFromBothEnds(origin, relation); + + origin.getRelations().add(relation); + if (relation.getSource() == origin) { + relation.getTarget().getRelations().add(relation); + } else { + relation.getSource().getRelations().add(relation); + } + } + + private void removeFromBothEnds(Item origin, Relation relation) { + origin.getRelations().remove(relation); + if (relation.getSource() == origin) { + relation.getTarget().getRelations().remove(relation); + } else { + relation.getSource().getRelations().remove(relation); + } + } + + + private Optional getCurrentRelation(RelationDescription relationDescription, + Landscape landscape, + Item origin + ) { + Item source = findBy(relationDescription.getSource(), landscape).orElseThrow(); + Item target = findBy(relationDescription.getTarget(), landscape).orElseThrow(); + Iterator iterator = origin.getRelations().iterator(); - Relation existing = null; - Relation created = new Relation(source.get(), target.get()); + Relation created = new Relation(source, target); + Relation existing; while (iterator.hasNext()) { existing = iterator.next(); if (existing.equals(created)) { - processLog.info(String.format("Updating relation between %s and %s", existing.getSource(), existing.getTarget())); - return Optional.of(RelationBuilder.update(existing, relationDescription)); + return Optional.of(existing); } } - created = new Relation(created.getSource(), - created.getTarget(), + return Optional.empty(); + } + + private Relation update(RelationDescription relationDescription, Relation existing + ) { + return RelationBuilder.update(existing, relationDescription); + } + + private Relation create(RelationDescription relationDescription, Landscape landscape) { + + return new Relation( + findBy(relationDescription.getSource(), landscape).orElseThrow(), + findBy(relationDescription.getTarget(), landscape).orElseThrow(), relationDescription.getDescription(), relationDescription.getFormat(), relationDescription.getType() ); - - processLog.info(String.format("Adding relation from %s to %s", created.getSource(), created.getTarget())); - return Optional.of(created); } private Optional findBy(String term, Landscape landscape) { diff --git a/src/main/java/de/bonndan/nivio/input/ProcessingChangelog.java b/src/main/java/de/bonndan/nivio/input/ProcessingChangelog.java new file mode 100644 index 000000000..da956092d --- /dev/null +++ b/src/main/java/de/bonndan/nivio/input/ProcessingChangelog.java @@ -0,0 +1,146 @@ +package de.bonndan.nivio.input; + +import de.bonndan.nivio.model.Component; +import de.bonndan.nivio.model.Relation; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Schema(description = "Changelog for a single landscape processing run (update of landscape from different sources).") +public class ProcessingChangelog { + + @Schema(description = "The key is the FullyQualifiedIdentifier of a component") + final Map changes = new HashMap<>(); + + /** + * Add a change for a component. + * + * @param component the affected component + * @param changeType created, updated, deleted + * @param message an optional message + */ + public void addEntry( + @NonNull final Component component, + @NonNull final ChangeType changeType, + @Nullable final String message + ) { + String id = Objects.requireNonNull(component).getFullyQualifiedIdentifier().toString(); + if (StringUtils.isEmpty(id)) { + throw new RuntimeException("Could not create a changelog entry id for " + component); + } + Entry entry = new Entry( + component.getClass().getSimpleName(), + Objects.requireNonNull(changeType), + message + ); + changes.put(id, entry); + } + + /** + * Add a change for a component. + * + * @param component the affected component + * @param changeType created, updated, deleted + */ + public void addEntry( + @NonNull final Component component, + @NonNull final ChangeType changeType + ) { + addEntry(component, changeType, null); + } + + public void addEntry( + @NonNull final Relation relation, + @NonNull final ChangeType changeType, + @Nullable final String message + ) { + Objects.requireNonNull(relation); + final String id = getRelationKey(relation); + if (StringUtils.isEmpty(id)) { + throw new RuntimeException("Could not create a changelog entry id for " + relation); + } + Entry entry = new Entry( + relation.getClass().getSimpleName(), + Objects.requireNonNull(changeType), + message + ); + changes.put(id, entry); + } + + String getRelationKey(final Relation relation) { + return String.format("%s;%s", + relation.getSource().getFullyQualifiedIdentifier().jsonValue(), + relation.getTarget().getFullyQualifiedIdentifier().jsonValue() + ); + } + + /** + * Merge the given changelog entries into the current. + * + * @param changelog new processor changelog + */ + public void merge(@Nullable ProcessingChangelog changelog) { + if (changelog == null) { + return; + } + changelog.changes.forEach((s, value) -> changes.merge( + s, + value, + (entry1, entry2) -> new Entry( + entry1.componentType, + ChangeType.valueOf(entry1.changeType), + entry1.message + "; " + entry2.message + ) + )); + } + + public static class Entry { + private final String componentType; + private final String changeType; + private final String message; + + Entry(@NonNull final String componentType, + @NonNull final ChangeType changeType, + @Nullable final String message + ) { + this.componentType = componentType; + this.changeType = changeType.name(); + this.message = message; + } + + @Schema(description = "The component type", allowableValues = {"Group", "Item", "Relation"}) + public String getComponentType() { + return componentType; + } + + @Schema(description = "The change type", allowableValues = {"CREATED", "UPDATED", "DELETED"}) + public String getChangeType() { + return changeType; + } + + @Nullable + public String getMessage() { + return message; + } + + @Override + public String toString() { + return "Entry{" + + "componentType='" + componentType + '\'' + + ", changeType='" + changeType + '\'' + + ", message='" + message + '\'' + + '}'; + } + } + + enum ChangeType { + CREATED, + UPDATED, + DELETED + } +} diff --git a/src/main/java/de/bonndan/nivio/input/ProcessingFinishedEvent.java b/src/main/java/de/bonndan/nivio/input/ProcessingFinishedEvent.java index cc507eab8..1e3b78be3 100644 --- a/src/main/java/de/bonndan/nivio/input/ProcessingFinishedEvent.java +++ b/src/main/java/de/bonndan/nivio/input/ProcessingFinishedEvent.java @@ -11,17 +11,29 @@ */ public class ProcessingFinishedEvent extends ProcessingEvent { + @NonNull private final LandscapeDescription input; + + @NonNull private final Landscape landscape; + @NonNull + private final ProcessingChangelog changelog; + /** * @param input the LandscapeDescription input which has been processed * @param landscape out + * @param changelog log of component changes + * @throws NullPointerException if any of the params is null */ - public ProcessingFinishedEvent(@NonNull final LandscapeDescription input, @NonNull final Landscape landscape) { + public ProcessingFinishedEvent(@NonNull final LandscapeDescription input, + @NonNull final Landscape landscape, + @NonNull final ProcessingChangelog changelog + ) { super(Objects.requireNonNull(input).getFullyQualifiedIdentifier()); this.input = input; this.landscape = Objects.requireNonNull(landscape); + this.changelog = Objects.requireNonNull(changelog); } @NonNull @@ -48,4 +60,9 @@ public String getType() { public String getMessage() { return "Processing of input data has finished."; } + + @NonNull + public ProcessingChangelog getChangelog() { + return changelog; + } } diff --git a/src/main/java/de/bonndan/nivio/input/Processor.java b/src/main/java/de/bonndan/nivio/input/Processor.java index fcadbb200..64e2ca8ca 100644 --- a/src/main/java/de/bonndan/nivio/input/Processor.java +++ b/src/main/java/de/bonndan/nivio/input/Processor.java @@ -5,8 +5,6 @@ /** * Modifies the target landscape using input. - * - * */ abstract class Processor { @@ -16,5 +14,12 @@ protected Processor(ProcessLog processLog) { this.processLog = processLog; } - public abstract void process(LandscapeDescription input, Landscape landscape); + /** + * Apply the input to the landscape. + * + * @param input input data + * @param landscape the landscape to be modified + * @return a map of changes containing {@link de.bonndan.nivio.model.FullyQualifiedIdentifier} as keys + */ + public abstract ProcessingChangelog process(LandscapeDescription input, Landscape landscape); } diff --git a/src/main/java/de/bonndan/nivio/model/ComponentDiff.java b/src/main/java/de/bonndan/nivio/model/ComponentDiff.java new file mode 100644 index 000000000..14dd01a4b --- /dev/null +++ b/src/main/java/de/bonndan/nivio/model/ComponentDiff.java @@ -0,0 +1,79 @@ +package de.bonndan.nivio.model; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import java.util.*; + +public class ComponentDiff { + + /** + * Compares changes in two strings. + * + * @param a first + * @param b second + * @param key label + */ + public static List compareStrings(@Nullable final String a, @Nullable final String b, @NonNull final String key) { + if (!org.apache.commons.lang3.StringUtils.equals(a, b)) { + return List.of(String.format("%s changed to: %s", key, b)); + } + + return Collections.emptyList(); + } + + /** + * Compares changes in string representation of two objects + * + * @param a first + * @param b second + * @param key label + */ + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static List compareOptionals(Optional a, Optional b, @NonNull final String key) { + + List changes = new ArrayList<>(); + if (a.isEmpty() && b.isEmpty()) { + return changes; + } + + boolean onlyAExists = a.isPresent() && b.isEmpty(); + //noinspection ConstantConditions left here for better readability + boolean onlyBExists = a.isEmpty() && b.isPresent(); + + if (onlyAExists || onlyBExists) { + changes.add(String.format("%s changed to: %s", key, b.orElse(""))); + return changes; + } + + String aString = a.map(Object::toString).orElse(""); + String bString = b.map(Object::toString).orElse(""); + if (!aString.equals(bString)) { + changes.add(String.format("%s changed to: %s", key, bString)); + } + + return changes; + } + + /** + * Compares to string collections. + * + * @param one first + * @param two second + * @param key label + */ + public static List compareCollections(@NonNull final Collection one, + @NonNull final Collection two, + @NonNull final String key + ) { + @SuppressWarnings("unchecked") Collection disjunction = CollectionUtils.disjunction(one, two); + String changedKeys = String.join(",", disjunction); + + if (!StringUtils.isEmpty(changedKeys)) { + return List.of(String.format("%s have differences : '%s'", key, changedKeys)); + } + return Collections.emptyList(); + } +} diff --git a/src/main/java/de/bonndan/nivio/model/Group.java b/src/main/java/de/bonndan/nivio/model/Group.java index 3ce02890c..44677e17b 100644 --- a/src/main/java/de/bonndan/nivio/model/Group.java +++ b/src/main/java/de/bonndan/nivio/model/Group.java @@ -12,6 +12,9 @@ import java.util.*; +import static de.bonndan.nivio.model.ComponentDiff.compareCollections; +import static de.bonndan.nivio.model.ComponentDiff.compareStrings; + /** * Group is a container for {@link Item}s. * @@ -222,4 +225,28 @@ public boolean removeItem(@Nullable Item item) { public String getLandscapeIdentifier() { return landscapeIdentifier; } + + /** + * Compare on field level against a newer version. + * + * @param newer the newer version + * @return a list of changes if any changes are present + * @throws IllegalArgumentException if the arg is not comparable + */ + public List getChanges(Group newer) { + if (!newer.getIdentifier().equalsIgnoreCase(this.identifier)) { + throw new IllegalArgumentException("Cannot compare group " + newer.getIdentifier() + " against " + this.getIdentifier()); + } + + List changes = new ArrayList<>(); + changes.addAll(compareStrings(this.contact, newer.contact, "Contact")); + changes.addAll(compareStrings(this.description, newer.description, "Description")); + changes.addAll(compareStrings(this.owner, newer.owner, "Owner")); + changes.addAll(compareStrings(this.color, newer.color, "Color")); + changes.addAll(compareCollections(this.labels.keySet(), newer.labels.keySet(), "Labels")); + changes.addAll(compareCollections(this.labels.values(), newer.labels.values(), "Label value")); + changes.addAll(compareCollections(this.links.keySet(), newer.links.keySet(), "Links")); + + return changes; + } } diff --git a/src/main/java/de/bonndan/nivio/model/Item.java b/src/main/java/de/bonndan/nivio/model/Item.java index 51e379484..84ca7a1cb 100644 --- a/src/main/java/de/bonndan/nivio/model/Item.java +++ b/src/main/java/de/bonndan/nivio/model/Item.java @@ -5,6 +5,9 @@ import de.bonndan.nivio.assessment.StatusValue; import de.bonndan.nivio.input.ItemRelationProcessor; import io.swagger.v3.oas.annotations.media.Schema; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.ListUtils; +import org.apache.commons.collections.MapUtils; import org.springframework.util.StringUtils; import javax.validation.constraints.NotNull; @@ -14,6 +17,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import static de.bonndan.nivio.model.ComponentDiff.*; + @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "fullyQualifiedIdentifier") @JsonInclude(JsonInclude.Include.NON_NULL) public class Item implements Linked, Tagged, Labeled, Assessable { @@ -267,4 +272,36 @@ public String toString() { public Set getAdditionalStatusValues() { return StatusValue.fromMapping(indexedByPrefix(Label.status)); } + + + /** + * Compare on field level against a newer version. + * + * @param newer the newer version + * @return a list of changes if any changes are present + * @throws IllegalArgumentException if the arg is not comparable + */ + public List getChanges(Item newer) { + if (!newer.equals(this)) { + throw new IllegalArgumentException("Cannot compare component " + newer.toString() + " against " + this.toString()); + } + + List changes = new ArrayList<>(); + changes.addAll(compareStrings(this.contact, newer.contact, "Contact")); + changes.addAll(compareStrings(this.description, newer.description, "Description")); + changes.addAll(compareStrings(this.name, newer.name, "Name")); + changes.addAll(compareStrings(this.owner, newer.owner, "Owner")); + changes.addAll(compareOptionals(Optional.ofNullable(this.address), Optional.ofNullable(newer.address), "Address")); + changes.addAll(compareCollections(this.labels.keySet(), newer.labels.keySet(), "Labels")); + changes.addAll(compareCollections(this.labels.values(), newer.labels.values(), "Label value")); + changes.addAll(compareCollections(this.links.keySet(), newer.links.keySet(), "Links")); + + List collect = this.interfaces.stream().map(ServiceInterface::toString).collect(Collectors.toList()); + List collect2 = newer.getInterfaces().stream().map(ServiceInterface::toString).collect(Collectors.toList()); + changes.addAll(compareCollections(collect, collect2, "Links")); + + return changes; + } + + } diff --git a/src/main/java/de/bonndan/nivio/model/Landscape.java b/src/main/java/de/bonndan/nivio/model/Landscape.java index a7ff300b5..3f702cfa2 100644 --- a/src/main/java/de/bonndan/nivio/model/Landscape.java +++ b/src/main/java/de/bonndan/nivio/model/Landscape.java @@ -175,16 +175,23 @@ public int hashCode() { return Objects.hash(StringUtils.trimAllWhitespace(identifier)); } - public void addGroup(@NonNull Group group) { + /** + * Add a group. + * + * @param group the group to add + * @return the added or merged group + */ + public Group addGroup(@NonNull Group group) { if (group == null) { throw new IllegalArgumentException("Trying to add null group"); } - if (groups.containsKey(group.getIdentifier())) { group = GroupFactory.merge(groups.get(group.getIdentifier()), group); } groups.put(group.getIdentifier(), group); + + return group; } /** diff --git a/src/main/java/de/bonndan/nivio/model/Relation.java b/src/main/java/de/bonndan/nivio/model/Relation.java index 169aec166..23b5b69c3 100644 --- a/src/main/java/de/bonndan/nivio/model/Relation.java +++ b/src/main/java/de/bonndan/nivio/model/Relation.java @@ -6,7 +6,13 @@ import org.springframework.util.StringUtils; import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; +import java.util.Optional; + +import static de.bonndan.nivio.model.ComponentDiff.compareOptionals; +import static de.bonndan.nivio.model.ComponentDiff.compareStrings; /** * Indication of an incoming or outgoing relation like data flow or dependency (provider). @@ -131,4 +137,24 @@ static class ApiModel { } } } + + /** + * Compare on field level against a newer version. + * + * @param newer the newer version + * @return a list of changes if any changes are present + * @throws IllegalArgumentException if the arg is not comparable + */ + public List getChanges(Relation newer) { + if (!newer.equals(this)) { + throw new IllegalArgumentException("Cannot compare relation " + newer.toString() + " against " + this.toString()); + } + + List changes = new ArrayList<>(); + changes.addAll(compareStrings(this.format, newer.format, "Format")); + changes.addAll(compareStrings(this.description, newer.description, "Description")); + changes.addAll(compareOptionals(Optional.ofNullable(this.type), Optional.ofNullable(newer.type), "Type")); + + return changes; + } } diff --git a/src/main/java/de/bonndan/nivio/model/RelationBuilder.java b/src/main/java/de/bonndan/nivio/model/RelationBuilder.java index 00d0699bd..f47e1c5d6 100644 --- a/src/main/java/de/bonndan/nivio/model/RelationBuilder.java +++ b/src/main/java/de/bonndan/nivio/model/RelationBuilder.java @@ -50,6 +50,14 @@ public static RelationDescription provides(String source, ItemDescription target return relationDescription; } + /** + * Returns a new relation with values updated by the description. + * + * @param existing existing relation + * @param description incoming data + * @return new copy + */ + @NonNull public static Relation update(@NonNull final Relation existing, @NonNull final RelationDescription description) { Objects.requireNonNull(existing); Objects.requireNonNull(description); diff --git a/src/main/java/de/bonndan/nivio/model/ServiceInterface.java b/src/main/java/de/bonndan/nivio/model/ServiceInterface.java index 80a3c338c..11fd40642 100644 --- a/src/main/java/de/bonndan/nivio/model/ServiceInterface.java +++ b/src/main/java/de/bonndan/nivio/model/ServiceInterface.java @@ -71,4 +71,14 @@ public String getPayload() { public String getSummary() { return summary; } + + @Override + public String toString() { + return "ServiceInterface{" + + "description='" + description + '\'' + + ", format='" + format + '\'' + + ", url=" + url + + ", name='" + name + '\'' + + '}'; + } } diff --git a/src/main/java/de/bonndan/nivio/notification/EventNotification.java b/src/main/java/de/bonndan/nivio/notification/EventNotification.java index c90127a52..0650b5fee 100644 --- a/src/main/java/de/bonndan/nivio/notification/EventNotification.java +++ b/src/main/java/de/bonndan/nivio/notification/EventNotification.java @@ -2,8 +2,12 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; +import de.bonndan.nivio.input.ProcessingChangelog; import de.bonndan.nivio.input.ProcessingEvent; +import de.bonndan.nivio.input.ProcessingFinishedEvent; import de.bonndan.nivio.model.FullyQualifiedIdentifier; +import de.bonndan.nivio.observation.InputChangedEvent; +import io.swagger.v3.oas.annotations.media.Schema; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -23,6 +27,7 @@ public class EventNotification { private final String level; private final String type; private final long timestamp; + private final ProcessingChangelog changelog; /** * @param processingEvent application event @@ -35,21 +40,47 @@ public static EventNotification from(ProcessingEvent processingEvent) { processingEvent.getType(), processingEvent.getLevel(), processingEvent.getTimestamp(), - processingEvent.getMessage() + processingEvent.getMessage(), + null ); } - public EventNotification(@NonNull final FullyQualifiedIdentifier landscapeIdentifier, - @NonNull final String type, - @NonNull final String level, - final long timestamp, - @Nullable final String message + public static EventNotification from(ProcessingFinishedEvent processingEvent) { + return new EventNotification( + processingEvent.getSource(), + processingEvent.getType(), + processingEvent.getLevel(), + processingEvent.getTimestamp(), + processingEvent.getMessage(), + processingEvent.getChangelog() + ); + } + + public static EventNotification from(InputChangedEvent inputChangedEvent) { + return new EventNotification( + inputChangedEvent.getSource().getLandscape().getFullyQualifiedIdentifier(), + InputChangedEvent.class.getSimpleName(), + ProcessingEvent.LOG_LEVEL_INFO, + inputChangedEvent.getTimestamp(), + String.join("; ", inputChangedEvent.getSource().getChanges()), + null + ); + } + + private EventNotification( + @NonNull final FullyQualifiedIdentifier landscapeIdentifier, + @NonNull final String type, + @NonNull final String level, + final long timestamp, + @Nullable final String message, + @Nullable final ProcessingChangelog changelog ) { this.landscapeIdentifier = Objects.requireNonNull(landscapeIdentifier); this.message = message; this.level = level; this.type = type; this.timestamp = timestamp; + this.changelog = changelog; } /** @@ -76,9 +107,7 @@ public String getMessage() { return message; } - /** - * The landscape identifier (can be used as url part). - */ + @Schema(description = "The landscape identifier (can be used as url part)") public String getLandscape() { return landscapeIdentifier.jsonValue(); } @@ -88,4 +117,10 @@ public LocalDateTime getDate() { Instant instant = Instant.ofEpochMilli(timestamp); return instant.atZone(ZoneId.systemDefault()).toLocalDateTime(); } + + @Schema(description = "In case of ProcessingFinishedEvent a changelog is contained.") + @Nullable + public ProcessingChangelog getChangelog() { + return changelog; + } } diff --git a/src/main/java/de/bonndan/nivio/notification/MessagingService.java b/src/main/java/de/bonndan/nivio/notification/MessagingService.java index eb7905cef..42e01c254 100644 --- a/src/main/java/de/bonndan/nivio/notification/MessagingService.java +++ b/src/main/java/de/bonndan/nivio/notification/MessagingService.java @@ -1,6 +1,6 @@ package de.bonndan.nivio.notification; -import de.bonndan.nivio.input.ProcessingEvent; +import de.bonndan.nivio.input.ProcessingErrorEvent; import de.bonndan.nivio.input.ProcessingFinishedEvent; import de.bonndan.nivio.observation.InputChangedEvent; import org.apache.commons.collections4.queue.CircularFifoQueue; @@ -32,7 +32,15 @@ public MessagingService(SimpMessagingTemplate template) { } @EventListener(ProcessingFinishedEvent.class) - public void onProcessingEvent(ProcessingEvent processingEvent) { + public void onProcessingFinishedEvent(ProcessingFinishedEvent processingEvent) { + EventNotification eventNotification = EventNotification.from(processingEvent); + fifo.add(eventNotification); + LOGGER.debug("Broadcasting processing event: " + processingEvent.getType()); + this.template.convertAndSend(WebSocketConfig.TOPIC + EVENTS, eventNotification); + } + + @EventListener(ProcessingErrorEvent.class) + public void onProcessingErrorEvent(ProcessingErrorEvent processingEvent) { EventNotification eventNotification = EventNotification.from(processingEvent); fifo.add(eventNotification); LOGGER.debug("Broadcasting processing event: " + processingEvent.getType()); @@ -41,13 +49,7 @@ public void onProcessingEvent(ProcessingEvent processingEvent) { @EventListener(InputChangedEvent.class) public void onInputChangedEvent(InputChangedEvent inputChangedEvent) { - EventNotification eventNotification = new EventNotification( - inputChangedEvent.getSource().getLandscape().getFullyQualifiedIdentifier(), - InputChangedEvent.class.getSimpleName(), - ProcessingEvent.LOG_LEVEL_INFO, - inputChangedEvent.getTimestamp(), - String.join("; ", inputChangedEvent.getSource().getChanges()) - ); + EventNotification eventNotification = EventNotification.from(inputChangedEvent); fifo.add(eventNotification); LOGGER.debug("Broadcasting input change event."); this.template.convertAndSend(WebSocketConfig.TOPIC + EVENTS, eventNotification); diff --git a/src/test/java/de/bonndan/nivio/input/GroupProcessorTest.java b/src/test/java/de/bonndan/nivio/input/GroupProcessorTest.java index d957fdc5f..79c031918 100644 --- a/src/test/java/de/bonndan/nivio/input/GroupProcessorTest.java +++ b/src/test/java/de/bonndan/nivio/input/GroupProcessorTest.java @@ -9,7 +9,9 @@ import org.slf4j.LoggerFactory; import java.util.Arrays; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; class GroupProcessorTest { @@ -27,13 +29,35 @@ public void setup() { @Test void process() { + //given LandscapeDescription input = getLandscapeDescription(); - groupProcessor.process(input, landscape); + //when + ProcessingChangelog process = groupProcessor.process(input, landscape); + //then assertEquals(3, landscape.getGroups().size()); } + @Test + void withNewGroupChangelog() { + + //given + LandscapeDescription input = getLandscapeDescription(); + ItemDescription itemDescription = new ItemDescription("a"); + itemDescription.setGroup("foobar"); + input.mergeItems(List.of(itemDescription)); + + //when + ProcessingChangelog process = groupProcessor.process(input, landscape); + + //then + assertEquals(4, landscape.getGroups().size()); + assertThat(process.changes).hasSize(3); + assertThat(process.changes).containsKey("test/foobar"); + assertThat(process.changes.get("test/foobar").getChangeType()).isEqualTo(ProcessingChangelog.ChangeType.CREATED.name()); + } + @Test void processAddCommonGroup() { diff --git a/src/test/java/de/bonndan/nivio/input/IndexerIntegrationTest.java b/src/test/java/de/bonndan/nivio/input/IndexerIntegrationTest.java index 4eb958c7b..4454029b4 100644 --- a/src/test/java/de/bonndan/nivio/input/IndexerIntegrationTest.java +++ b/src/test/java/de/bonndan/nivio/input/IndexerIntegrationTest.java @@ -185,7 +185,15 @@ public void testIncrementalUpdate() { assertEquals("Other name", wordpress.getName()); assertEquals("content", wordpress.getGroup()); - + //testing changelog + ArgumentCaptor captor = ArgumentCaptor.forClass(ProcessingFinishedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(captor.capture()); + ProcessingFinishedEvent value = captor.getAllValues().get(1); + assertThat(value).isNotNull(); + ProcessingChangelog changelog = value.getChangelog(); + assertThat(changelog).isNotNull(); + assertThat(changelog.changes).hasSize(3); + assertThat(changelog.changes).containsKey("nivio:example/content/wordpress-web"); } /** diff --git a/src/test/java/de/bonndan/nivio/input/ItemRelationProcessorTest.java b/src/test/java/de/bonndan/nivio/input/ItemRelationProcessorTest.java new file mode 100644 index 000000000..93a1d8efa --- /dev/null +++ b/src/test/java/de/bonndan/nivio/input/ItemRelationProcessorTest.java @@ -0,0 +1,102 @@ +package de.bonndan.nivio.input; + +import de.bonndan.nivio.input.dto.ItemDescription; +import de.bonndan.nivio.input.dto.LandscapeDescription; +import de.bonndan.nivio.input.dto.RelationDescription; +import de.bonndan.nivio.model.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + + +class ItemRelationProcessorTest { + + private LandscapeDescription input; + private Landscape landscape; + private ItemRelationProcessor processor; + + @BeforeEach + void setUp() { + input = new LandscapeDescription("test"); + Set items = new HashSet<>(); + Item foo = ItemFactory.getTestItem("a", "foo"); + items.add(foo); + Item bar = ItemFactory.getTestItem("a", "bar"); + items.add(bar); + Item baz = ItemFactory.getTestItem("a", "baz"); + items.add(baz); + + foo.getRelations().add(new Relation(foo, bar)); + bar.getRelations().add(new Relation(foo, bar)); + + foo.getRelations().add(new Relation(foo, baz)); + baz.getRelations().add(new Relation(foo, baz)); + + landscape = LandscapeFactory.createForTesting("test", "test").withItems(items).build(); + + processor = new ItemRelationProcessor(new ProcessLog(LoggerFactory.getLogger(ItemRelationProcessorTest.class))); + } + + @Test + void processAddsRelation() { + + ItemDescription description = new ItemDescription("foo"); + description.setGroup("a"); + description.addRelation(new RelationDescription("foo", "bar")); + description.addRelation(new RelationDescription("foo", "baz")); + input.mergeItems(List.of(description)); + //new + ItemDescription bar = new ItemDescription("bar"); + bar.setGroup("a"); + description.addRelation(new RelationDescription("bar", "baz")); + input.mergeItems(List.of(bar)); + + //when + ProcessingChangelog process = processor.process(input, landscape); + + //then + assertThat(process.changes).hasSize(1); //no updates, one created + } + + //only changes are counted as updates + @Test + void processAddsUpdates() { + + ItemDescription description = new ItemDescription("foo"); + description.setGroup("a"); + RelationDescription relationItem = new RelationDescription("foo", "bar"); + relationItem.setFormat("JSON"); + description.addRelation(relationItem); + description.addRelation(new RelationDescription("foo", "baz")); + input.mergeItems(List.of(description)); + + //when + ProcessingChangelog process = processor.process(input, landscape); + + //then + assertThat(process.changes).hasSize(1); //one update + } + + @Test + void processRemovesRelation() { + + ItemDescription description = new ItemDescription("foo"); + description.setGroup("a"); + description.addRelation(new RelationDescription("foo", "bar")); + input.mergeItems(List.of(description)); + + //when + ProcessingChangelog process = processor.process(input, landscape); + + //then + assertThat(process.changes).hasSize(1); //no update, one delete + assertThat(process.changes).containsKey("test/a/foo;test/a/baz"); + assertThat(process.changes.get("test/a/foo;test/a/baz").getChangeType()).isEqualTo(ProcessingChangelog.ChangeType.DELETED.name()); + } +} \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/input/ProcessingChangelogTest.java b/src/test/java/de/bonndan/nivio/input/ProcessingChangelogTest.java new file mode 100644 index 000000000..2a8d2ae91 --- /dev/null +++ b/src/test/java/de/bonndan/nivio/input/ProcessingChangelogTest.java @@ -0,0 +1,114 @@ +package de.bonndan.nivio.input; + +import de.bonndan.nivio.model.Item; +import de.bonndan.nivio.model.ItemFactory; +import de.bonndan.nivio.model.Relation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class ProcessingChangelogTest { + + private ProcessingChangelog changelog; + private Item testItem; + + @BeforeEach + void setUp() { + changelog = new ProcessingChangelog(); + testItem = ItemFactory.getTestItem("a", "b"); + } + + @Test + void addEntry() { + //when + changelog.addEntry(testItem, ProcessingChangelog.ChangeType.CREATED, "foo"); + + //then + Map changes = changelog.changes; + assertThat(changes).isNotNull().hasSize(1); + + ProcessingChangelog.Entry actual = changes.get(testItem.getFullyQualifiedIdentifier().jsonValue()); + assertThat(actual).isNotNull(); + assertThat(actual.getChangeType()).isEqualTo(ProcessingChangelog.ChangeType.CREATED.name()); + assertThat(actual.getComponentType()).isEqualTo(testItem.getClass().getSimpleName()); + assertThat(actual.getMessage()).isEqualTo("foo"); + } + + @Test + void testAddEntryWithoutMessage() { + //when + changelog.addEntry(testItem, ProcessingChangelog.ChangeType.DELETED); + + //then + Map changes = changelog.changes; + assertThat(changes).isNotNull().hasSize(1); + + //then + ProcessingChangelog.Entry actual = changes.get(testItem.getFullyQualifiedIdentifier().jsonValue()); + assertThat(actual).isNotNull(); + assertThat(actual.getChangeType()).isEqualTo(ProcessingChangelog.ChangeType.DELETED.name()); + assertThat(actual.getComponentType()).isEqualTo(testItem.getClass().getSimpleName()); + assertThat(actual.getMessage()).isNull(); + } + + @Test + void addRelation() { + //when + Item bar = ItemFactory.getTestItem("foo", "bar"); + Relation relation = new Relation(this.testItem, bar); + changelog.addEntry(relation, ProcessingChangelog.ChangeType.DELETED, null); + + //then + Map changes = changelog.changes; + assertThat(changes).isNotNull().hasSize(1); + + //then + String key = changelog.getRelationKey(relation); + ProcessingChangelog.Entry actual = changes.get(key); + assertThat(actual).isNotNull(); + assertThat(actual.getChangeType()).isEqualTo(ProcessingChangelog.ChangeType.DELETED.name()); + assertThat(actual.getComponentType()).isEqualTo(relation.getClass().getSimpleName()); + assertThat(actual.getMessage()).isNull(); + } + + @Test + void merge() { + //given + changelog.addEntry(testItem, ProcessingChangelog.ChangeType.UPDATED, "foo"); + + ProcessingChangelog incoming = new ProcessingChangelog(); + incoming.addEntry(testItem, ProcessingChangelog.ChangeType.UPDATED, "bar"); + + //when + changelog.merge(incoming); + + //then + Map changes = changelog.changes; + assertThat(changes).isNotNull().hasSize(1); + ProcessingChangelog.Entry actual = changes.get(testItem.getFullyQualifiedIdentifier().jsonValue()); + assertThat(actual).isNotNull(); + assertThat(actual.getMessage()).isEqualTo("foo; bar"); + + } + + @Test + void mergeDifferent() { + //given + changelog.addEntry(testItem, ProcessingChangelog.ChangeType.UPDATED, "foo"); + + ProcessingChangelog incoming = new ProcessingChangelog(); + incoming.addEntry(ItemFactory.getTestItem("b", "hihi"), ProcessingChangelog.ChangeType.UPDATED, "bar"); + + //when + changelog.merge(incoming); + + //then + Map changes = changelog.changes; + assertThat(changes).isNotNull().hasSize(2); + + } +} \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/model/ComponentDiffTest.java b/src/test/java/de/bonndan/nivio/model/ComponentDiffTest.java new file mode 100644 index 000000000..791d228eb --- /dev/null +++ b/src/test/java/de/bonndan/nivio/model/ComponentDiffTest.java @@ -0,0 +1,106 @@ +package de.bonndan.nivio.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class ComponentDiffTest { + + private List changes; + + @BeforeEach + void setup() { + changes = new ArrayList<>(); + } + + @Test + void twoNulls() { + changes.addAll(ComponentDiff.compareStrings(null, null, "foo")); + assertThat(changes).hasSize(0); + } + + @Test + void twoNull() { + changes.addAll(ComponentDiff.compareStrings("a", null, "foo")); + assertThat(changes).hasSize(1); + } + + @Test + void oneNull() { + changes.addAll(ComponentDiff.compareStrings(null, "b", "foo")); + assertThat(changes).hasSize(1); + } + + @Test + void similarString() { + changes.addAll(ComponentDiff.compareStrings("b", "b", "foo")); + assertThat(changes).hasSize(0); + } + + @Test + void differentString() { + changes.addAll(ComponentDiff.compareStrings("a", "b", "foo")); + assertThat(changes).hasSize(1); + } + + @Test + void bothEmpty() { + changes.addAll(ComponentDiff.compareOptionals(Optional.empty(), Optional.empty(), "foo")); + assertThat(changes).hasSize(0); + } + + @Test + void oneEmpty() { + changes.addAll(ComponentDiff.compareOptionals(Optional.empty(), Optional.of("a"), "foo")); + assertThat(changes).hasSize(1); + } + + @Test + void twoEmpty() { + changes.addAll(ComponentDiff.compareOptionals(Optional.of("a"), Optional.empty(), "foo")); + assertThat(changes).hasSize(1); + } + + @Test + void similarOptionals() { + changes.addAll(ComponentDiff.compareOptionals(Optional.of("a"), Optional.of("a"), "foo")); + + assertThat(changes).hasSize(0); + } + + @Test + void differentOptionals() { + changes.addAll(ComponentDiff.compareOptionals(Optional.of("a"), Optional.of("b"), "foo")); + + assertThat(changes).hasSize(1); + } + + @Test + void similarCollections() { + changes.addAll(ComponentDiff.compareCollections(List.of("a", "b"), List.of("b", "a"), "foo")); + assertThat(changes).hasSize(0); + } + + @Test + void differentCollectionEntries() { + changes.addAll(ComponentDiff.compareCollections(List.of("a", "b"), List.of("c", "a"), "foo")); + assertThat(changes).hasSize(1); + } + + @Test + void differentCollectionSize() { + changes.addAll(ComponentDiff.compareCollections(List.of("a", "b"), List.of("a", "b", "c"), "foo")); + assertThat(changes).hasSize(1); + } + + @Test + void differentCollectionSize2() { + changes.addAll(ComponentDiff.compareCollections(List.of("a", "b", "c"), List.of("a", "b"), "foo")); + assertThat(changes).hasSize(1); + } +} \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/model/GroupTest.java b/src/test/java/de/bonndan/nivio/model/GroupTest.java index 872a7a231..862c717d5 100644 --- a/src/test/java/de/bonndan/nivio/model/GroupTest.java +++ b/src/test/java/de/bonndan/nivio/model/GroupTest.java @@ -2,6 +2,8 @@ import org.junit.jupiter.api.Test; +import java.util.List; + import static de.bonndan.nivio.model.ItemFactory.getTestItem; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -56,4 +58,24 @@ void removeItemFails() { assertThat(b).isFalse(); assertThat(g.getItems()).hasSize(1); } + + @Test + void hasNoChanges() { + Group g1 = new Group("foo", "bar", "John", null, null, null, null); + Group g2 = new Group("foo", "bar", "John", null, null, null, null); + + //when + List changes = g1.getChanges(g2); + assertThat(changes).isEmpty(); + } + + @Test + void hasChanges() { + Group g1 = new Group("foo", "bar", "John", null, null, null, null); + Group g2 = new Group("foo", "bar", "Doe", null, null, null, null); + + //when + List changes = g1.getChanges(g2); + assertThat(changes).hasSize(1); + } } \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/model/ItemTest.java b/src/test/java/de/bonndan/nivio/model/ItemTest.java index 16386d772..b26f5948e 100644 --- a/src/test/java/de/bonndan/nivio/model/ItemTest.java +++ b/src/test/java/de/bonndan/nivio/model/ItemTest.java @@ -2,9 +2,11 @@ import org.junit.jupiter.api.Test; +import java.util.List; import java.util.Map; import static de.bonndan.nivio.model.ItemFactory.getTestItem; +import static de.bonndan.nivio.model.ItemFactory.getTestItemBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; @@ -61,4 +63,83 @@ public void labelsAreNotGroupedInApi() { assertThat(labels).containsKey("foo.one"); assertThat(labels).containsKey("foo.two"); } + + @Test + public void getChangesInLabels() { + Landscape landscape = LandscapeFactory.createForTesting("l1", "l1Landscape").build(); + + Item s1 = getTestItem("g1", "a", landscape); + s1.getLabels().put("foo.one", "one"); + + Item s2 = getTestItem("g1", "a", landscape); + s2.getLabels().put("foo.one", "two"); + + List changes = s1.getChanges(s2); + assertThat(changes).isNotNull().hasSize(1); + assertThat(changes.get(0)).contains("two"); + } + + @Test + public void getChangesInName() { + + Item s1 = getTestItemBuilder("g1", "a") + .withName("foo") + .build(); + + Item s2 = getTestItemBuilder("g1", "a") + .withName("bar") + .build(); + + List changes = s1.getChanges(s2); + assertThat(changes).isNotNull().hasSize(1); + assertThat(changes.get(0)).contains("Name"); + } + + @Test + public void getChangesInDescription() { + + Item s1 = getTestItemBuilder("g1", "a") + .withDescription("foo") + .build(); + + Item s2 = getTestItemBuilder("g1", "a") + .withDescription("bar") + .build(); + + List changes = s1.getChanges(s2); + assertThat(changes).isNotNull().hasSize(1); + assertThat(changes.get(0)).contains("Description"); + } + + @Test + public void getChangesInOwner() { + + Item s1 = getTestItemBuilder("g1", "a") + .withOwner("foo") + .build(); + + Item s2 = getTestItemBuilder("g1", "a") + .withOwner("bar") + .build(); + + List changes = s1.getChanges(s2); + assertThat(changes).isNotNull().hasSize(1); + assertThat(changes.get(0)).contains("Owner"); + } + + @Test + public void getChangesInLinks() { + + Item s1 = getTestItemBuilder("g1", "a") + .withLinks(Map.of("foo", new Link("https://acme.com"))) + .build(); + + Item s2 = getTestItemBuilder("g1", "a") + .withLinks(Map.of("bar", new Link("https://acme.com"))) + .build(); + + List changes = s1.getChanges(s2); + assertThat(changes).isNotNull().hasSize(1); + assertThat(changes.get(0)).contains("Links"); + } } diff --git a/src/test/java/de/bonndan/nivio/model/RelationTest.java b/src/test/java/de/bonndan/nivio/model/RelationTest.java index 33305a082..f98d90672 100644 --- a/src/test/java/de/bonndan/nivio/model/RelationTest.java +++ b/src/test/java/de/bonndan/nivio/model/RelationTest.java @@ -2,6 +2,8 @@ import org.junit.jupiter.api.Test; +import java.util.List; + import static de.bonndan.nivio.model.ItemFactory.getTestItem; import static de.bonndan.nivio.model.ItemFactory.getTestItemBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -51,4 +53,63 @@ void inboundName() { assertThat(apiModel.direction).isEqualTo(Relation.ApiModel.INBOUND); assertThat(apiModel.name).isEqualTo("huhu"); } + + @Test + void getChangesInType() { + Item b = getTestItem("a", "b"); + Item c = getTestItem("a", "c"); + Relation before = new Relation(b, c, "foo", "JSON", null); + Relation after = new Relation(b, c, "foo", "JSON", RelationType.PROVIDER); + + //when + List changes = before.getChanges(after); + + //then + assertThat(changes).isNotNull().hasSize(1); + assertThat(changes.get(0)).contains("Type changed "); + } + + @Test + void getChangesInFormat() { + Item b = getTestItem("a", "b"); + Item c = getTestItem("a", "c"); + Relation before = new Relation(b, c, "foo", "JSON", RelationType.PROVIDER); + Relation after = new Relation(b, c, "foo", "XML", RelationType.PROVIDER); + + //when + List changes = before.getChanges(after); + + //then + assertThat(changes).isNotNull().hasSize(1); + assertThat(changes.get(0)).contains("Format changed "); + } + + @Test + void getChangesInDescription() { + Item b = getTestItem("a", "b"); + Item c = getTestItem("a", "c"); + Relation before = new Relation(b, c, "foo", "JSON", RelationType.PROVIDER); + Relation after = new Relation(b, c, "bar", "JSON", RelationType.PROVIDER); + + //when + List changes = before.getChanges(after); + + //then + assertThat(changes).isNotNull().hasSize(1); + assertThat(changes.get(0)).contains("Description changed "); + } + + @Test + void hasNoChange() { + Item b = getTestItem("a", "b"); + Item c = getTestItem("a", "c"); + Relation before = new Relation(b, c, "foo", "JSON", RelationType.PROVIDER); + Relation after = new Relation(b, c, "foo", "JSON", RelationType.PROVIDER); + + //when + List changes = before.getChanges(after); + + //then + assertThat(changes).isNotNull().hasSize(0); + } } \ No newline at end of file diff --git a/src/test/java/de/bonndan/nivio/notification/MessagingServiceTest.java b/src/test/java/de/bonndan/nivio/notification/MessagingServiceTest.java index d3e954834..6b8e268ff 100644 --- a/src/test/java/de/bonndan/nivio/notification/MessagingServiceTest.java +++ b/src/test/java/de/bonndan/nivio/notification/MessagingServiceTest.java @@ -1,5 +1,6 @@ package de.bonndan.nivio.notification; +import de.bonndan.nivio.input.ProcessingChangelog; import de.bonndan.nivio.input.ProcessingFinishedEvent; import de.bonndan.nivio.input.dto.LandscapeDescription; import de.bonndan.nivio.model.LandscapeFactory; @@ -29,12 +30,13 @@ public void setup() { } @Test - void onProcessingEvent() { + void onProcessingFinishedEvent() { ProcessingFinishedEvent processingFinishedEvent = new ProcessingFinishedEvent( new LandscapeDescription("test", "testLandscape", null), - LandscapeFactory.createForTesting("test", "testLandscape").build() + LandscapeFactory.createForTesting("test", "testLandscape").build(), + new ProcessingChangelog() ); - messagingService.onProcessingEvent(processingFinishedEvent); + messagingService.onProcessingFinishedEvent(processingFinishedEvent); ArgumentCaptor captor = ArgumentCaptor.forClass(EventNotification.class); verify(tpl).convertAndSend(eq(WebSocketConfig.TOPIC + WebSocketConfig.EVENTS), captor.capture()); @@ -69,9 +71,10 @@ void getLast() { ProcessingFinishedEvent processingFinishedEvent = new ProcessingFinishedEvent( new LandscapeDescription("test", "testLandscape", null), - LandscapeFactory.createForTesting("test", "testLandscape").build() + LandscapeFactory.createForTesting("test", "testLandscape").build(), + new ProcessingChangelog() ); - messagingService.onProcessingEvent(processingFinishedEvent); + messagingService.onProcessingFinishedEvent(processingFinishedEvent); EventNotification[] last = messagingService.getLast(); assertNotNull(last); diff --git a/src/test/java/de/bonndan/nivio/observation/ObserverRegistryTest.java b/src/test/java/de/bonndan/nivio/observation/ObserverRegistryTest.java index 5976e9877..f06dff5c9 100644 --- a/src/test/java/de/bonndan/nivio/observation/ObserverRegistryTest.java +++ b/src/test/java/de/bonndan/nivio/observation/ObserverRegistryTest.java @@ -1,9 +1,6 @@ package de.bonndan.nivio.observation; -import de.bonndan.nivio.input.IndexingDispatcher; -import de.bonndan.nivio.input.ProcessingFinishedEvent; -import de.bonndan.nivio.input.FileFetcher; -import de.bonndan.nivio.input.LandscapeDescriptionFactory; +import de.bonndan.nivio.input.*; import de.bonndan.nivio.input.dto.LandscapeDescription; import de.bonndan.nivio.input.http.HttpService; import de.bonndan.nivio.model.LandscapeFactory; @@ -58,7 +55,7 @@ public void onProcessingFinishedEvent() { .withSource(source) .build(); - ProcessingFinishedEvent event = new ProcessingFinishedEvent(description, landscape); + ProcessingFinishedEvent event = new ProcessingFinishedEvent(description, landscape, new ProcessingChangelog()); when(this.landscapeDescriptionFactory.from(landscape)).thenReturn(description); when(observerPoolFactory.getObserversFor(eq(landscape), eq(description))).thenReturn(new ArrayList<>()); diff --git a/src/test/java/de/bonndan/nivio/output/map/RenderCacheTest.java b/src/test/java/de/bonndan/nivio/output/map/RenderCacheTest.java index 76e778fc0..c96a1566e 100644 --- a/src/test/java/de/bonndan/nivio/output/map/RenderCacheTest.java +++ b/src/test/java/de/bonndan/nivio/output/map/RenderCacheTest.java @@ -1,5 +1,6 @@ package de.bonndan.nivio.output.map; +import de.bonndan.nivio.input.ProcessingChangelog; import de.bonndan.nivio.input.ProcessingFinishedEvent; import de.bonndan.nivio.input.AppearanceProcessor; import de.bonndan.nivio.input.ProcessLog; @@ -76,7 +77,8 @@ void toSVG() { void onProcessingFinishedEvent() { renderCache.onApplicationEvent(new ProcessingFinishedEvent( new LandscapeDescription("test", "testLandscape", null), - getLandscape("test", "testLandscape") + getLandscape("test", "testLandscape"), + new ProcessingChangelog() )); verify(stylesheetFactory, times(1)).getMapStylesheet(any(), any());