/*
* 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.filters.With.type;
import java.time.Instant;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import org.hawkular.inventory.api.Action;
import org.hawkular.inventory.api.Configuration;
import org.hawkular.inventory.api.Parents;
import org.hawkular.inventory.api.Query;
import org.hawkular.inventory.api.Relationships;
import org.hawkular.inventory.api.filters.Filter;
import org.hawkular.inventory.api.filters.Related;
import org.hawkular.inventory.api.filters.SwitchElementType;
import org.hawkular.inventory.api.model.AbstractElement;
import org.hawkular.inventory.api.model.Entity;
import org.hawkular.inventory.api.model.Relationship;
import org.hawkular.inventory.base.spi.Discriminator;
import org.hawkular.inventory.base.spi.InventoryBackend;
import org.hawkular.inventory.paths.Path;
import rx.subjects.Subject;
/**
* Holds the data needed throughout the construction of inventory traversal.
*
* @author Lukas Krejci
* @since 0.1.0
*/
public final class TraversalContext<BE, E extends AbstractElement<?, ?>> {
/**
* The inventory instance we're operating in.
*/
protected final BaseInventory<BE> inventory;
/**
* The query to the "point" right before the entities of interest.
*/
protected final Query sourcePath;
/**
* A query that will select the entities of interest from the {@link #sourcePath}.
*/
protected final Query selectCandidates;
/**
* The inventory backend to be used for querying and persistence.
*/
private final InventoryBackend<BE> backend;
/**
* The type of the entity currently being sought after.
*/
protected final Class<E> entityClass;
/**
* The user provided configuration.
*/
protected final Configuration configuration;
/**
* The previous context, from which this one was created. Can be null.
*/
protected final TraversalContext<BE, ?> previous;
private final ObservableContext observableContext;
private final int transactionRetries;
private final Instant now;
/**
* Optimization for quickly retrieving the entity that has been just created. We have all the data ready at the
* creation time so it seems silly to load it from backend as soon as the caller requires to see the results of the
* creation.
*/
private E createdEntity;
private final TransactionConstructor<BE> transactionConstructor;
TraversalContext(BaseInventory<BE> inventory, Instant now, Query sourcePath, Query selectCandidates,
InventoryBackend<BE> backend, Class<E> entityClass, Configuration configuration,
ObservableContext observableContext, TransactionConstructor<BE> transactionConstructor) {
this(inventory, now, sourcePath, selectCandidates, backend, entityClass, configuration, observableContext,
getTransactionRetries(configuration), null, null, transactionConstructor);
}
private TraversalContext(BaseInventory<BE> inventory, Instant now, Query sourcePath, Query selectCandidates,
InventoryBackend<BE> backend, Class<E> entityClass, Configuration configuration,
ObservableContext observableContext, int transactionRetries,
TraversalContext<BE, ?> previous, E createdEntity,
TransactionConstructor<BE> transactionConstructor) {
this.inventory = inventory;
this.now = now;
this.sourcePath = sourcePath;
this.selectCandidates = selectCandidates;
this.backend = backend;
this.entityClass = entityClass;
this.configuration = configuration;
this.observableContext = observableContext;
this.transactionRetries = transactionRetries;
this.previous = previous;
this.createdEntity = createdEntity;
this.transactionConstructor = transactionConstructor == null
? TransactionConstructor.startInBackend() : transactionConstructor;
}
private static int getTransactionRetries(Configuration configuration) {
String retries = configuration.getProperty(BaseInventory.TRANSACTION_RETRIES, "10");
return Integer.parseInt(retries);
}
/**
* @return the entity previously created on this traversal position or null if no such thing happened.
*/
public E getCreatedEntity() {
return createdEntity;
}
public void setCreatedEntity(E entity) {
createdEntity = entity;
}
/**
* If the current position in the traversal defines any select candidates, the new context will have its source path
* composed by appending the select candidates to the current source path.
*
* @return a context builder with the modified source path
*/
Builder<BE, E> proceed() {
return new Builder<>(this, hop(), Query.filter(), entityClass);
}
/**
* The new context will have the source path composed by appending current select candidates to the current source
* path and its select candidates will filter for entities related by the provided relationship to the new sources
* and will have the provided type.
*
* @param over the relationship with which the select candidates will be related to the entities on the source
* path
* @param entityType the type of the entities related to the entities on the source path
* @param <T> the type of the "target" entities
* @return a context builder with the modified source path, select candidates and type
*/
<T extends Entity<?, ?>> Builder<BE, T> proceedTo(Relationships.WellKnown over, Class<T> entityType) {
return new Builder<>(this, hop(), Query.filter(), entityType).hop(Related.by(over), type(entityType));
}
/**
* Virtually identical to {@link #proceedTo(Relationships.WellKnown, Class)} only follows the relationship in the
* opposite direction.
*
* @param over the relationship to retreat over (i.e. if the current position is the target of the
* relationship, the sought after entity type needs to be the source of the relationship).
* @param entityType the type of the entities to retreat to
* @param <T> the type of the "target" entities
* @return a context builder with the modified source path, select candidates and type
*/
<T extends Entity<?, ?>> Builder<BE, T> retreatTo(Relationships.WellKnown over, Class<T> entityType) {
return new Builder<>(this, hop(), Query.filter(), entityType).hop(Related.asTargetBy(over), type(entityType));
}
/**
* Proceeds the traversal to the next entities taking into account what parents the next entities should have.
*
* @param nextEntityType the type of the entities the new context will target
* @param parentsType the type of the enum containing the possible parent types
* @param currentParent the parent type representing the current position in the traversal
* @param parents the parents to proceed to target entities over
* @param hopBuilder the producer function that will convert the parent into a filter path to apply to the
* traversal
* @param <T> the type of the next entity
* @param <P> the type of the enum of the possible parents of the target entity
* @return a new traversal context
*/
<T extends Entity<?, ?>, P extends Enum<P> & Parents>
TraversalContext<BE, T> proceedWithParents(Class<T> nextEntityType, Class<P> parentsType, P currentParent,
P[] parents,
BiConsumer<P, Query.SymmetricExtender> hopBuilder) {
EnumSet<P> ps = ParentsUtil.convert(parentsType, currentParent, parents);
TraversalContext.Builder<BE, E> bld = proceed();
for (P p : ps) {
Query.Builder qb = bld.rawQueryBuilder();
qb = qb.branch();
Query.SymmetricExtender extender = qb.symmetricExtender();
hopBuilder.accept(p, extender);
qb.done();
}
return bld.getting(nextEntityType);
}
/**
* The new context will have the source path composed by appending current select candidates to the current source
* path. The new context will have select candidates such that it will select the relationships in given direction
* stemming from the entities on the new source path.
*
* @param direction the direction of the relationships to look for
* @return a context builder with the modified source path, select candidates and type
*/
Builder<BE, Relationship> proceedToRelationships(Relationships.Direction direction) {
return new Builder<>(this, hop(), Query.filter(), Relationship.class)
.hop(new SwitchElementType(direction, false));
}
/**
* An opposite of {@link #proceedToRelationships(Relationships.Direction)}.
*
* @param direction the direction in which to "leave" the relationships
* @param entityType the type of entities to "hop to"
* @param <T> the type of entities to "hop to"
* @return a context builder with the modified source path, select candidates and type
*/
<T extends Entity<?, ?>> Builder<BE, T> proceedFromRelationshipsTo(Relationships.Direction direction,
Class<T> entityType) {
return new Builder<>(this, hop(), Query.filter(), entityType)
.hop(new SwitchElementType(direction, true)).where(type(entityType));
}
/**
* @return a new query selecting the select candidates from the source path. The resulting extender
* is set up to append filter fragments.
*/
Query.SymmetricExtender select() {
return sourcePath.extend().filter().withExact(selectCandidates);
}
/**
* @return appends the select candidates to the source path. The only difference between this and {@link #select()}
* is that this method returns the extender set up to append path fragments.
*/
Query.SymmetricExtender hop() {
return sourcePath.extend().path().withExact(selectCandidates).path();
}
/**
* Constructs a new traversal context with given time as its "now".
* @param time the time of now
* @return a new traversal context
*/
TraversalContext<BE, E> at(Instant time) {
return new TraversalContext<>(inventory, time, sourcePath, Query.empty(), backend, entityClass, configuration,
observableContext, transactionRetries, this, null, transactionConstructor);
}
Discriminator discriminator() {
return now == null ? Discriminator.latest() : Discriminator.time(now);
}
/**
* Returns the point in time to operate at or null if no such time was set up.
*
* @return a "now"
*/
@Nullable Instant declaredNow() {
return now;
}
/**
* Constructs a new traversal context by replacing the source path with the provided query and clearing out the
* selected candidates.
*
* @param path the source path of the new context
* @return a new traversal context with the provided source path and empty select candidates, but otherwise
* identical to this one.
*/
TraversalContext<BE, E> replacePath(Query path) {
return new TraversalContext<>(inventory, now, path, Query.empty(), backend, entityClass, configuration,
observableContext, transactionRetries, this, null, transactionConstructor);
}
TraversalContext<BE, E> toCreatedEntity(E entity, boolean cache) {
return new TraversalContext<>(inventory, now, Query.to(entity.getPath()), Query.empty(), backend, entityClass,
configuration, observableContext, transactionRetries, this, cache ? entity : null, null);
}
TraversalContext<BE, E> proceedTo(Path path) {
if (!AbstractElement.segmentTypeFromType(entityClass).equals(path.getSegment().getElementType())) {
throw new IllegalArgumentException("Path doesn't point to the type of element currently being accessed.");
}
return replacePath(Util.extendTo(this, path));
}
/**
* Sends out the notification to the subscribers.
*
* @param entity the entity on which the action took place
* @param action the action (for which the entity and context resolve to the same type)
* @param <V> the type of the entity and at the same time the type of the action context
* @see #notify(Object, Object, Action)
*/
<V> void notify(V entity, Action<V, V> action) {
notify(entity, entity, action);
}
/**
* Sends out the notification to the subscribers.
*
* @param entity the entity on which the action occured
* @param actionContext the description of the action
* @param action the actual action
* @param <C> the type of the action description (aka context)
* @param <V> the type of the entity on which the action occurred
*/
<C, V> void notify(V entity, C actionContext, Action<C, V> action) {
Iterator<Subject<C, C>> subjects = observableContext.matchingSubjects(action, entity);
while (subjects.hasNext()) {
Subject<C, C> s = subjects.next();
s.onNext(actionContext);
}
}
public int getTransactionRetriesCount() {
return transactionRetries;
}
public TransactionConstructor<BE> getTransactionConstructor() {
return transactionConstructor;
}
/**
* Sends out all the pending notifications in the supplied object.
*
* @param entityAndNotifications the list of pending notifications
*/
void notifyAll(EntityAndPendingNotifications<BE, ?> entityAndNotifications) {
entityAndNotifications.getNotifications().forEach(this::notify);
}
/**
* Another way of sending out a notification.
*
* @param notification the notification to send out
* @param <C> the type of the action description (aka context)
* @param <V> the type of the entity on which the action occurred
*/
<C, V> void notify(Notification<C, V> notification) {
notify(notification.getValue(), notification.getActionContext(), notification.getAction());
}
Transaction<BE> startTransaction() {
return startTransaction(new BasePreCommit<>());
}
Transaction<BE> startTransaction(Transaction.PreCommit<BE> preCommit) {
Transaction<BE> tx = transactionConstructor.construct(backend, preCommit);
tx.getPreCommit().initialize(inventory.keepTransaction(tx), tx);
return tx;
}
/**
* Builds a new traversal context.
*
* @param <BE> the type of the backend elements
* @param <E> the type of the inventory element the new context will represent
*/
public static final class Builder<BE, E extends AbstractElement<?, ?>> {
private final TraversalContext<BE, ?> sourceContext;
private final Query.SymmetricExtender pathExtender;
private final Query.SymmetricExtender selectExtender;
private final Class<E> entityClass;
public Builder(TraversalContext<BE, ?> sourceContext, Query.SymmetricExtender pathExtender,
Query.SymmetricExtender selectExtender, Class<E> entityClass) {
this.sourceContext = sourceContext;
this.pathExtender = pathExtender;
this.selectExtender = selectExtender;
this.entityClass = entityClass;
}
/**
* Appends the sets of filters in succession to the select candidates.
*
* @param filters the sets of filters to apply
* @return this builder
* @see #where(Filter[][])
* @see #where(Filter...)
*/
public Builder<BE, E> whereAll(Filter[][] filters) {
if (filters.length == 1) {
return where(filters[0]);
} else {
for (Filter[] fs : filters) {
hop().where(fs);
}
return this;
}
}
/**
* Create query branches in the select candidates with each of the provided sets of filters.
*
* @param filters the sets of filters, each representing a new branch in the query
* @return this builder
*/
public Builder<BE, E> where(Filter[][] filters) {
selectExtender.filter().with(filters);
return this;
}
/**
* Appends the provided set of filters to the current select candidates.
*
* @param filters the set of filters to append
* @return this builder
*/
public Builder<BE, E> where(Filter... filters) {
selectExtender.filter().with(filters);
return this;
}
/**
* Create query branches in the select candidates with each of the provided sets of filters.
* The filters are applied as path fragments.
*
* @param filters the sets of the filters to append as path fragments
* @return this builder
*/
public Builder<BE, E> hop(Filter[][] filters) {
selectExtender.path().with(filters);
return this;
}
/**
* Appends the provided set of filters to the current select candidates.
* The filters are applied as path fragments.
*
* @param filters the set of filters to append as path fragments
* @return this builder
*/
public Builder<BE, E> hop(Filter... filters) {
selectExtender.path().with(filters);
return this;
}
public Query.Builder rawQueryBuilder() {
return selectExtender.rawQueryBuilder();
}
/**
* @return a new traversal context set up using this builder
*/
TraversalContext<BE, E> get() {
return new TraversalContext<>(sourceContext.inventory, sourceContext.now, pathExtender.get(),
selectExtender.get(), sourceContext.backend, entityClass, sourceContext.configuration,
sourceContext.observableContext, sourceContext.transactionRetries, sourceContext, null,
sourceContext.transactionConstructor);
}
/**
* Changes the entity type of the to-be-returned traversal context.
*
* @param entityType the type of entities to be returned by traversals using the new context
* @param <T> the type
* @return a new traversal context set up using this builder and querying for entities of the provided type
*/
<T extends AbstractElement<?, ?>> TraversalContext<BE, T> getting(Class<T> entityType) {
return new TraversalContext<>(sourceContext.inventory, sourceContext.now, pathExtender.get(),
selectExtender.get(), sourceContext.backend, entityType, sourceContext.configuration,
sourceContext.observableContext, sourceContext.transactionRetries, sourceContext, null, null);
}
}
}