package org.jabref.model.groups;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.jabref.model.FieldChange;
import org.jabref.model.TreeNode;
import org.jabref.model.database.BibDatabase;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.search.SearchMatcher;
import org.jabref.model.search.matchers.MatcherSet;
import org.jabref.model.search.matchers.MatcherSets;
/**
* A node in the groups tree that holds exactly one AbstractGroup.
*/
public class GroupTreeNode extends TreeNode<GroupTreeNode> {
private static final String PATH_DELEMITER = " > ";
private AbstractGroup group;
/**
* Creates this node and associates the specified group with it.
*
* @param group the group underlying this node
*/
public GroupTreeNode(AbstractGroup group) {
super(GroupTreeNode.class);
this.group = Objects.requireNonNull(group);
}
public static GroupTreeNode fromGroup(AbstractGroup group) {
return new GroupTreeNode(group);
}
/**
* Returns the group underlying this node.
*
* @return the group associated with this node
*/
public AbstractGroup getGroup() {
return group;
}
/**
* Associates the specified group with this node.
*
* @param newGroup the new group (has to be non-null)
* @deprecated use {@link #setGroup(AbstractGroup, boolean, boolean, List)}} instead
*/
@Deprecated
public void setGroup(AbstractGroup newGroup) {
this.group = Objects.requireNonNull(newGroup);
}
/**
* Associates the specified group with this node while also providing the possibility to modify previous matched
* entries so that they are now matched by the new group.
*
* @param newGroup the new group (has to be non-null)
* @param shouldKeepPreviousAssignments specifies whether previous matched entries should be added to the new group
* @param shouldRemovePreviousAssignments specifies whether previous matched entries should be removed from the old group
* @param entriesInDatabase list of entries in the database
*/
public List<FieldChange> setGroup(AbstractGroup newGroup, boolean shouldKeepPreviousAssignments,
boolean shouldRemovePreviousAssignments, List<BibEntry> entriesInDatabase) {
AbstractGroup oldGroup = getGroup();
setGroup(newGroup);
List<FieldChange> changes = new ArrayList<>();
boolean shouldRemove = shouldRemovePreviousAssignments && (oldGroup instanceof GroupEntryChanger);
boolean shouldAdd = shouldKeepPreviousAssignments && (newGroup instanceof GroupEntryChanger);
if (shouldAdd || shouldRemove) {
List<BibEntry> entriesMatchedByOldGroup = entriesInDatabase.stream().filter(oldGroup::isMatch)
.collect(Collectors.toList());
if (shouldRemove) {
GroupEntryChanger entryChanger = (GroupEntryChanger) oldGroup;
changes.addAll(entryChanger.remove(entriesMatchedByOldGroup));
}
if (shouldAdd) {
GroupEntryChanger entryChanger = (GroupEntryChanger) newGroup;
changes.addAll(entryChanger.add(entriesMatchedByOldGroup));
}
}
return changes;
}
/**
* Creates a {@link SearchMatcher} that matches entries of this group and that takes the hierarchical information
* into account. I.e., it finds elements contained in this nodes group,
* or the union of those elements in its own group and its
* children's groups (recursively), or the intersection of the elements in
* its own group and its parent's group (depending on the hierarchical settings stored in the involved groups)
*/
public SearchMatcher getSearchMatcher() {
return getSearchMatcher(group.getHierarchicalContext());
}
private SearchMatcher getSearchMatcher(GroupHierarchyType originalContext) {
final GroupHierarchyType context = group.getHierarchicalContext();
if (context == GroupHierarchyType.INDEPENDENT) {
return group;
}
MatcherSet searchRule = MatcherSets.build(
context == GroupHierarchyType.REFINING ? MatcherSets.MatcherType.AND : MatcherSets.MatcherType.OR);
searchRule.addRule(group);
if ((context == GroupHierarchyType.INCLUDING) && (originalContext != GroupHierarchyType.REFINING)) {
for (GroupTreeNode child : getChildren()) {
searchRule.addRule(child.getSearchMatcher(originalContext));
}
} else if ((context == GroupHierarchyType.REFINING) && !isRoot() && (originalContext
!= GroupHierarchyType.INCLUDING)) {
//noinspection OptionalGetWithoutIsPresent
searchRule.addRule(getParent().get().getSearchMatcher(originalContext));
}
return searchRule;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ((o == null) || (getClass() != o.getClass())) {
return false;
}
GroupTreeNode that = (GroupTreeNode) o;
return Objects.equals(group, that.group) &&
Objects.equals(getChildren(), that.getChildren());
}
@Override
public int hashCode() {
return Objects.hash(group);
}
public List<GroupTreeNode> getContainingGroups(List<BibEntry> entries, boolean requireAll) {
List<GroupTreeNode> groups = new ArrayList<>();
// Add myself if I contain the entries
if (requireAll) {
if (this.group.containsAll(entries)) {
groups.add(this);
}
} else {
if (this.group.containsAny(entries)) {
groups.add(this);
}
}
// Traverse children
for (GroupTreeNode child : getChildren()) {
groups.addAll(child.getContainingGroups(entries, requireAll));
}
return groups;
}
public List<GroupTreeNode> getMatchingGroups(List<BibEntry> entries) {
List<GroupTreeNode> groups = new ArrayList<>();
// Add myself if I contain the entries
SearchMatcher matcher = getSearchMatcher();
for (BibEntry entry : entries) {
if (matcher.isMatch(entry)) {
groups.add(this);
break;
}
}
// Traverse children
for (GroupTreeNode child : getChildren()) {
groups.addAll(child.getMatchingGroups(entries));
}
return groups;
}
public String getName() {
return group.getName();
}
public GroupTreeNode addSubgroup(AbstractGroup subgroup) {
GroupTreeNode child = GroupTreeNode.fromGroup(subgroup);
addChild(child);
return child;
}
@Override
public GroupTreeNode copyNode() {
return GroupTreeNode.fromGroup(group);
}
/**
* Determines the number of entries in the specified list which are matched by this group.
* @param entries list of entries to be searched
* @return number of hits
*/
public int calculateNumberOfMatches(List<BibEntry> entries) {
int hits = 0;
SearchMatcher matcher = getSearchMatcher();
for (BibEntry entry : entries) {
if (matcher.isMatch(entry)) {
hits++;
}
}
return hits;
}
/**
* Determines the number of entries in the specified database which are matched by this group.
* @param database database to be searched
* @return number of hits
*/
public int calculateNumberOfMatches(BibDatabase database) {
return calculateNumberOfMatches(database.getEntries());
}
/**
* Returns whether this group matches the specified {@link BibEntry} while taking the hierarchical information
* into account.
*/
public boolean matches(BibEntry entry) {
return getSearchMatcher().isMatch(entry);
}
/**
* Get the path from the root of the tree as a string (every group name is separated by {@link #PATH_DELEMITER}.
*
* The name of the root is not included.
*/
public String getPath() {
return getPathFromRoot().stream()
.skip(1) // Skip root
.map(GroupTreeNode::getName)
.collect(Collectors.joining(PATH_DELEMITER));
}
@Override
public String toString() {
return "GroupTreeNode{" +
"group=" + group +
'}';
}
/**
* Finds a children using the given path.
* Each group name should be separated by {@link #PATH_DELEMITER}.
*
* The path should be generated using {@link #getPath()}.
*/
public Optional<GroupTreeNode> getChildByPath(String pathToSource) {
GroupTreeNode present = this;
for (String groupName : pathToSource.split(PATH_DELEMITER)) {
Optional<GroupTreeNode> childWithName = present.getChildren().stream()
.filter(group -> Objects.equals(group.getName(), groupName))
.findFirst();
if (childWithName.isPresent()) {
present = childWithName.get();
} else {
// No child with that name found -> path seems to be invalid
return Optional.empty();
}
}
return Optional.of(present);
}
/**
* Adds the specified entries to this group.
* If the group does not support explicit adding of entries (i.e., does not implement {@link GroupEntryChanger}),
* then no action is performed.
*/
public List<FieldChange> addEntriesToGroup(List<BibEntry> entries) {
if (getGroup() instanceof GroupEntryChanger) {
return ((GroupEntryChanger) getGroup()).add(entries);
} else {
return Collections.emptyList();
}
}
/**
* Removes the given entries from this group. If the group does not support the explicit removal of entries (i.e.,
* does not implement {@link GroupEntryChanger}), then no action is performed.
*/
public List<FieldChange> removeEntriesFromGroup(List<BibEntry> entries) {
if (getGroup() instanceof GroupEntryChanger) {
return ((GroupEntryChanger) getGroup()).remove(entries);
} else {
return Collections.emptyList();
}
}
/**
* Returns true if the underlying groups of both {@link GroupTreeNode}s is the same.
*/
public boolean isSameGroupAs(GroupTreeNode other) {
return Objects.equals(group, other.group);
}
}