/*
* 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.Relationships.Direction.both;
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.Relationships.WellKnown.defines;
import static org.hawkular.inventory.api.Relationships.WellKnown.hasData;
import static org.hawkular.inventory.api.Relationships.WellKnown.incorporates;
import static org.hawkular.inventory.api.Relationships.WellKnown.isParentOf;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.hawkular.inventory.api.Relationships;
import org.hawkular.inventory.api.model.Environment;
import org.hawkular.inventory.api.model.Feed;
import org.hawkular.inventory.api.model.MetadataPack;
import org.hawkular.inventory.base.spi.Discriminator;
/**
* Some well-known relationships have certain semantic rules that need to be checked for when creating/deleting them.
*
* <p>This class concentrates those checks so that they can be called easily from the various places in the codebase
* that work with relationships.
*
* @author Lukas Krejci
* @since 0.2.0
*/
public final class RelationshipRules {
private static final Map<Relationships.WellKnown, List<RuleCheck<?>>> CREATE_RULES;
private static final Map<Relationships.WellKnown, List<RuleCheck<?>>> DELETE_RULES;
private static final List<RuleCheck<?>> GLOBAL_CREATE_RULES;
static {
GLOBAL_CREATE_RULES = new ArrayList<>();
CREATE_RULES = new HashMap<>();
DELETE_RULES = new HashMap<>();
GLOBAL_CREATE_RULES.add(RelationshipRules::disallowCreateAcrossTenants);
CREATE_RULES.put(contains, Arrays.asList(RelationshipRules::checkDiamonds, RelationshipRules::checkLoops));
CREATE_RULES.put(isParentOf, Collections.singletonList(RelationshipRules::checkLoops));
CREATE_RULES.put(hasData, Collections.singletonList(RelationshipRules::disallowCreate));
CREATE_RULES.put(defines, Collections.singletonList(RelationshipRules::disallowCreate));
CREATE_RULES.put(incorporates,
Arrays.asList(RelationshipRules::disallowCreateOfIfFeedAlreadyIncorporatedInAnotherEnvironment,
RelationshipRules::disallowWhenMetadataPackIsSource));
DELETE_RULES.put(contains, Collections.singletonList(RelationshipRules::disallowDelete));
DELETE_RULES.put(defines, Collections.singletonList(RelationshipRules::disallowDelete));
DELETE_RULES.put(isParentOf, Collections.singletonList(
(i, b, o, d, r, t) -> disallowDeleteWhenTheresContainsToo(i, b, o, d, r, t, "This would mean that a" +
" sub-resource would no longer be considered a child of the parent resource, which doesn't " +
" make sense.")));
DELETE_RULES.put(incorporates, Collections.singletonList(
(i, b, o, d, r, t) -> disallowDeleteWhenTheresContainsToo(i, b, o, d, r, t, "When an entity is contained" +
" within another, it implies it is also incorporated. It would be illegal to delete only the" +
" 'incorporates' relationship.")));
DELETE_RULES.put(hasData, Collections.singletonList(RelationshipRules::disallowDelete));
DELETE_RULES.put(incorporates, Collections.singletonList(RelationshipRules::disallowWhenMetadataPackIsSource));
}
private RelationshipRules() {
}
public static <E> void checkCreate(Discriminator discriminator, Transaction<E> backend, E origin,
Relationships.Direction direction,
String relationship, E target) {
check(discriminator, backend, origin, direction, relationship, target, CheckType.CREATE);
}
public static <E> void checkDelete(Discriminator discriminator, Transaction<E> backend, E origin,
Relationships.Direction direction,
String relationship, E target) {
check(discriminator, backend, origin, direction, relationship, target, CheckType.DELETE);
}
@SuppressWarnings("unchecked")
private static <E> void check(Discriminator discriminator, Transaction<E> backend, E origin,
Relationships.Direction direction,
String relationship, E target, CheckType checkType) {
List<RuleCheck<?>> rules = checkType.getRuleChecks(relationship);
rules.forEach((r) -> ((RuleCheck<E>) r).check(discriminator, backend, origin, direction, relationship, target));
}
private static <E> void checkDiamonds(Discriminator discriminator, Transaction<E> backend, E origin,
Relationships.Direction direction,
String relationship, E target) {
if (direction == outgoing && backend.hasRelationship(discriminator, target, Relationships.Direction.incoming,
relationship)) {
throw new IllegalArgumentException("The target is already connected with another entity using the" +
" relationship: '" + relationship + "'. It is illegal for such relationships to form" +
" diamonds.");
} else if (direction == Relationships.Direction.incoming) {
if (backend.hasRelationship(discriminator, origin, Relationships.Direction.incoming, relationship)) {
throw new IllegalArgumentException("The source is already connected with another entity using the" +
" relationship: '" + relationship + "'. It is illegal for such relationships to form" +
" diamonds.");
}
}
}
private static <E> void checkLoops(Discriminator discriminator, Transaction<E> backend, E origin, Relationships.Direction direction,
String relationship, E target) {
if (direction == Relationships.Direction.both) {
throw new IllegalArgumentException("Relationship '" + relationship + "' cannot form a loop" +
" between 2 entities.");
}
if (origin.equals(target)) {
throw new IllegalArgumentException("Relationship '" + relationship + "' cannot both start and end" +
" on the same entity.");
}
if (direction == Relationships.Direction.incoming) {
Iterator<E> closure = backend.getTransitiveClosureOver(discriminator, origin, outgoing, relationship);
while (closure.hasNext()) {
E e = closure.next();
if (e.equals(target)) {
throw new IllegalArgumentException("The target and the source (indirectly) form a loop while" +
" traversing over '" + relationship + "' relationships. This is illegal for that" +
" relationship.");
}
}
} else if (direction == outgoing) {
Iterator<E> closure = backend.getTransitiveClosureOver(discriminator, origin, incoming, relationship);
while (closure.hasNext()) {
E e = closure.next();
if (e.equals(target)) {
throw new IllegalArgumentException("The source and the target (indirectly) form a loop while" +
" traversing over '" + relationship + "' relationships. This is illegal for that" +
" relationship.");
}
}
}
}
private static <E> void disallowDelete(Discriminator discriminator, Transaction<E> backend, E origin, Relationships.Direction direction,
String relationship, E target) {
throw new IllegalArgumentException("Relationship '" + relationship + "' cannot be explicitly deleted.");
}
private static <E> void disallowCreate(Discriminator discriminator, Transaction<E> backend, E origin, Relationships.Direction direction,
String relationship, E target) {
throw new IllegalArgumentException("Relationship '" + relationship + "' cannot be explicitly created.");
}
private static <E> void disallowWhenMetadataPackIsSource(Discriminator discriminator, Transaction<E> backend, E origin,
Relationships.Direction direction, String relationship,
E target) {
String message = "Manual manipulation of the 'incorporates' relationships where the" +
" MetadataPack is the source is disallowed.";
if (direction == both) {
throw new IllegalArgumentException(message);
}
Class<?> type = backend.extractType(direction == outgoing ? origin : target);
if (MetadataPack.class.equals(type)) {
throw new IllegalArgumentException(message);
}
}
private static <E> void disallowDeleteWhenTheresContainsToo(Discriminator discriminator, Transaction<E> backend, E origin,
Relationships.Direction direction, String relationship,
E target, String errorDetails) {
if (backend.hasRelationship(discriminator, origin, target, contains.name())) {
throw new IllegalArgumentException("'" + relationship + "' relationship cannot be deleted if there is" +
" also a '" + contains + "' relationship between the same two entities. " + errorDetails);
}
}
private static <E>
void disallowCreateOfIfFeedAlreadyIncorporatedInAnotherEnvironment(Discriminator discriminator, Transaction<E> backend,
E origin, Relationships.Direction direction,
String relationship, E target) {
if (!incorporates.name().equals(relationship)) {
return;
}
Class<?> originType = backend.extractType(origin);
Class<?> targetType = backend.extractType(target);
if (Environment.class.equals(originType) && Feed.class.equals(targetType) && backend.hasRelationship(
discriminator, target,
incoming, relationship)) {
throw new IllegalArgumentException("Relationship '" + relationship + "' between "
+ originType.getSimpleName() + " and " + targetType.getSimpleName() + " is 1:N." +
" The target entity - " + backend.extractCanonicalPath(target) + " - is already a target of" +
" another relationship of this name. Creating another would be illegal.");
}
}
private static <E> void disallowCreateAcrossTenants(Discriminator discriminator, Transaction<E> backend, E origin,
Relationships.Direction direction, String relationship,
E target) {
}
@FunctionalInterface
private interface RuleCheck<E> {
void check(Discriminator discriminator, Transaction<E> backend, E origin, Relationships.Direction direction,
String relationship, E target);
}
private enum CheckType {
CREATE {
@Override
public List<RuleCheck<?>> getRuleChecks(String relationship) {
List<RuleCheck<?>> ret = new ArrayList<>(GLOBAL_CREATE_RULES);
Relationships.WellKnown r = getWellKnown(relationship);
if (r != null) {
List<RuleCheck<?>> additional = CREATE_RULES.get(r);
if (additional != null) {
ret.addAll(additional);
}
}
return ret;
}
}, DELETE {
@Override
public List<RuleCheck<?>> getRuleChecks(String relationship) {
Relationships.WellKnown r = getWellKnown(relationship);
if (r != null) {
List<RuleCheck<?>> checks = DELETE_RULES.get(r);
if (checks != null) {
return checks;
}
}
return Collections.emptyList();
}
};
public abstract List<RuleCheck<?>> getRuleChecks(String relationship);
private static Relationships.WellKnown getWellKnown(String relationship) {
for (Relationships.WellKnown r : Relationships.WellKnown.values()) {
if (r.name().equals(relationship)) {
return r;
}
}
return null;
}
}
}