/*
* 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 org.hawkular.inventory.api.Action.created;
import static org.hawkular.inventory.api.Relationships.Direction.incoming;
import static org.hawkular.inventory.api.Relationships.Direction.outgoing;
import static org.hawkular.inventory.api.Relationships.WellKnown.contains;
import static org.hawkular.inventory.api.filters.With.id;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.hawkular.inventory.api.Action;
import org.hawkular.inventory.api.EntityAlreadyExistsException;
import org.hawkular.inventory.api.EntityNotFoundException;
import org.hawkular.inventory.api.Query;
import org.hawkular.inventory.api.Relationships;
import org.hawkular.inventory.api.model.AbstractElement;
import org.hawkular.inventory.api.model.Blueprint;
import org.hawkular.inventory.api.model.Entity;
import org.hawkular.inventory.api.model.Relationship;
import org.hawkular.inventory.api.model.Tenant;
import org.hawkular.inventory.api.paging.Page;
import org.hawkular.inventory.api.paging.Pager;
import org.hawkular.inventory.base.spi.Discriminator;
import org.hawkular.inventory.base.spi.ElementNotFoundException;
import org.hawkular.inventory.paths.CanonicalPath;
import org.hawkular.inventory.paths.ElementTypeVisitor;
import org.hawkular.inventory.paths.SegmentType;
/**
* @author Lukas Krejci
* @since 0.1.0
*/
abstract class Mutator<BE, E extends Entity<?, U>, B extends Blueprint, U extends Entity.Update, Id>
extends Traversal<BE, E> {
protected Mutator(TraversalContext<BE, E> context) {
super(context);
}
/**
* Extracts the proposed ID from the blueprint or identifies the ID through some other means.
*
*
* @param tx the transaction this method is called within
* @param blueprint the blueprint of the entity to be created
* @return the ID to be used for the new entity
*/
protected abstract String getProposedId(Transaction<BE> tx, B blueprint);
/**
* A helper method to be used in the implementation of the
* {@link org.hawkular.inventory.api.WriteInterface#create(Blueprint, boolean)} method.
*
* <p>The callers may merely use the returned query and construct a new {@code *Single} instance using it.
*
* @param blueprint the blueprint of the new entity
* @return the created entity
*/
protected final E doCreate(B blueprint) {
ResultWithNofifications<E, BE> result = inTxWithNotifications(tx -> doCreate(blueprint, tx).getEntity());
E entity = result.getResult();
//now try to see if the notifications emitted contain the notification about the entity creation - this will
//be true if the transaction was really committed above, but will not be true if we are being run inside a
//transaction frame.
for (EntityAndPendingNotifications<BE, ?> ns : result.getSentNotifications()) {
if (!ns.getEntity().getPath().equals(entity.getPath())) {
continue;
}
Optional<?> createdNotification = ns.getNotifications().stream()
.filter(n -> n.getAction().asEnum() == Action.Enumerated.CREATED)
.findAny();
if (createdNotification.isPresent()) {
//ok, the entity that has been notified about will have complete info. The entity returned from the
//transaction might not have.
//In particular, the entity returned from the transaction doesn't have an identity hash assigned because
//identity hash is computed only in the pre-commit phase.
entity = (E) ns.getEntity();
break;
}
}
return entity;
}
/**
* Creates the entity specified by the provided blueprint using the provided transaction.
*
* <p>Note that all the notifications and actions ARE fed into the transaction by this method so there's no need
* to that once this method returns. The returned object can be used to obtain either the created entity or its
* backend representation though.
*
* @param blueprint the blueprint of the entity to create
* @param tx the transaction in which to operate
* @return the entity object and its backend representation. Ignore the notifications, they've been handled.
*/
EntityAndPendingNotifications<BE, E> doCreate(B blueprint, Transaction<BE> tx) {
String id = getProposedId(tx, blueprint);
Discriminator now = context.discriminator();
if (!tx.isUniqueIndexSupported()) {
//poor man's way of ensuring uniqueness of CPs
Query existenceCheck = context.hop().filter().with(id(id)).get();
Page<BE> results = tx.query(now, existenceCheck, Pager.single());
if (results.hasNext()) {
throw new EntityAlreadyExistsException(id, Query.filters(existenceCheck));
}
}
preCreate(blueprint, tx);
BE parent = getParent(tx);
CanonicalPath parentCanonicalPath = parent == null ? null : tx.extractCanonicalPath(parent);
EntityAndPendingNotifications<BE, E> newEntity;
BE containsRel = null;
CanonicalPath entityPath;
if (parent == null) {
if (context.entityClass == Tenant.class) {
entityPath = CanonicalPath.of().tenant(id).get();
} else {
throw new IllegalStateException("Could not find the parent of the entity to be created," +
"yet the entity is not a tenant: " + blueprint);
}
} else {
entityPath = parentCanonicalPath.extend(AbstractElement.segmentTypeFromType(context.entityClass), id)
.get();
}
BE entityObject = tx.persist(now, entityPath, blueprint);
if (parentCanonicalPath != null) {
//no need to check for contains rules - we're connecting a newly created entity
containsRel = tx.relate(now, parent, entityObject, contains.name(), Collections.emptyMap());
Relationship rel = tx.convert(now, containsRel, Relationship.class);
tx.getPreCommit().addNotifications(
new EntityAndPendingNotifications<>(containsRel, rel, new Notification<>(rel, rel, created())));
}
newEntity = wireUpNewEntity(now, entityObject, blueprint, parentCanonicalPath, parent, tx);
if (blueprint instanceof Entity.Blueprint) {
Entity.Blueprint b = (Entity.Blueprint) blueprint;
createCustomRelationships(now, entityObject, outgoing, b.getOutgoingRelationships(), tx);
createCustomRelationships(now, entityObject, incoming, b.getIncomingRelationships(), tx);
}
postCreate(entityObject, newEntity.getEntity(), tx);
List<Notification<?, ?>> notifs = new ArrayList<>(newEntity.getNotifications());
notifs.add(new Notification<>(newEntity.getEntity(), newEntity.getEntity(), Action.created()));
EntityAndPendingNotifications<BE, E> pending =
new EntityAndPendingNotifications<>(newEntity.getEntityRepresentation(), newEntity.getEntity(),
notifs);
tx.getPreCommit().addNotifications(pending);
return newEntity;
}
public final void update(Id id, U update) throws EntityNotFoundException {
inTx(tx -> {
Query q = id == null ? context.select().get() : context.select().with(id(id.toString())).get();
Util.update(context.discriminator(), context.entityClass, tx, q, update, (e, u, t) -> preUpdate(id, e, u, t), this::postUpdate
);
return null;
});
}
public final void delete(Id id) throws EntityNotFoundException {
inTx(tx -> {
Query q = id == null ? context.select().get() : context.select().with(id(id.toString())).get();
Util.delete(context.discriminator(), context.entityClass, tx, q, (e, t) -> preDelete(id, e, t),
this::postDelete, false);
return null;
});
}
public final void eradicate(Id id) throws EntityNotFoundException {
inTx(tx -> {
Query q = id == null ? context.select().get() : context.select().with(id(id.toString())).get();
Util.delete(context.discriminator(), context.entityClass, tx, q, (e, t) -> preDelete(id, e, t),
this::postDelete, true);
return null;
});
}
protected void preCreate(B blueprint, Transaction<BE> transaction) {
}
protected void postCreate(BE entityObject, E entity, Transaction<BE> transaction) {
}
/**
* A hook that can run additional clean up logic inside the delete transaction.
*
* <p>This hook is called prior to anything being deleted.
*
* <p>By default this does nothing.
* @param id the id of the entity being deleted
* @param entityRepresentation the backend specific representation of the entity
* @param transaction the transaction in which the delete is executing
*/
protected void preDelete(Id id, BE entityRepresentation, Transaction<BE> transaction) {
}
protected void postDelete(BE entityRepresentation, Transaction<BE> transaction) {
}
/**
* A hook that can run additional logic inside the update transaction before anything has been persisted to the
* backend database.
*
* <p>By default, this does nothing
* @param id the id of the entity being updated
* @param entityRepresentation the backend representation of the updated entity
* @param update the update object
* @param transaction the transaction in which the update is executing
*/
protected void preUpdate(Id id, BE entityRepresentation, U update, Transaction<BE> transaction) {
}
protected void postUpdate(BE entityRepresentation, Transaction<BE> transaction) {
}
protected BE getParent(Transaction<BE> tx) {
return ElementTypeVisitor.accept(AbstractElement.segmentTypeFromType(context.entityClass),
new ElementTypeVisitor.Simple<BE, Void>() {
@SuppressWarnings("unchecked")
@Override
protected BE defaultAction(SegmentType elementType, Void parameter) {
BE res = tx.querySingle(context.discriminator(), context.sourcePath);
if (res == null) {
throw new EntityNotFoundException(context.previous.entityClass,
Query.filters(context.sourcePath));
}
return res;
}
@Override
public BE visitTenant(Void parameter) {
return null;
}
}, null);
}
/**
* Wires up the freshly created entity in the appropriate places in inventory. The "contains" relationship between
* the parent and the new entity will already have been created so the implementations don't need to do that again.
*
* <p>The wiring up might result in new relationships being created or other "notifiable" actions - the returned
* object needs to reflect that so that the notification can correctly be emitted.
*
*
* @param discriminator the discriminator specifying the time at which the entity was created
* @param entity the freshly created, uninitialized entity
* @param blueprint the blueprint that it prescribes how the entity should be initialized
* @param parentPath the path to the parent entity
* @param parent the actual parent entity
* @param transaction the transaction this is being executed in
* @return an object with the initialized and converted entity together with any pending notifications to be sent
* out
*/
protected abstract EntityAndPendingNotifications<BE, E>
wireUpNewEntity(Discriminator discriminator, BE entity, B blueprint, CanonicalPath parentPath, BE parent,
Transaction<BE> transaction);
private void createCustomRelationships(Discriminator discriminator, BE entity, Relationships.Direction direction,
Map<String, Set<CanonicalPath>> otherEnds,
Transaction<BE> tx) {
otherEnds.forEach((name, ends) -> ends.forEach((end) -> {
try {
BE endObject = tx.find(discriminator, end);
BE from = direction == outgoing ? entity : endObject;
BE to = direction == outgoing ? endObject : entity;
EntityAndPendingNotifications<BE, Relationship> res = Util.createAssociation(discriminator,
tx, from, name, to, null);
tx.getPreCommit().addNotifications(res);
} catch (ElementNotFoundException e) {
throw new EntityNotFoundException(Query.filters(Query.to(end)));
}
}));
}
}