/**
* Copyright (C) 2013-2014 Olaf Lessenich
* Copyright (C) 2014-2015 University of Passau, Germany
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*
* Contributors:
* Olaf Lessenich <lessenic@fim.uni-passau.de>
* Georg Seibt <seibt@fim.uni-passau.de>
*/
package de.fosd.jdime.artifact;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.fosd.jdime.config.merge.MergeContext;
import de.fosd.jdime.config.merge.Revision;
import de.fosd.jdime.matcher.matching.Matching;
import de.fosd.jdime.operations.MergeOperation;
import de.fosd.jdime.stats.StatisticsInterface;
import de.fosd.jdime.strdump.DumpMode;
/**
* A generic <code>Artifact</code> that has a tree structure.
*
* @author Olaf Lessenich
*
* @param <T>
* type of artifact
*/
public abstract class Artifact<T extends Artifact<T>> implements Comparable<T>, StatisticsInterface {
private static final Logger LOG = Logger.getLogger(Artifact.class.getCanonicalName());
/**
* Children of the artifact.
*/
protected ArtifactList<T> children = null;
/**
* Left side of a conflict.
*/
protected T left = null;
/**
* Right side of a conflict.
*/
protected T right = null;
/**
* Whether this artifact represents a conflict.
*/
private boolean conflict = false;
/**
* Whether this artifact represents a choice node.
*/
private boolean choice = false;
/**
* If the artifact is a choice node, it has variants (values of map) that are present under conditions (keys of map)
*/
protected HashMap<String, T> variants;
/**
* Map to store matches.
*/
protected Map<Revision, Matching<T>> matches;
/**
* Whether the artifact has been already merged.
*/
private boolean merged;
/**
* Parent artifact.
*/
private T parent;
/**
* Revision the artifact belongs to.
*/
private Revision revision;
/**
* Number used to identify the artifact.
*/
private int number;
/**
* Constructs a new <code>Artifact</code>.
*
* @param rev
* the <code>Revision</code> for the <code>Artifact</code>
* @param number
* the DFS index of the <code>Artifact</code> in the <code>Artifact</code> tree it is a part of
*/
protected Artifact(Revision rev, int number) {
this.matches = new LinkedHashMap<>();
this.revision = rev;
this.number = number;
}
/**
* Adds a child.
*
* @param child
* child to add
* @return added child
*/
public abstract T addChild(T child);
/**
* Adds a matching.
*
* @param matching
* matching to be added
*/
public void addMatching(Matching<T> matching) {
matches.put(matching.getMatchingArtifact(this).getRevision(), matching);
}
/**
* Clones matches from another artifact.
*
* @param other
* artifact to clone matches from
*/
@SuppressWarnings("unchecked")
public void cloneMatches(T other) {
matches = new LinkedHashMap<>();
for (Map.Entry<Revision, Matching<T>> entry : other.matches.entrySet()) {
Matching<T> m = entry.getValue().clone();
m.updateMatching((T) this);
matches.put(entry.getKey(), m);
}
}
public abstract T clone();
/**
* Returns an <code>Artifact</code> that represents a merge conflict.
* A conflict contains two alternative <code>Artifact</code> (left and right) and is handled in a special way
* while pretty-printed.
*
* @param left
* left alternative <code>Artifact</code>
* @param right
* right alternative <code>Artifact</code>
* @return conflict <code>Artifact</code>
*/
public abstract T createConflictArtifact(T left, T right);
/**
* Returns a choice artifact.
*
* @param condition presence condition
* @param artifact conditional artifact
* @return choice artifact
*/
public abstract T createChoiceArtifact(String condition, T artifact);
/**
* Returns an empty <code>Artifact</code>. This is used while performing two-way merges where the
* base <code>Artifact</code> is empty.
*
* @param revision
* the <code>Revision</code> for the artifact
* @return an empty artifact
*/
public abstract T createEmptyArtifact(Revision revision);
/**
* Pretty-prints the <code>Artifact</code> to source code.
*
* @return Pretty-printed AST (source code)
*/
public abstract String prettyPrint();
/**
* Dumps this <code>Artifact</code> to a <code>String</code> using the given <code>DumpMode</code>. Uses the
* {@link Artifact#toString()} method for producing labels for nodes.
*
* @param mode
* the <code>DumpMode</code> to use
* @return the dump result
*/
public String dump(DumpMode mode) {
return dump(mode, Artifact::toString);
}
/**
* Dumps this <code>Artifact</code> to a <code>String</code> using the given <code>DumpMode</code>.
*
* @param mode
* the <code>DumpMode</code> to use
* @param getLabel
* the <code>Function</code> for producing labels for nodes
* @return the dump result
*/
public String dump(DumpMode mode, Function<Artifact<T>, String> getLabel) {
return mode.getDumper().dump(this, getLabel);
}
/**
* Returns true if this artifact physically exists.
*
* @return true if the artifact exists.
*/
public abstract boolean exists();
/**
* Return child <code>Artifact</code> at position i.
*
* @param i
* position of child <code>Artifact</code>
* @return child <code>Artifact</code> at position i
*/
public T getChild(int i) {
assert (children != null);
return children.get(i);
}
/**
* Returns all children of the <code>Artifact</code>.
*
* @return the children of the <code>Artifact</code>
*/
public ArtifactList<T> getChildren() {
if (isLeaf()) {
return new ArtifactList<>();
}
return children;
}
public abstract void deleteChildren();
/**
* Returns the identifier of the <code>Artifact</code>,
* which contains the <code>Revision</code> name and a number.
*
* This method is basically useful for debugging JDime.
*
* @return identifier of the <code>Artifact</code>
*/
public abstract String getId();
/**
* Returns the <code>Matching</code> for a specific <code>Revision</code> or <code>null</code> if there is no such
* <code>Matching</code>.
*
* @param rev
* <code>Revision</code>
* @return <code>Matching</code> with <code>Revision</code>
*/
public Matching<T> getMatching(Revision rev) {
return matches.get(rev);
}
/**
* Returns all <code>Matching</code>s added for this <code>Artifact</code>.
*
* @return the <code>Matching</code>s
*/
public Set<Matching<T>> getMatchings() {
return new HashSet<>(matches.values());
}
/**
* Returns an unmodifiable view of the map used to store the <code>Matchings</code> of this <code>Artifact</code>.
*
* @return the matchings
*/
public Map<Revision, Matching<T>> getMatches() {
return Collections.unmodifiableMap(matches);
}
/**
* Returns the number of the <code>Artifact</code>.
*
* @return number of the <code>Artifact</code>
*/
public int getNumber() {
return number;
}
/**
* Sets the number of all <code>Artifact</code>s contained in the tree rooted at this artifact to their index in
* a depth-first traversal of the tree.
*/
public void renumber() {
renumber(new AtomicInteger()::getAndIncrement);
}
/**
* Sets the number of all <code>Artifact</code>s contained in the tree rooted at this artifact to the number
* supplied by <code>number</code> when traversing the tree in depth-first order.
*
* @param number
* the supplier for the new numbers
*/
private void renumber(Supplier<Integer> number) {
this.number = number.get();
for (Artifact<T> child : children) {
child.renumber(number);
}
}
/**
* Returns the number of children the <code>Artifact</code> has.
*
* @return number of children
*/
public int getNumChildren() {
if (isLeaf()) {
return 0;
}
return children == null ? 0 : children.size();
}
/**
* Returns the parent <code>Artifact</code>.
*
* @return the parent <code>Artifact</code>
*/
public T getParent() {
return parent;
}
/**
* Returns the <code>Revision</code> the <code>Artifact</code> belongs to.
*
* @return the <code>Revision</code> the <code>Artifact</code> belongs to.
*/
public Revision getRevision() {
return revision;
}
/**
* Returns the maximum depth of any node in the tree.
*
* @return the maximum depth
*/
public int getMaxDepth() {
return 1 + children.parallelStream().map(T::getMaxDepth).max(Integer::compare).orElse(0);
}
/**
* Returns the size of the subtree. The <code>Artifact</code> itself is not included.
*
* @return size of subtree
*/
public int getSubtreeSize() {
int size = getNumChildren();
for (int i = 0; i < getNumChildren(); i++) {
size += getChild(i).getSubtreeSize();
}
return size;
}
/**
* Returns the size of the tree. The <code>Artifact</code> itself is also included.
*
* @return size of tree
*/
public int getTreeSize() {
return getSubtreeSize() + 1;
}
/**
* Returns whether the <code>Artifact</code> or its subtree has changes.
*
* @return whether the <code>Artifact</code> or its subtree has changes
*/
public boolean hasChanges() {
// FIXME: this method does currently not detect deletions as changes.
boolean hasChanges = !hasMatches();
for (int i = 0; !hasChanges && i < getNumChildren(); i++) {
hasChanges = getChild(i).hasChanges();
}
return hasChanges;
}
/**
* Returns whether the <code>Artifact</code> or its subtree has changes compared to <code>Revision</code> revision.
*
* @param revision <Code>Revision</Code> to compare to
* @return whether the <code>Artifact</code> or its subtree has changes compared to <code>Revision</code> revision
*/
public boolean hasChanges(Revision revision) {
boolean hasChanges = !hasMatching(revision);
if (!hasChanges) {
T baseArtifact = getMatching(revision).getMatchingArtifact(this);
hasChanges = baseArtifact.hasChanges();
}
for (int i = 0; !hasChanges && i < getNumChildren(); i++) {
hasChanges = getChild(i).hasChanges(revision);
}
return hasChanges;
}
/**
* Returns true if the <code>Artifact</code> is a change.
*
* @return true if the <code>Artifact</code> is a change
*/
public boolean isChange() {
return !hasMatches();
}
/**
* Returns true if the <code>Artifact</code> has children.
*
* @return true if the <code>Artifact</code> has children
*/
public boolean hasChildren() {
return getNumChildren() > 0;
}
/**
* Returns whether this <code>Artifact</code> has any matches.
*
* @return true if the <code>Artifact</code> has matches
*/
public boolean hasMatches() {
return !matches.isEmpty();
}
/**
* Returns whether this <code>Artifact</code> has a <code>Matching</code> for a specific <code>Revision</code>.
*
* @param rev
* <code>Revision</code>
* @return true if <code>Artifact</code> has a <code>Matching</code> with <code>Revision</code>
*/
public final boolean hasMatching(Revision rev) {
boolean hasMatching = matches.containsKey(rev);
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest(getId() + ".hasMatching(" + rev + ")");
if (!matches.isEmpty()) {
for (Revision r : matches.keySet()) {
LOG.finest("Matching found with: " + r + " (" + matches.get(r).getMatchingArtifact(this).getId() + ")");
LOG.finest("hasMatching(" + r + ") = " + hasMatching);
}
} else {
LOG.finest("no matches for " + getId() + " and " + rev);
}
}
if (!hasMatching && isChoice()) {
// choice nodes have to be treated specially ...
for (T variant: variants.values()) {
if (variant.hasMatching(rev)) {
hasMatching = true;
break;
}
}
}
return hasMatching;
}
/**
* Returns whether a <code>Matching</code> exists for a specific <code>Artifact</code>.
*
* @param other
* other <code>Artifact</code> to search <code>Matching</code>s for
* @return whether a <code>Matching</code> exists
*/
public final boolean hasMatching(T other) {
Revision otherRev = other.getRevision();
boolean hasMatching = matches.containsKey(otherRev) && matches.get(otherRev).getMatchingArtifact(this) == other;
if (LOG.isLoggable(Level.FINEST)) {
LOG.finest(getId() + ".hasMatching(" + other.getId() + ")");
if (!matches.isEmpty()) {
for (Revision r : matches.keySet()) {
LOG.finest("Matching found with: " + r + " (" + other.getId() + ")");
LOG.finest("hasMatching(" + r + ") = " + hasMatching);
}
} else {
LOG.finest("no matches for " + getId() + " and " + other.getId());
}
}
if (!hasMatching && isChoice()) {
// choice nodes have to be treated specially ...
for (T variant: variants.values()) {
if (variant.hasMatching(otherRev) && matches.get(otherRev).getMatchingArtifact(variant) == other) {
hasMatching = true;
break;
}
}
}
return hasMatching;
}
/**
* Returns a <code>Supplier</code> producing a unique label for this <code>Artifact</code> or an empty optional
* if there is no such label. If there is a unique label a more efficient <code>UnorderedMatcher</code> can be used.
*
* @return optionally a <code>Supplier</code> producing a unique label
*/
public abstract Optional<Supplier<String>> getUniqueLabel();
/**
* Returns true if the <code>Artifact</code> is a conflict node.
*
* @return true if the <code>Artifact</code> represents a conflict
*/
public boolean isConflict() {
return conflict;
}
/**
* Returns true if the artifact is a choice node.
*
* @return true if the artifact represents a choice node
*/
public final boolean isChoice() {
return choice;
}
/**
* Returns true if the <code>Artifact</code> is empty.
*
* @return true if the <code>Artifact</code> is empty
*/
public abstract boolean isEmpty();
/**
* Returns true if the <code>Artifact</code> is a leaf.
*
* @return true if the <code>Artifact</code> is a leaf
*/
public abstract boolean isLeaf();
/**
* Returns true if the <code>Artifact</code> has already been merged.
* @return true if the <code>Artifact</code> has already been merged
*/
public boolean isMerged() {
return merged;
}
/**
* Returns true if the declaration order of the <code>Artifact</code> is essential.
*
* @return true if the declaration order of the <code>Artifact</code> is essential
*/
public abstract boolean isOrdered();
/**
* Returns true if the <code>Artifact</code> is the root node.
*
* @return true if the <code>Artifact</code> is the root node
*/
public boolean isRoot() {
return getParent() == null;
}
/**
* Returns true, if this <code>Artifact</code> matches another <code>Artifact</code>.
*
* @param other
* other <code>Artifact</code>
* @return true, if the <code>Artifact</code>s match
*/
public abstract boolean matches(T other);
/**
* Performs a merge on the provided merge triple.
* This method selects the <code>MergeStrategy</code> and triggers the merge.
*
* @param operation
* merge operation
* @param context
* merge context
*/
public abstract void merge(MergeOperation<T> operation, MergeContext context);
/**
* Sets the children of the <code>Artifact</code>.
*
* @param children
* the new children to set
*/
public void setChildren(ArtifactList<T> children) {
this.children = children;
}
/**
* Marks this <code>Artifact</code> as a conflict.
*
* @param left
* left alternative
* @param right
* right alternative
*/
protected void setConflict(T left, T right) {
this.conflict = true;
this.left = left;
this.right = right;
}
/**
* Returns the left alternative of a conflict.
*
* @return the left <code>Artifact</code>
*/
public T getLeft() {
return left;
}
/**
* Returns the right alternative of a conflict.
*
* @return the right <code>Artifact</code>
*/
public T getRight() {
return right;
}
/**
* Marks this artifact as a choice.
*
* @param condition presence condition
* @param artifact conditional artifact
*/
public final void setChoice(final String condition, final T artifact) {
this.choice = true;
if (condition == null) {
throw new RuntimeException("condition must not be null!");
}
addVariant(condition, artifact);
}
public void addVariant(String condition, final T artifact) {
if (!choice) {
throw new RuntimeException("addVariant() can only be called on choice nodes!");
}
if (condition == null) {
throw new RuntimeException("condition must not be null!");
}
LOG.fine("Add node " + artifact.getId() + " under condition " + condition);
if (variants == null) {
variants = new HashMap<>();
}
// merge conditions for same artifact
List<String> mergedConditions = new ArrayList<>();
for (String existingCondition : variants.keySet()) {
if (variants.get(existingCondition).equals(artifact)) {
mergedConditions.add(existingCondition);
condition = existingCondition + " || " + condition;
}
}
for (String mergedCondition : mergedConditions) {
variants.remove(mergedCondition);
}
variants.put(condition, artifact);
}
/**
* Set whether the <code>Artifact</code> has already been merged.
*/
public void setMerged() {
this.merged = true;
}
/**
* Sets the number of the <code>Artifact</code>
* @param number
* the number to set
*/
public void setNumber(int number) {
this.number = number;
}
/**
* Sets the parent <code>Artifact</code>.
*
* @param parent
* the parent to set
*/
protected void setParent(T parent) {
this.parent = parent;
}
/**
* Sets the <code>Revision</code>.
*
* @param revision
* the <code>Revision</code> to set
*/
public void setRevision(Revision revision) {
setRevision(revision, false);
}
public void setRevision(Revision revision, boolean recursive) {
this.revision = revision;
if (recursive && children != null) {
for (T child : children) {
child.setRevision(revision, true);
}
}
}
@Override
public abstract String toString();
@Override
public final int compareTo(T o) {
return getId().compareTo(o.getId());
}
/**
* If the artifact is a choice node, it has variants (values of map) that are present under conditions (keys of map)
*/
public HashMap<String, T> getVariants() {
return variants;
}
/**
* Attempts to find an <code>Artifact</code> with the given number in the <code>Artifact</code> tree with this
* <code>Artifact</code> at its root.
*
* @param number
* the number of the <code>Artifact</code> to find
* @return optionally the <code>Artifact</code> with the sought number
*/
public Optional<Artifact<T>> find(int number) {
if (this.number == number) {
return Optional.of(this);
}
return children.stream().map(c -> c.find(number)).filter(Optional::isPresent).findFirst().map(Optional::get);
}
}