Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #628: implement hierarchical keywords #2703

Merged
merged 6 commits into from Apr 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Expand Up @@ -45,7 +45,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `#
- We added a few properties to a group:
- Icon (with customizable color) that is shown in the groups panel (implements a [feature request in the forum](http://discourse.jabref.org/t/assign-colors-to-groups/321)).
- Description text that is shown on mouse hover (implements old feature requests [489](https://sourceforge.net/p/jabref/feature-requests/489/) and [818](https://sourceforge.net/p/jabref/feature-requests/818/)
- We introduced "automatic groups" that automatically create subgroups based on a certain criteria (e.g. a subgroup for every author or keyword). Implements [91](https://sourceforge.net/p/jabref/feature-requests/91/), [398](https://sourceforge.net/p/jabref/feature-requests/398/) and [#1173](https://github.com/JabRef/jabref/issues/1173).
- We introduced "automatic groups" that automatically create subgroups based on a certain criteria (e.g. a subgroup for every author or keyword) and supports hierarchies. Implements [91](https://sourceforge.net/p/jabref/feature-requests/91/), [398](https://sourceforge.net/p/jabref/feature-requests/398/) and [#1173](https://github.com/JabRef/jabref/issues/1173) and [#628](https://github.com/JabRef/jabref/issues/628).
- Expansion status of groups are saved across sessions. [#1428](https://github.com/JabRef/jabref/issues/1428)
- We removed the ordinals-to-superscript formatter from the recommendations for biblatex save actions [#2596](https://github.com/JabRef/jabref/issues/2596)
- The `Move linked files to default file directory`-Cleanup operation respects the `File directory pattern` setting
Expand Down
21 changes: 14 additions & 7 deletions src/main/java/org/jabref/gui/groups/GroupDialog.java
Expand Up @@ -39,6 +39,7 @@
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.search.SearchQuery;
import org.jabref.model.entry.FieldName;
import org.jabref.model.entry.Keyword;
import org.jabref.model.groups.AbstractGroup;
import org.jabref.model.groups.AutomaticKeywordGroup;
import org.jabref.model.groups.AutomaticPersonsGroup;
Expand Down Expand Up @@ -100,6 +101,7 @@ class GroupDialog extends JDialog implements Dialog<AbstractGroup> {
Localization.lang("Generate groups from keywords in a BibTeX field"));
private final JTextField autoGroupKeywordsField = new JTextField(60);
private final JTextField autoGroupKeywordsDeliminator = new JTextField(60);
private final JTextField autoGroupKeywordsHierarchicalDeliminator = new JTextField(60);
private final JRadioButton autoGroupPersonsOption = new JRadioButton(
Localization.lang("Generate groups for author last names"));
private final JTextField autoGroupPersonsField = new JTextField(60);
Expand Down Expand Up @@ -183,22 +185,24 @@ public GroupDialog(JabRefFrame jabrefFrame, AbstractGroup editedGroup) {
bg.add(autoGroupPersonsOption);

FormLayout layoutAutoGroup = new FormLayout("left:20dlu, 4dlu, left:pref, 4dlu, fill:60dlu",
"p, 2dlu, p, 2dlu, p, 2dlu, p, 2dlu, p");
"p, 2dlu, p, 2dlu, p, p, 2dlu, p, 2dlu, p");
FormBuilder builderAutoGroup = FormBuilder.create();
builderAutoGroup.layout(layoutAutoGroup);
builderAutoGroup.add(autoGroupKeywordsOption).xyw(1, 1, 5);
builderAutoGroup.add(Localization.lang("Field to group by") + ":").xy(3, 3);
builderAutoGroup.add(autoGroupKeywordsField).xy(5, 3);
builderAutoGroup.add(Localization.lang("Use the following delimiter character(s):")).xy(3, 5);
builderAutoGroup.add(autoGroupKeywordsDeliminator).xy(5, 5);
builderAutoGroup.add(autoGroupPersonsOption).xyw(1, 7, 5);
builderAutoGroup.add(Localization.lang("Field to group by") + ":").xy(3, 9);
builderAutoGroup.add(autoGroupPersonsField).xy(5, 9);
builderAutoGroup.add(autoGroupKeywordsHierarchicalDeliminator).xy(5, 6);
builderAutoGroup.add(autoGroupPersonsOption).xyw(1, 8, 5);
builderAutoGroup.add(Localization.lang("Field to group by") + ":").xy(3, 10);
builderAutoGroup.add(autoGroupPersonsField).xy(5, 10);
optionsPanel.add(builderAutoGroup.build(), String.valueOf(GroupDialog.INDEX_AUTO_GROUP));

autoGroupKeywordsOption.setSelected(true);
autoGroupKeywordsField.setText(Globals.prefs.get(JabRefPreferences.GROUPS_DEFAULT_FIELD));
autoGroupKeywordsDeliminator.setText(Globals.prefs.get(JabRefPreferences.KEYWORD_SEPARATOR));
autoGroupKeywordsHierarchicalDeliminator.setText(Keyword.DEFAULT_HIERARCHICAL_DELIMITER.toString());
autoGroupPersonsField.setText(FieldName.AUTHOR);

// ... for buttons panel
Expand Down Expand Up @@ -349,9 +353,11 @@ public void actionPerformed(ActionEvent e) {
}
} else if (autoRadioButton.isSelected()) {
if (autoGroupKeywordsOption.isSelected()) {
resultingGroup = new AutomaticKeywordGroup(nameField.getText().trim(), getContext(),
resultingGroup = new AutomaticKeywordGroup(
nameField.getText().trim(), getContext(),
autoGroupKeywordsField.getText().trim(),
autoGroupKeywordsDeliminator.getText().charAt(0));
autoGroupKeywordsDeliminator.getText().charAt(0),
autoGroupKeywordsHierarchicalDeliminator.getText().charAt(0));
} else {
resultingGroup = new AutomaticPersonsGroup(nameField.getText().trim(), getContext(),
autoGroupPersonsField.getText().trim());
Expand Down Expand Up @@ -429,7 +435,8 @@ public void actionPerformed(ActionEvent e) {

if (editedGroup.getClass() == AutomaticKeywordGroup.class) {
AutomaticKeywordGroup group = (AutomaticKeywordGroup) editedGroup;
autoGroupKeywordsDeliminator.setText(group.getKeywordSeperator().toString());
autoGroupKeywordsDeliminator.setText(group.getKeywordDelimiter().toString());
autoGroupKeywordsHierarchicalDeliminator.setText(group.getKeywordHierarchicalDelimiter().toString());
autoGroupKeywordsField.setText(group.getField());
} else if (editedGroup.getClass() == AutomaticPersonsGroup.class) {
AutomaticPersonsGroup group = (AutomaticPersonsGroup) editedGroup;
Expand Down
27 changes: 6 additions & 21 deletions src/main/java/org/jabref/gui/groups/GroupNodeViewModel.java
@@ -1,14 +1,9 @@
package org.jabref.gui.groups;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
Expand Down Expand Up @@ -67,16 +62,12 @@ public GroupNodeViewModel(BibDatabaseContext databaseContext, StateManager state
if (groupNode.getGroup() instanceof AutomaticGroup) {
AutomaticGroup automaticGroup = (AutomaticGroup) groupNode.getGroup();

// TODO: Update on changes to entry list (however: there is no flatMap and filter as observable TransformationLists)
children = databaseContext.getDatabase()
.getEntries().stream()
.flatMap(stream -> createSubgroups(automaticGroup, stream))
.filter(distinctByKey(group -> group.getGroupNode().getName()))
children = automaticGroup.createSubgroups(databaseContext.getDatabase().getEntries()).stream()
.map(this::toViewModel)
.sorted((group1, group2) -> group1.getDisplayName().compareToIgnoreCase(group2.getDisplayName()))
.collect(Collectors.toCollection(FXCollections::observableArrayList));
} else {
children = EasyBind.map(groupNode.getChildren(),
child -> new GroupNodeViewModel(databaseContext, stateManager, taskExecutor, child));
children = EasyBind.map(groupNode.getChildren(), this::toViewModel);
}
hasChildren = new SimpleBooleanProperty();
hasChildren.bind(Bindings.isNotEmpty(children));
Expand All @@ -97,18 +88,12 @@ public GroupNodeViewModel(BibDatabaseContext databaseContext, StateManager state
this(databaseContext, stateManager, taskExecutor, new GroupTreeNode(group));
}

private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Map<Object,Boolean> seen = new ConcurrentHashMap<>();
return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

static GroupNodeViewModel getAllEntriesGroup(BibDatabaseContext newDatabase, StateManager stateManager, TaskExecutor taskExecutor) {
return new GroupNodeViewModel(newDatabase, stateManager, taskExecutor, DefaultGroupsFactory.getAllEntriesGroup());
}

private Stream<GroupNodeViewModel> createSubgroups(AutomaticGroup automaticGroup, BibEntry entry) {
return automaticGroup.createSubgroups(entry).stream()
.map(child -> new GroupNodeViewModel(databaseContext, stateManager, taskExecutor, child));
private GroupNodeViewModel toViewModel(GroupTreeNode child) {
return new GroupNodeViewModel(databaseContext, stateManager, taskExecutor, child);
}

public List<FieldChange> addEntriesToGroup(List<BibEntry> entries) {
Expand Down Expand Up @@ -243,7 +228,7 @@ public String getPath() {
}

public Optional<GroupNodeViewModel> getChildByPath(String pathToSource) {
return groupNode.getChildByPath(pathToSource).map(child -> new GroupNodeViewModel(databaseContext, stateManager, taskExecutor, child));
return groupNode.getChildByPath(pathToSource).map(this::toViewModel);
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/org/jabref/logic/exporter/GroupSerializer.java
Expand Up @@ -152,7 +152,9 @@ private String serializeAutomaticKeywordGroup(AutomaticKeywordGroup group) {
appendAutomaticGroupDetails(sb, group);
sb.append(StringUtil.quote(group.getField(), MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR, MetadataSerializationConfiguration.GROUP_QUOTE_CHAR));
sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR);
sb.append(group.getKeywordSeperator());
sb.append(group.getKeywordDelimiter());
sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR);
sb.append(group.getKeywordHierarchicalDelimiter());
sb.append(MetadataSerializationConfiguration.GROUP_UNIT_SEPARATOR);
appendGroupDetails(sb, group);
return sb.toString();
Expand Down
Expand Up @@ -127,8 +127,9 @@ private static AbstractGroup automaticKeywordGroupFromString(String string) {
String name = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR);
GroupHierarchyType context = GroupHierarchyType.getByNumberOrDefault(Integer.parseInt(tok.nextToken()));
String field = StringUtil.unquote(tok.nextToken(), MetadataSerializationConfiguration.GROUP_QUOTE_CHAR);
Character separator = tok.nextToken().charAt(0);
AutomaticKeywordGroup newGroup = new AutomaticKeywordGroup(name, context, field, separator);
Character delimiter = tok.nextToken().charAt(0);
Character hierarchicalDelimiter = tok.nextToken().charAt(0);
AutomaticKeywordGroup newGroup = new AutomaticKeywordGroup(name, context, field, delimiter, hierarchicalDelimiter);
addGroupDetails(tok, newGroup);
return newGroup;
}
Expand Down
70 changes: 41 additions & 29 deletions src/main/java/org/jabref/model/ChainNode.java
Expand Up @@ -58,35 +58,24 @@ public Optional<T> getParent() {
}

/**
* Returns this node's child or an empty Optional if this node has no child.
* Sets the parent node of this node.
* <p>
* This method does not set this node as the child of the new parent nor does it remove this node
* from the old parent. You should probably call {@link #moveTo(ChainNode)} to change the chain.
*
* @return this node's child T, or an empty Optional if this node has no child
* @param parent the new parent
*/
public Optional<T> getChild() {
return Optional.ofNullable(child);
protected void setParent(T parent) {
this.parent = Objects.requireNonNull(parent);
}

/**
* Removes this node from its parent and makes it a child of the specified node.
* In this way the whole subchain based at this node is moved to the given node.
* Returns this node's child or an empty Optional if this node has no child.
*
* @param target the new parent
* @throws NullPointerException if target is null
* @throws UnsupportedOperationException if target is an descendant of this node
* @return this node's child T, or an empty Optional if this node has no child
*/
public void moveTo(T target) {
Objects.requireNonNull(target);

// Check that the target node is not an ancestor of this node, because this would create loops in the tree
if (this.isAncestorOf(target)) {
throw new UnsupportedOperationException("the target cannot be a descendant of this node");
}

// Remove from previous parent
getParent().ifPresent(oldParent -> oldParent.removeChild());

// Add as child
target.setChild((T) this);
public Optional<T> getChild() {
return Optional.ofNullable(child);
}

/**
Expand All @@ -110,15 +99,26 @@ public T setChild(T child) {
}

/**
* Sets the parent node of this node.
* <p>
* This method does not set this node as the child of the new parent nor does it remove this node
* from the old parent. You should probably call {@link #moveTo(ChainNode)} to change the chain.
* Removes this node from its parent and makes it a child of the specified node.
* In this way the whole subchain based at this node is moved to the given node.
*
* @param parent the new parent
* @param target the new parent
* @throws NullPointerException if target is null
* @throws UnsupportedOperationException if target is an descendant of this node
*/
protected void setParent(T parent) {
this.parent = Objects.requireNonNull(parent);
public void moveTo(T target) {
Objects.requireNonNull(target);

// Check that the target node is not an ancestor of this node, because this would create loops in the tree
if (this.isAncestorOf(target)) {
throw new UnsupportedOperationException("the target cannot be a descendant of this node");
}

// Remove from previous parent
getParent().ifPresent(oldParent -> oldParent.removeChild());

// Add as child
target.setChild((T) this);
}

/**
Expand Down Expand Up @@ -151,4 +151,16 @@ public boolean isAncestorOf(T anotherNode) {
return child.isAncestorOf(anotherNode);
}
}

/**
* Adds the given node at the end of the chain.
* E.g., "A > B > C" + "D" -> "A > B > C > D".
*/
public void addAtEnd(T node) {
if (child == null) {
setChild(node);
} else {
child.addAtEnd(node);
}
}
}
5 changes: 5 additions & 0 deletions src/main/java/org/jabref/model/database/BibDatabase.java
Expand Up @@ -3,6 +3,7 @@
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
Expand Down Expand Up @@ -198,6 +199,10 @@ public synchronized boolean insertEntry(BibEntry entry, EntryEventSource eventSo
return duplicationChecker.isDuplicateCiteKeyExisting(entry);
}

public synchronized void insertEntries(BibEntry... entries) throws KeyCollisionException {
insertEntries(Arrays.asList(entries), EntryEventSource.LOCAL);
}

public synchronized void insertEntries(List<BibEntry> entries) throws KeyCollisionException {
insertEntries(entries, EntryEventSource.LOCAL);
}
Expand Down