/*
* Copyright 2015-2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.hawkular.inventory.base;
import static java.util.Collections.singletonList;
import static org.hawkular.inventory.api.Relationships.Direction.outgoing;
import static org.hawkular.inventory.api.Relationships.WellKnown.contains;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Spliterators;
import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.hawkular.inventory.api.Data;
import org.hawkular.inventory.api.EntityNotFoundException;
import org.hawkular.inventory.api.Feeds;
import org.hawkular.inventory.api.Inventory;
import org.hawkular.inventory.api.Log;
import org.hawkular.inventory.api.MetricTypes;
import org.hawkular.inventory.api.Metrics;
import org.hawkular.inventory.api.OperationTypes;
import org.hawkular.inventory.api.Query;
import org.hawkular.inventory.api.ResolvableToSingle;
import org.hawkular.inventory.api.ResourceTypes;
import org.hawkular.inventory.api.Resources;
import org.hawkular.inventory.api.Synced;
import org.hawkular.inventory.api.model.AbstractElement;
import org.hawkular.inventory.api.model.Blueprint;
import org.hawkular.inventory.api.model.DataEntity;
import org.hawkular.inventory.api.model.ElementBlueprintVisitor;
import org.hawkular.inventory.api.model.Entity;
import org.hawkular.inventory.api.model.Feed;
import org.hawkular.inventory.api.model.InventoryStructure;
import org.hawkular.inventory.api.model.Metric;
import org.hawkular.inventory.api.model.MetricType;
import org.hawkular.inventory.api.model.OperationType;
import org.hawkular.inventory.api.model.Resource;
import org.hawkular.inventory.api.model.ResourceType;
import org.hawkular.inventory.api.model.SyncConfiguration;
import org.hawkular.inventory.api.model.SyncHash;
import org.hawkular.inventory.api.model.SyncRequest;
import org.hawkular.inventory.api.model.Syncable;
import org.hawkular.inventory.base.spi.ElementNotFoundException;
import org.hawkular.inventory.paths.CanonicalPath;
import org.hawkular.inventory.paths.DataRole;
import org.hawkular.inventory.paths.ElementTypeVisitor;
import org.hawkular.inventory.paths.Path;
import org.hawkular.inventory.paths.RelativePath;
import org.hawkular.inventory.paths.SegmentType;
/**
* @author Lukas Krejci
* @since 0.15.0
*/
abstract class SingleSyncedFetcher<BE, E extends Entity<B, U> & Syncable, B extends Entity.Blueprint,
U extends Entity.Update>
extends SingleEntityFetcher<BE, E, U>
implements Synced.SingleEntity<E, B, U> {
SingleSyncedFetcher(TraversalContext<BE, E> context) {
super(context);
}
@Override public void synchronize(SyncRequest<B> syncRequest) {
inTx(tx -> {
BE root = tx.querySingle(context.discriminator(), context.select().get());
boolean rootFullyInitialized = true;
if (root == null) {
Mutator<BE, E, B, U, String> mutator = createMutator(tx);
EntityAndPendingNotifications<BE, E> res =
mutator.doCreate(syncRequest.getInventoryStructure().getRoot(), tx);
root = res.getEntityRepresentation();
rootFullyInitialized = false;
}
CanonicalPath rootPath = tx.extractCanonicalPath(root);
Map.Entry<InventoryStructure<B>, SyncHash.Tree> structAndTree;
if (rootFullyInitialized) {
structAndTree = treeHashAndStructure(tx);
} else {
structAndTree = new SimpleImmutableEntry<>(
InventoryStructure.of(syncRequest.getInventoryStructure().getRoot()).build(),
SyncHash.Tree.builder().build());
}
SyncHash.Tree currentTree = structAndTree.getValue();
InventoryStructure<B> currentStructure = structAndTree.getKey();
InventoryStructure<B> newStructure =
mergeTree(currentStructure, syncRequest.getInventoryStructure(), syncRequest.getConfiguration());
SyncHash.Tree newTree = SyncHash.treeOf(newStructure, rootPath);
syncTrees(tx, rootPath, root, currentTree, newTree, newStructure);
return null;
});
}
@Override public SyncHash.Tree treeHash() {
return inTx(tx -> treeHashAndStructure(tx).getValue());
}
private InventoryStructure<B> mergeTree(InventoryStructure<B> currentTree, InventoryStructure<B> newTree,
SyncConfiguration configuration) {
if (configuration.isDeepSearch()) {
return mergeDeepTree(InventoryStructure.Offline.copy(currentTree).asBuilder(),
InventoryStructure.Offline.copy(newTree).asBuilder(), configuration.getSyncedTypes()).build();
} else {
return mergeShallowTree(InventoryStructure.Offline.copy(currentTree).asBuilder(),
InventoryStructure.Offline.copy(newTree).asBuilder(), configuration.getSyncedTypes()).build();
}
}
@SuppressWarnings("unchecked")
private InventoryStructure.Builder<B> mergeShallowTree(InventoryStructure.AbstractBuilder<?> currentTree,
InventoryStructure.AbstractBuilder<?> newTree,
Set<SegmentType> mergedTypes) {
Set<Path.Segment> currentChildPaths = currentTree.getChildrenPaths();
Set<Path.Segment> newChildPaths = newTree.getChildrenPaths();
for (Path.Segment ccp : currentChildPaths) {
if (newChildPaths.contains(ccp)) {
mergeShallowTree(currentTree.getChild(ccp), newTree.getChild(ccp), mergedTypes);
} else if (!mergedTypes.contains(ccp.getElementType())) {
newTree.addChild(currentTree.getChild(ccp), true);
}
}
if (newTree instanceof InventoryStructure.Builder) {
return (InventoryStructure.Builder<B>) newTree;
} else {
return null;
}
}
@SuppressWarnings("unchecked")
private InventoryStructure.Builder<B> mergeDeepTree(InventoryStructure.AbstractBuilder<?> currentTree,
InventoryStructure.AbstractBuilder<?> newTree,
Set<SegmentType> mergedTypes) {
Set<Path.Segment> currentChildPaths = currentTree.getChildrenPaths();
Set<Path.Segment> newChildPaths = newTree.getChildrenPaths();
for (Path.Segment ccp : currentChildPaths) {
if (newChildPaths.contains(ccp)) {
mergeDeepTree(currentTree.getChild(ccp), newTree.getChild(ccp), mergedTypes);
} else if (!mergedTypes.contains(ccp.getElementType())) {
newTree.addChild(currentTree.getChild(ccp).getBlueprint());
mergeDeepTree(currentTree.getChild(ccp), newTree.getChild(ccp), mergedTypes);
}
}
if (newTree instanceof InventoryStructure.Builder) {
return (InventoryStructure.Builder<B>) newTree;
} else {
return null;
}
}
@SuppressWarnings("unchecked")
private void syncTrees(Transaction<BE> tx, CanonicalPath root, BE oldElement, SyncHash.Tree oldTree,
SyncHash.Tree newTree, InventoryStructure<?> newStructure) {
if (!Objects.equals(oldTree.getHash(), newTree.getHash())) {
//we only need to do something if the hashes don't match. If they do, it means this entity and its whole
//subtree is equivalent. But it isn't because the hashes differ, so...
Blueprint newState = newStructure.get(newTree.getPath());
Entity.Update entityUpdate = updateFromBlueprint(newState);
Inventory inv = context.inventory.keepTransaction(tx);
//update the current element - use the full API call so that all checks are enforced
inv.inspect(tx.extractCanonicalPath(oldElement), ResolvableToSingle.class).update(entityUpdate);
//now look through the old and new children and make old match new
//it is important to make sure that resource or metric types are create prior to resources or metrics
//we can exploit the InventoryStructure.EntityType enum which is ordered with this in mind.
Set<SyncHash.Tree> unprocessedChildren = sortByType(newTree.getChildren());
Map<SyncHash.Tree, SyncHash.Tree> updates = new HashMap<>();
//again, it is important to create types before to resources or metrics, etc.
Map<InventoryStructure.EntityType, Set<SyncHash.Tree>> childrenByType = splitByType(oldTree
.getChildren());
for (InventoryStructure.EntityType type : InventoryStructure.EntityType.values()) {
Set<SyncHash.Tree> oldChildren = childrenByType.get(type);
if (oldChildren == null) {
continue;
}
for (SyncHash.Tree oldChild : oldChildren) {
SyncHash.Tree newChild = newTree.getChild(oldChild.getPath().getSegment());
if (newChild == null) {
//ok, this entity is no longer in the new structure
CanonicalPath childCp = oldChild.getPath().applyTo(root);
try {
//delete using a normal API so that all checks are run
inv.inspect(childCp, ResolvableToSingle.class).delete();
} catch (EntityNotFoundException e) {
Log.LOGGER.debug("Failed to find a child to be deleted on canonical path " + childCp
+ ". Ignoring this since we were going to delete it anyway.", e);
}
} else {
//kewl, we have a matching child that we need to sync
//let's just postpone the actual update until the end of the method, just in case Java gets
//tail-call optimization ;)
unprocessedChildren.remove(newChild);
updates.put(oldChild, newChild);
}
}
}
//now create the new children
unprocessedChildren.forEach(c -> create(tx, root, c, newStructure));
//and finally updates...
for (Map.Entry<SyncHash.Tree, SyncHash.Tree> e : updates.entrySet()) {
SyncHash.Tree oldChild = e.getKey();
SyncHash.Tree update = e.getValue();
CanonicalPath childCp = update.getPath().applyTo(root);
try {
BE child = tx.find(context.discriminator(), childCp);
syncTrees(tx, root, child, oldChild, update, newStructure);
} catch (ElementNotFoundException ex) {
Log.LOGGER.debug("Failed to find entity on " + childCp + " that we thought was there. Never mind " +
"though, we can just create it again.", ex);
create(tx, root, update, newStructure);
}
}
}
}
private Set<SyncHash.Tree> sortByType(Collection<SyncHash.Tree> col) {
Set<SyncHash.Tree> set = new TreeSet<>((a, b) -> {
InventoryStructure.EntityType aType =
InventoryStructure.EntityType.of(a.getPath().getSegment().getElementType());
InventoryStructure.EntityType bType =
InventoryStructure.EntityType.of(b.getPath().getSegment().getElementType());
int ret = aType.ordinal() - bType.ordinal();
if (ret == 0) {
//this is actually not important.. we only need to make sure we have a total ordering and that
//the entities are sorted by their type..
ret = a.getHash().compareTo(b.getHash());
}
return ret;
});
set.addAll(col);
return set;
}
@SuppressWarnings("unchecked")
private void create(Transaction<BE> tx, CanonicalPath root, SyncHash.Tree tree,
InventoryStructure<?> newStructure) {
Inventory inv = context.inventory.keepTransaction(tx);
CanonicalPath childCp = tree.getPath().applyTo(root);
@SuppressWarnings("unchecked")
ResolvableToSingle<?, ?> parentAccess = inv.inspect(childCp.up(), ResolvableToSingle.class);
Blueprint blueprint = newStructure.get(tree.getPath());
//the blueprint might actually be null, because the hash tree computes hash using some "virtual nodes"
//for data entities - i.e. if a resource doesn't have a configuration an empty "virtual" config is used for
//hash computation.
//We'd see these nodes here, but there's no need to create them.
if (blueprint == null) {
return;
}
childCp.getSegment().accept(new ElementTypeVisitor.Simple<Void, Void>() {
@Override public Void visitFeed(Void parameter) {
((Feeds.Container<Feeds.ReadWrite>) parentAccess).feeds().create((Feed.Blueprint) blueprint);
return null;
}
@Override public Void visitMetric(Void parameter) {
((Metrics.Container<Metrics.ReadWrite>) parentAccess).metrics()
.create((Metric.Blueprint) blueprint);
return null;
}
@Override public Void visitMetricType(Void parameter) {
((MetricTypes.Container<MetricTypes.ReadWrite>) parentAccess).metricTypes()
.create((MetricType.Blueprint) blueprint);
return null;
}
@Override public Void visitResource(Void parameter) {
((Resources.Container<Resources.ReadWrite>) parentAccess).resources()
.create((Resource.Blueprint) blueprint);
return null;
}
@Override public Void visitResourceType(Void parameter) {
((ResourceTypes.Container<ResourceTypes.ReadWrite>) parentAccess).resourceTypes()
.create((ResourceType.Blueprint) blueprint);
return null;
}
@SuppressWarnings("rawtypes")
@Override public Void visitData(Void parameter) {
((Data.Container<Data.ReadWrite>) parentAccess).data()
.create((DataEntity.Blueprint<?>) blueprint);
return null;
}
@Override public Void visitOperationType(Void parameter) {
((OperationTypes.Container<OperationTypes.ReadWrite>) parentAccess).operationTypes()
.create((OperationType.Blueprint) blueprint);
return null;
}
}, null);
for (SyncHash.Tree child : tree.getChildren()) {
create(tx, root, child, newStructure);
}
}
private Entity.Update updateFromBlueprint(Blueprint blueprint) {
return blueprint.accept(new ElementBlueprintVisitor.Simple<Entity.Update, Void>() {
@Override public DataEntity.Update visitData(DataEntity.Blueprint<?> data, Void parameter) {
return fillCommon(DataEntity.Update.builder(), data).withValue(data.getValue()).build();
}
@Override public Feed.Update visitFeed(Feed.Blueprint feed, Void parameter) {
return fillCommon(Feed.Update.builder(), feed).build();
}
@Override public Metric.Update visitMetric(Metric.Blueprint metric, Void parameter) {
return fillCommon(Metric.Update.builder(), metric).withInterval(metric.getCollectionInterval()).build();
}
@Override public MetricType.Update visitMetricType(MetricType.Blueprint type, Void parameter) {
return fillCommon(MetricType.Update.builder(), type).withInterval(type.getCollectionInterval()).build();
}
@Override public OperationType.Update visitOperationType(OperationType.Blueprint operationType,
Void parameter) {
return fillCommon(OperationType.Update.builder(), operationType).build();
}
@Override public Resource.Update visitResource(Resource.Blueprint resource, Void parameter) {
return fillCommon(Resource.Update.builder(), resource).build();
}
@Override public ResourceType.Update visitResourceType(ResourceType.Blueprint type, Void parameter) {
return fillCommon(ResourceType.Update.builder(), type).build();
}
private <EE extends Entity<?, UU>, UU extends Entity.Update,
Bld extends Entity.Update.Builder<EE, UU, Bld>>
Bld fillCommon(Bld bld, Entity.Blueprint bl) {
if (bl.getProperties() != null) {
bld.withProperties(bl.getProperties());
}
return bld.withName(bl.getName());
}
}, null);
}
private Map.Entry<InventoryStructure<B>, SyncHash.Tree> treeHashAndStructure(Transaction<BE> tx) {
BE root = tx.querySingle(context.discriminator(), context.select().get());
E entity = tx.convert(context.discriminator(), root, context.entityClass);
SyncHash.Tree.Builder bld = SyncHash.Tree.builder();
InventoryStructure.Builder<B> structBld = InventoryStructure.of(Inventory.asBlueprint(entity));
bld.withPath(RelativePath.empty().get()).withHash(entity.getSyncHash());
//the closure is returned in a breadth-first manner
Iterator<BE> closure = tx.getTransitiveClosureOver(context.discriminator(), root, outgoing, contains.name());
if (closure.hasNext()) {
Function<BE, Entity<? extends Entity.Blueprint, ?>> convert =
e -> (Entity<Entity.Blueprint, ?>) tx.convert(context.discriminator(), e, tx.extractType(e));
Stream<BE> st = StreamSupport.stream(Spliterators.spliteratorUnknownSize(closure, 0), false);
Iterator<Entity<? extends Entity.Blueprint, ?>> entities = st.map(convert).iterator();
buildChildTree(tx, entity.getPath(), singletonList(bld), singletonList(structBld), new ArrayList<>(),
new ArrayList<>(), entities.next(), entities);
}
return new SimpleImmutableEntry<>(structBld.build(), bld.build());
}
@SuppressWarnings("unchecked")
private void buildChildTree(Transaction<BE> tx, CanonicalPath root,
List<? extends SyncHash.Tree.AbstractBuilder<?>> possibleTreeParents,
List<? extends InventoryStructure.AbstractBuilder<?>> possibleStructParents,
List<SyncHash.Tree.ChildBuilder<?>> currentTreeRow,
List<InventoryStructure.ChildBuilder<?>> currentStructRow,
Entity<? extends Entity.Blueprint, ?> currentElement,
Iterator<Entity<? extends Entity.Blueprint, ?>> nextElements) {
Consumer<Entity<? extends Entity.Blueprint, ?>> decider = e -> {
if (!(e instanceof Syncable)) {
return;
}
CanonicalPath currentPath = e.getPath();
RelativePath relativeCurrentPath = currentPath.relativeTo(root);
SyncHash.Tree.AbstractBuilder<?> treeParent = findTreeParent(possibleTreeParents, relativeCurrentPath);
InventoryStructure.AbstractBuilder<?> structParent = findStructParent(possibleStructParents, relativeCurrentPath);
if ((treeParent == null && structParent != null) || (treeParent != null && structParent == null)) {
throw new IllegalStateException(
"Inconsistent tree hash and inventory structure builders while computing the child tree of" +
" entity " + root + ". This is a bug.");
}
if (treeParent == null) {
//ok, our parents don't have this child. Seems like we need to start a new row.
//first end our parents
possibleTreeParents.forEach(p -> {
if (p instanceof SyncHash.Tree.ChildBuilder) {
((SyncHash.Tree.ChildBuilder<?>) p).endChild();
}
});
possibleStructParents.forEach(p -> {
if (p instanceof InventoryStructure.ChildBuilder) {
((InventoryStructure.ChildBuilder<?>) p).end();
}
});
//and start processing the next row
buildChildTree(tx, root, currentTreeRow, currentStructRow, new ArrayList<>(), new ArrayList<>(), e,
nextElements);
} else {
SyncHash.Tree.ChildBuilder<?> childTreeBuilder = treeParent.startChild();
childTreeBuilder.withHash(((Syncable) e).getSyncHash());
childTreeBuilder.withPath(relativeCurrentPath);
currentTreeRow.add(childTreeBuilder);
InventoryStructure.ChildBuilder<?> childStructBuilder =
structParent.startChild(Inventory.asBlueprint(e));
currentStructRow.add(childStructBuilder);
}
};
decider.accept(currentElement);
while (nextElements.hasNext()) {
decider.accept(nextElements.next());
}
for (SyncHash.Tree.ChildBuilder<?> cb : currentTreeRow) {
cb.endChild();
}
for (InventoryStructure.ChildBuilder<?> cb : currentStructRow) {
cb.end();
}
}
private static SyncHash.Tree.AbstractBuilder<?>
findTreeParent(List<? extends SyncHash.Tree.AbstractBuilder<?>> possibleParents, RelativePath childPath) {
return possibleParents.stream()
.filter(p -> p.getPath().isParentOf(childPath) && p.getPath().getDepth() == childPath.getDepth() - 1)
.findAny()
.orElse(null);
}
private static InventoryStructure.AbstractBuilder<?>
findStructParent(List<? extends InventoryStructure.AbstractBuilder<?>> possibleParents, RelativePath childPath) {
return possibleParents.stream()
.filter(p -> p.getPath().isParentOf(childPath) && p.getPath().getDepth() == childPath.getDepth() - 1)
.findAny()
.orElse(null);
}
private static Map<InventoryStructure.EntityType, Set<SyncHash.Tree>>
splitByType(Collection<SyncHash.Tree> group) {
EnumMap<InventoryStructure.EntityType, Set<SyncHash.Tree>> ret =
new EnumMap<>(InventoryStructure.EntityType.class);
group.forEach(t -> {
SegmentType elementType = t.getPath().getSegment().getElementType();
InventoryStructure.EntityType entityType = InventoryStructure.EntityType.of(elementType);
Set<SyncHash.Tree> siblings = ret.get(entityType);
if (siblings == null) {
siblings = new HashSet<>();
ret.put(entityType, siblings);
}
siblings.add(t);
});
return ret;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private Mutator<BE, E, B, U, String> createMutator(Transaction<BE> tx) {
return (Mutator<BE, E, B, U, String>) ElementTypeVisitor.accept(
Inventory.types().byElement(context.entityClass).getSegmentType(),
new ElementTypeVisitor<Mutator<BE, ?, ?, ?, String>, Void>() {
@Override public Mutator<BE, ?, ?, ?, String> visitTenant(Void parameter) {
return new BaseTenants.ReadWrite(context.previous);
}
@Override public Mutator<BE, ?, ?, ?, String> visitEnvironment(Void parameter) {
return new BaseEnvironments.ReadWrite(context.previous);
}
@Override public Mutator<BE, ?, ?, ?, String> visitFeed(Void parameter) {
return new BaseFeeds.ReadWrite(context.previous);
}
@Override public Mutator<BE, ?, ?, ?, String> visitMetric(Void parameter) {
return new BaseMetrics.ReadWrite(context.previous);
}
@Override public Mutator<BE, ?, ?, ?, String> visitMetricType(Void parameter) {
return new BaseMetricTypes.ReadWrite(context.previous);
}
@Override public Mutator<BE, ?, ?, ?, String> visitResource(Void parameter) {
return new BaseResources.ReadWrite(context.previous);
}
@Override public Mutator<BE, ?, ?, ?, String> visitResourceType(Void parameter) {
return new BaseResourceTypes.ReadWrite(context.previous);
}
@Override public Mutator<BE, ?, ?, ?, String> visitRelationship(Void parameter) {
throw new IllegalArgumentException(
"Cannot synchronize relationships. This codepath should have never been allowed and" +
" is a bug.");
}
@Override public Mutator<BE, ?, ?, ?, String> visitData(Void parameter) {
BE parent = tx.querySingle(context.discriminator(), context.previous.sourcePath);
if (parent == null) {
throw new EntityNotFoundException(Query.filters(context.previous.sourcePath));
}
Class parentType = tx.extractType(parent);
return new BaseData.ReadWrite(context.previous, dataRoleType(parentType),
dataModificationChecks(parentType));
}
@Override public Mutator<BE, ?, ?, ?, String> visitOperationType(Void parameter) {
return new BaseOperationTypes.ReadWrite(context.previous);
}
@Override public Mutator<BE, ?, ?, ?, String> visitMetadataPack(Void parameter) {
throw new IllegalStateException(
"Cannot synchronize metadata packs. This codepath should have never been allowed and" +
" is a bug.");
}
@Override public Mutator<BE, ?, ?, ?, String> visitUnknown(Void parameter) {
throw new IllegalStateException(
"This is a bug! Unhandled type of entity for synchronization: " + context.entityClass);
}
private <E extends AbstractElement<B, U>, B extends Blueprint, U extends AbstractElement.Update>
Class<? extends DataRole> dataRoleType(Class<E> parentType) {
SegmentType st = Inventory.types().byElement(parentType).getSegmentType();
switch (st) {
case r:
return DataRole.Resource.class;
case rt:
return DataRole.ResourceType.class;
case ot:
return DataRole.OperationType.class;
default:
throw new IllegalStateException("This is a bug! Unhandled data role class" +
" for type " + context.entityClass);
}
}
private <E extends AbstractElement<B, U>, B extends Blueprint, U extends AbstractElement.Update>
BaseData.DataModificationChecks<BE> dataModificationChecks(Class<E> parentType) {
SegmentType st = Inventory.types().byElement(parentType).getSegmentType();
switch (st) {
case r:
return BaseData.DataModificationChecks.none();
case rt:
return new BaseResourceTypes.ResourceTypeDataModificationChecks<>(context.previous);
case ot:
return new BaseOperationTypes.OperationTypeDataModificationChecks<>(context.previous);
default:
throw new IllegalStateException("This is a bug! Unhandled data modification checks" +
" for type " + context.entityClass);
}
}
}, null);
}
}