blob: c247bb10aee36cdbab18676ee31e1f71bb735a27 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010, 2021 THALES GLOBAL SERVICES.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Obeo - initial API and implementation
*******************************************************************************/
package org.eclipse.sirius.diagram.sequence.ui.tool.internal.edit.validator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gef.requests.ChangeBoundsRequest;
import org.eclipse.sirius.diagram.sequence.business.internal.RangeHelper;
import org.eclipse.sirius.diagram.sequence.business.internal.elements.AbstractFrame;
import org.eclipse.sirius.diagram.sequence.business.internal.elements.CombinedFragment;
import org.eclipse.sirius.diagram.sequence.business.internal.elements.ISequenceEvent;
import org.eclipse.sirius.diagram.sequence.business.internal.elements.InteractionUse;
import org.eclipse.sirius.diagram.sequence.business.internal.elements.Lifeline;
import org.eclipse.sirius.diagram.sequence.business.internal.elements.Operand;
import org.eclipse.sirius.diagram.sequence.business.internal.elements.State;
import org.eclipse.sirius.diagram.sequence.business.internal.layout.LayoutConstants;
import org.eclipse.sirius.diagram.sequence.ui.tool.internal.util.RequestQuery;
import org.eclipse.sirius.diagram.sequence.util.Range;
import org.eclipse.sirius.ext.base.Option;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
/**
* This class is responsible to check whether a request on an interaction use should be accepted (i.e. it would produce
* a well-formed diagram). While doing the validation, it also stores all the relevant information required to actually
* perform the interaction properly.
*
* @author mporhel
*/
public abstract class AbstractInteractionFrameValidator {
private static final String FRAME_RESIZE_VALIDATOR = "org.eclipse.sirius.sequence.resize.frame.validator"; //$NON-NLS-1$
/**
* Expansion zone.
*/
protected Range expansionZone = Range.emptyRange();
/**
* Current interaction use or combined fragment.
*/
protected final AbstractFrame frame;
/**
* Final Range of the current interaction use.
*/
protected Range finalRange;
/**
* Initial Range of the current interaction use.
*/
protected Range initialRange;
/**
* The default interaction use/combined fragment height.
*/
protected int defaultFrameHeight;
/**
* Validation status.
*/
protected boolean valid = true;
/**
* Other moved elements.
*/
protected final Set<ISequenceEvent> movedElements = new HashSet<>();
/**
* Not in moved elements.
*/
protected final Predicate<ISequenceEvent> unmoved = Predicates.not(Predicates.in(movedElements));
private boolean initialized;
private final Collection<Integer> invalidPositions = new ArrayList<>();
private Predicate<Object> unMove = Predicates.instanceOf(Lifeline.class);
private Predicate<Object> invalidParents;
private Function<ISequenceEvent, Range> futureRangeFunction = new Function<ISequenceEvent, Range>() {
@Override
public Range apply(ISequenceEvent from) {
Range range = from.getVerticalRange();
if (frame.equals(from)) {
range = finalRange;
} else if (expansionZone != null && !expansionZone.isEmpty()) {
if (range.includes(expansionZone.getLowerBound())) {
range = new Range(range.getLowerBound(), range.getUpperBound() + expansionZone.width());
} else if (range.getLowerBound() >= expansionZone.getLowerBound()) {
range = range.shifted(expansionZone.width());
}
}
return range;
}
};
/**
* Allow to query the request.
*/
private final RequestQuery requestQuery;
/**
* Constructor.
*
* @param frame
* the interaction use or combined fragment which will be resized.
* @param requestQuery
* a query on the request targeting the execution.
*/
public AbstractInteractionFrameValidator(AbstractFrame frame, RequestQuery requestQuery) {
this.frame = frame;
this.requestQuery = requestQuery;
this.valid = false;
this.invalidParents = Predicates.or(Predicates.instanceOf(AbstractFrame.class), Predicates.instanceOf(State.class));
}
/**
* Return the validation status. Validate the request result in the first call only.
*
* @return the validation status.
*/
public final boolean isValid() {
if (!initialized) {
validate();
initialized = true;
}
return valid;
}
public Range getFinalRange() {
return finalRange;
}
/**
* Performs all the computations required to validate the resizing, and stores any important information which will
* be useful to actually execute the resize if it is valid, like for example avoid contact with siblings.
*/
protected void validate() {
valid = checkAndComputeRanges();
if (valid) {
Collection<Lifeline> coveredLifelines = frame.computeCoveredLifelines();
Collection<ISequenceEvent> finalParents = getFinalParentsWithAutoExpand(coveredLifelines);
Collection<ISequenceEvent> movableParents = Lists.newArrayList(Iterables.filter(finalParents, Predicates.not(unMove)));
Collection<ISequenceEvent> fixedParents = Lists.newArrayList(Iterables.filter(finalParents, unMove));
if (movableParents.isEmpty() || !movedElements.containsAll(movableParents)) {
valid = valid && Iterables.isEmpty(Iterables.filter(finalParents, invalidParents));
valid = valid && checkParentOperands(finalParents, coveredLifelines);
valid = valid && checkFinalRangeStrictlyIncludedInParents(movableParents);
valid = valid && checkLocalSiblings(movableParents, coveredLifelines);
}
valid = valid && checkFinalRangeStrictlyIncludedInParents(fixedParents);
valid = valid && checkLocalSiblings(fixedParents, coveredLifelines);
}
if (getRequestQuery().isResize()) {
valid = valid && checkGlobalPositions();
}
}
private boolean checkParentOperands(Collection<ISequenceEvent> finalParents, Collection<Lifeline> coveredLifelines) {
Iterable<Operand> finalOperandParents = Iterables.filter(finalParents, Operand.class);
// Step1 : check that parentOperands contains at most one Operand.
boolean checked;
if (Iterables.size(finalOperandParents) > 1) {
// If two or more Operands are detected, this means that after this move/resize, the current frame would
// not be fully included in one of the parents.
checked = false;
} else {
// We need to check which Operand might contained the current Frame after move/resize.
// finalOperandParents.size == 0
// No reference parent operand directly in finalParents.
// finalOperandParents.size == 1
// If one operand is found in the parents, it might have other co-parents on some lifelines (executions
// which are sub events or parent events of the moved/resized frame).
// We must ensure that the found potential parent Operand or the parent Operand of the parent executions is
// compatible with the current move/resize : it must be unique (or null) and able to contain the current
// frame (compatible coverage).
checked = true;
Operand commonParentOperand = null;
boolean operandProvided = false;
// Step 2: check that all final parents belongs to the same operand.
// if operand is not null : it must be the parent operand of all other final parent.
// if operand is null : all parent operands must have the same parent operand
for (ISequenceEvent finalParent : finalParents) {
Operand parentOperand;
if (finalParent instanceof Operand) {
parentOperand = (Operand) finalParent;
} else {
parentOperand = finalParent.getParentOperand().get();
}
if (!operandProvided) {
// Operand of the first final parent.
operandProvided = true;
commonParentOperand = parentOperand;
} else {
if (commonParentOperand != null && commonParentOperand.equals(parentOperand)) {
// Same final parent operand
checked = true;
} else if (commonParentOperand == null && parentOperand == null) {
// Common parent operand is still null
checked = true;
} else {
// Several final parent operands found (Op_1 vs Op_2 or null vs Op_1)
checked = false;
break;
}
}
}
if (checked && commonParentOperand != null) {
checked = commonParentOperand.computeCoveredLifelines().containsAll(coveredLifelines);
}
}
return checked;
}
private Collection<ISequenceEvent> getFinalParentsWithAutoExpand(Collection<Lifeline> coveredLifelines) {
Collection<ISequenceEvent> finalParentsWithAutoExpand = new ArrayList<>();
Collection<ISequenceEvent> finalParents = getFinalParents(coveredLifelines);
for (ISequenceEvent localParent : finalParents) {
// check the need of space expansion
if (localParent != null) {
if (!movedElements.contains(localParent) && !localParent.canChildOccupy(frame, finalRange, new ArrayList<ISequenceEvent>(movedElements), coveredLifelines)) {
expansionZone = computeExpansionZone();
}
finalParentsWithAutoExpand.add(localParent);
}
}
return finalParentsWithAutoExpand;
}
/**
* Computes, checks and stores the initial and final range of the interaction use if the resize is performed.
*/
private boolean checkAndComputeRanges() {
// Proper range
initialRange = frame.getVerticalRange();
Rectangle newBounds = getResizedBounds(new Rectangle(0, initialRange.getLowerBound(), 0, initialRange.width()));
if (newBounds.height < defaultFrameHeight) {
finalRange = initialRange;
return false;
}
finalRange = RangeHelper.verticalRange(newBounds);
return true;
}
/**
* Resizing an interaction use can not change which parents it is on, and can not have any impact on that parents's
* ranges, so the final range of the interaction use after the resize must be strictly included in the ranges of the
* parents.
*/
private boolean checkFinalRangeStrictlyIncludedInParents(Collection<ISequenceEvent> parentEvents) {
boolean checked = true;
Iterable<ISequenceEvent> unMovedParents = Iterables.filter(parentEvents, unmoved);
Iterator<ISequenceEvent> iterator = unMovedParents.iterator();
while (checked && iterator.hasNext()) {
ISequenceEvent parent = iterator.next();
Range parentRange = parent.getVerticalRange();
if (expansionZone != null && !expansionZone.isEmpty()) {
parentRange = new Range(parentRange.getLowerBound(), parentRange.getUpperBound() + expansionZone.width());
}
/*
* We make two tests separately so that is is easier when debugging to determine which of the conditions
* went wrong, if any.
*/
boolean interactionInRange = parentRange.includes(finalRange.grown(LayoutConstants.EXECUTION_CHILDREN_MARGIN));
checked = checked && interactionInRange;
}
return checked;
}
/**
* Get final parents event after application of the current interaction.
*
* @param coveredLifelines
* the lifeline covered by the current frame.
*
* @return final parents.
*/
protected abstract Collection<ISequenceEvent> getFinalParents(Collection<Lifeline> coveredLifelines);
private boolean checkLocalSiblings(Collection<ISequenceEvent> finalParents, Collection<Lifeline> coveredLifelines) {
boolean okForSiblings = true;
for (ISequenceEvent localParent : finalParents) {
for (ISequenceEvent localSibling : Iterables.filter(localParent.getSubEvents(), unmoved)) {
if (frame.equals(localSibling) || finalParents.contains(localSibling)) {
// Frame is moved : to not check it.
// Do not consider elements identified as final parents as siblings. By construction, their range
// will intersects with the final range of the current frame.
continue;
}
Option<Lifeline> localSiblingLifeline = localSibling.getLifeline();
if (localSiblingLifeline.some() && !coveredLifelines.contains(localSiblingLifeline.get())) {
// localSibling is not on the same lifeline, no need to check it.
continue;
}
okForSiblings = checkSibling(localSibling);
if (!okForSiblings) {
break;
}
}
if (!okForSiblings) {
break;
}
}
return okForSiblings;
}
private boolean checkSibling(ISequenceEvent sibling) {
Range siblingRange = sibling.getVerticalRange();
if (canExpand()) {
if (expansionZone != null && !expansionZone.isEmpty()) {
siblingRange = getExpandedRange(siblingRange);
} else if (siblingRange.intersects(finalRange)) {
expansionZone = computeExpansionZone();
siblingRange = getExpandedRange(siblingRange);
}
// Uncomment to avoid forbidden feedback between resize and resize +
// auto-expand
// return !siblingRange.intersects(finalRange);
}
return !siblingRange.intersects(finalRange);
}
private Range getExpandedRange(Range siblingRange) {
if (expansionZone != null && !expansionZone.isEmpty() && siblingRange.intersects(expansionZone)) {
return siblingRange.shifted(expansionZone.width());
}
return siblingRange;
}
private Rectangle getResizedBounds(Rectangle bounds) {
return getRequestQuery().getLogicalTransformedRectangle(bounds);
}
/**
* Get the expansion zone requested to validate the move.
*
* @return an expansion zone.
*/
public Range getExpansionZone() {
return canExpand() && expansionZone != null ? expansionZone : Range.emptyRange();
}
/**
* Check that the current validator handles auto-expand.
*
* @return true if the validator supports the autoexpand.
*/
protected abstract boolean canExpand();
/**
* Compute the allowed expansion zone.
*
* @return the computed expansion zone.
*/
protected abstract Range computeExpansionZone();
/**
* Other moved elements.
*
* @param otherMovedElements
* the other moved elements.
*/
public void setMovedElements(Collection<ISequenceEvent> otherMovedElements) {
if (otherMovedElements != null && !otherMovedElements.isEmpty()) {
movedElements.addAll(otherMovedElements);
}
}
/**
* Get the validator from the request extended data or a new one.
*
* @param cbr
* the current resize request.
* @param host
* the host frame
* @return a validator.
*/
public static AbstractInteractionFrameValidator getOrCreateResizeValidator(ChangeBoundsRequest cbr, AbstractFrame host) {
RequestQuery requestQuery = new RequestQuery(cbr);
Preconditions.checkArgument(requestQuery.isResize());
AbstractInteractionFrameValidator validator = null;
Object object = cbr.getExtendedData().get(FRAME_RESIZE_VALIDATOR);
if (object instanceof AbstractInteractionFrameValidator) {
validator = (AbstractInteractionFrameValidator) object;
if (!validator.getRequestQuery().getLogicalDelta().equals(requestQuery.getLogicalDelta())) {
validator = null;
}
}
if (validator == null && requestQuery.isResize()) {
if (host instanceof CombinedFragment) {
validator = new CombinedFragmentResizeValidator((CombinedFragment) host, requestQuery);
} else if (host instanceof InteractionUse) {
validator = new InteractionUseResizeValidator((InteractionUse) host, requestQuery);
}
cbr.getExtendedData().put(FRAME_RESIZE_VALIDATOR, validator);
}
return validator;
}
public Collection<Integer> getInvalidPositions() {
return invalidPositions;
}
private boolean checkGlobalPositions() {
boolean safeMove = true;
invalidPositions.addAll(new PositionsChecker(frame.getDiagram(), futureRangeFunction).getInvalidPositions());
safeMove = invalidPositions.isEmpty();
return safeMove;
}
public RequestQuery getRequestQuery() {
return requestQuery;
}
}