diff --git a/CHANGELOG.md b/CHANGELOG.md index e972adb0c36..66735228adc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We improved the Medline importer to correctly import ISO dates for `revised`. [#9536](https://github.com/JabRef/jabref/issues/9536) - To avoid cluttering of the directory, We always delete the `.sav` file upon successful write. [#9675](https://github.com/JabRef/jabref/pull/9675) - We improved the unlinking/deletion of multiple linked files of an entry using the Delete key. [#9473](https://github.com/JabRef/jabref/issues/9473) +- The field names of customized entry types are now exchanged preserving the case. [#9993](https://github.com/JabRef/jabref/pull/9993) - We moved the custom entry types dialog into the preferences dialog. [#9760](https://github.com/JabRef/jabref/pull/9760) - We moved the manage content selectors dialog to the library properties. [#9768](https://github.com/JabRef/jabref/pull/9768) - We moved the preferences menu command from the options menu to the file menu. [#9768](https://github.com/JabRef/jabref/pull/9768) diff --git a/build.gradle b/build.gradle index 4bf16a16552..5da1beb056f 100644 --- a/build.gradle +++ b/build.gradle @@ -201,7 +201,8 @@ dependencies { implementation 'com.vladsch.flexmark:flexmark:0.64.8' implementation group: 'net.harawata', name: 'appdirs', version: '1.2.1' - implementation group: 'org.zalando', name: 'faux-pas', version: '0.9.0' + + implementation group: 'org.jooq', name: 'jool', version: '0.9.15' testImplementation 'io.github.classgraph:classgraph:4.8.160' testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3' diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 62fd79bf992..91229649901 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -96,9 +96,10 @@ requires flexmark; requires flexmark.util.ast; requires flexmark.util.data; + requires com.h2database.mvstore; - requires faux.pas; + requires org.jooq.jool; // fulltext search requires org.apache.lucene.core; diff --git a/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java b/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java index 92eb4e88817..d57148409be 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java +++ b/src/main/java/org/jabref/gui/fieldeditors/EditorTextArea.java @@ -76,7 +76,6 @@ public void paste() { */ @FunctionalInterface public interface PasteActionHandler { - void handle(); } } diff --git a/src/main/java/org/jabref/gui/importer/BibEntryTypePrefsAndFileViewModel.java b/src/main/java/org/jabref/gui/importer/BibEntryTypePrefsAndFileViewModel.java new file mode 100644 index 00000000000..27735d96c4a --- /dev/null +++ b/src/main/java/org/jabref/gui/importer/BibEntryTypePrefsAndFileViewModel.java @@ -0,0 +1,18 @@ +package org.jabref.gui.importer; + +import org.jabref.logic.exporter.MetaDataSerializer; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.entry.BibEntryType; + +public record BibEntryTypePrefsAndFileViewModel(BibEntryType customTypeFromPreferences, BibEntryType customTypeFromFile) { + /** + * Used to render in the UI. This is different from {@link BibEntryType#toString()}, because this is the serialization the user expects + */ + @Override + public String toString() { + return Localization.lang("%0 (from file)\n%1 (current setting)", + MetaDataSerializer.serializeCustomEntryTypes(customTypeFromFile), + MetaDataSerializer.serializeCustomEntryTypes(customTypeFromPreferences)); + } +} + diff --git a/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialog.java b/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialog.java index a99f090628f..93ce743ad3b 100644 --- a/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialog.java +++ b/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialog.java @@ -21,10 +21,12 @@ public class ImportCustomEntryTypesDialog extends BaseDialog { private final List customEntryTypes; + + @Inject private PreferencesService preferencesService; @FXML private VBox boxDifferentCustomization; + @FXML private CheckListView unknownEntryTypesCheckList; - @Inject private PreferencesService preferencesService; - @FXML private CheckListView differentCustomizationCheckList; + @FXML private CheckListView differentCustomizationCheckList; private final BibDatabaseMode mode; private ImportCustomEntryTypesDialogViewModel viewModel; @@ -39,7 +41,11 @@ public ImportCustomEntryTypesDialog(BibDatabaseMode mode, List cus setResultConverter(btn -> { if (btn == ButtonType.OK) { - viewModel.importBibEntryTypes(unknownEntryTypesCheckList.getCheckModel().getCheckedItems(), differentCustomizationCheckList.getCheckModel().getCheckedItems()); + viewModel.importBibEntryTypes( + unknownEntryTypesCheckList.getCheckModel().getCheckedItems(), + differentCustomizationCheckList.getCheckModel().getCheckedItems().stream() + .map(BibEntryTypePrefsAndFileViewModel::customTypeFromPreferences) + .toList()); } return null; }); diff --git a/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialogViewModel.java b/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialogViewModel.java index 7f910016f44..0a7102698e0 100644 --- a/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialogViewModel.java +++ b/src/main/java/org/jabref/gui/importer/ImportCustomEntryTypesDialogViewModel.java @@ -12,13 +12,18 @@ import org.jabref.model.entry.types.EntryTypeFactory; import org.jabref.preferences.PreferencesService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class ImportCustomEntryTypesDialogViewModel { + private static final Logger LOGGER = LoggerFactory.getLogger(ImportCustomEntryTypesDialogViewModel.class); + private final BibDatabaseMode mode; private final PreferencesService preferencesService; private final ObservableList newTypes = FXCollections.observableArrayList(); - private final ObservableList differentCustomizationTypes = FXCollections.observableArrayList(); + private final ObservableList differentCustomizationTypes = FXCollections.observableArrayList(); public ImportCustomEntryTypesDialogViewModel(BibDatabaseMode mode, List entryTypes, PreferencesService preferencesService) { this.mode = mode; @@ -29,8 +34,10 @@ public ImportCustomEntryTypesDialogViewModel(BibDatabaseMode mode, List newTypes() { return this.newTypes; } - public ObservableList differentCustomizations() { + public ObservableList differentCustomizations() { return this.differentCustomizationTypes; } diff --git a/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesView.java b/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesView.java index a6fec6aa7ab..405275aa306 100644 --- a/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesView.java +++ b/src/main/java/org/jabref/gui/libraryproperties/constants/ConstantsPropertiesView.java @@ -72,7 +72,6 @@ public void initialize() { dialogService.showErrorDialogAndWait(Localization.lang( "A string with the label '%0' already exists.", cellEvent.getNewValue())); - cellItem.setLabel(cellEvent.getOldValue()); } else { cellItem.setLabel(cellEvent.getNewValue()); diff --git a/src/main/java/org/jabref/gui/preferences/customentrytypes/CustomEntryTypesTab.java b/src/main/java/org/jabref/gui/preferences/customentrytypes/CustomEntryTypesTab.java index 5cea7fded54..02317d97885 100644 --- a/src/main/java/org/jabref/gui/preferences/customentrytypes/CustomEntryTypesTab.java +++ b/src/main/java/org/jabref/gui/preferences/customentrytypes/CustomEntryTypesTab.java @@ -35,8 +35,6 @@ import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.entry.field.Field; -import org.jabref.model.entry.field.UnknownField; -import org.jabref.model.strings.StringUtil; import com.airhacks.afterburner.views.ViewLoader; import com.tobiasdiez.easybind.EasyBind; @@ -146,30 +144,32 @@ private void setupEntryTypesTable() { } private void setupFieldsTable() { - fieldNameColumn.setCellValueFactory(item -> item.getValue().nameProperty()); + fieldNameColumn.setCellValueFactory(item -> item.getValue().displayNameProperty()); fieldNameColumn.setCellFactory(TextFieldTableCell.forTableColumn()); fieldNameColumn.setEditable(true); - fieldNameColumn.setOnEditCommit( - (TableColumn.CellEditEvent event) -> { - // This makes the displayed name consistent to org.jabref.model.entry.field.Field #getDisplayName() - String newFieldValue = StringUtil.capitalizeFirst(event.getNewValue()); - UnknownField field = (UnknownField) event.getRowValue().getField(); - EntryTypeViewModel selectedEntryType = viewModel.selectedEntryTypeProperty().get(); - ObservableList entryFields = selectedEntryType.fields(); - // The first predicate will check if the user input the original field name or doesn't edit anything after double click - boolean fieldExists = !newFieldValue.equals(field.getDisplayName()) && entryFields.stream().anyMatch(fieldViewModel -> - fieldViewModel.nameProperty().getValue().equalsIgnoreCase(newFieldValue)); - - if (fieldExists) { - dialogService.notify(Localization.lang("Unable to change field name. \"%0\" already in use.", newFieldValue)); - event.getTableView().edit(-1, null); - event.getTableView().refresh(); - } else { - event.getRowValue().setField(newFieldValue); - field.setName(newFieldValue); - event.getTableView().refresh(); - } - }); + fieldNameColumn.setOnEditCommit((TableColumn.CellEditEvent event) -> { + String newDisplayName = event.getNewValue(); + if (newDisplayName.isBlank()) { + dialogService.notify(Localization.lang("Name cannot be empty")); + event.getTableView().edit(-1, null); + event.getTableView().refresh(); + return; + } + + FieldViewModel fieldViewModel = event.getRowValue(); + String currentDisplayName = fieldViewModel.displayNameProperty().getValue(); + EntryTypeViewModel selectedEntryType = viewModel.selectedEntryTypeProperty().get(); + ObservableList entryFields = selectedEntryType.fields(); + // The first predicate will check if the user input the original field name or doesn't edit anything after double click + boolean fieldExists = !newDisplayName.equals(currentDisplayName) && viewModel.displayNameExists(newDisplayName); + if (fieldExists) { + dialogService.notify(Localization.lang("Unable to change field name. \"%0\" already in use.", newDisplayName)); + event.getTableView().edit(-1, null); + } else { + fieldViewModel.displayNameProperty().setValue(newDisplayName); + } + event.getTableView().refresh(); + }); fieldTypeColumn.setCellFactory(CheckBoxTableCell.forTableColumn(fieldTypeColumn)); fieldTypeColumn.setCellValueFactory(item -> item.getValue().requiredProperty()); @@ -182,7 +182,7 @@ private void setupFieldsTable() { fieldTypeActionColumn.setSortable(false); fieldTypeActionColumn.setReorderable(false); fieldTypeActionColumn.setEditable(false); - fieldTypeActionColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty()); + fieldTypeActionColumn.setCellValueFactory(cellData -> cellData.getValue().displayNameProperty()); new ValueTableCellFactory() .withGraphic(item -> IconTheme.JabRefIcons.DELETE_ENTRY.getGraphicNode()) diff --git a/src/main/java/org/jabref/gui/preferences/customentrytypes/CustomEntryTypesTabViewModel.java b/src/main/java/org/jabref/gui/preferences/customentrytypes/CustomEntryTypesTabViewModel.java index 7edf2b74dff..3e63facf759 100644 --- a/src/main/java/org/jabref/gui/preferences/customentrytypes/CustomEntryTypesTabViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/customentrytypes/CustomEntryTypesTabViewModel.java @@ -81,7 +81,7 @@ public CustomEntryTypesTabViewModel(BibDatabaseMode mode, } public void setValues() { - if (this.entryTypesWithFields.size() > 0) { + if (!this.entryTypesWithFields.isEmpty()) { this.entryTypesWithFields.clear(); } Collection allTypes = entryTypesManager.getAllTypes(bibDatabaseMode); @@ -105,12 +105,12 @@ public void storeSettings() { multilineFields.addAll(allFields.stream() .filter(FieldViewModel::isMultiline) - .map(FieldViewModel::getField) + .map(FieldViewModel::toField) .toList()); List required = allFields.stream() .filter(FieldViewModel::isRequired) - .map(FieldViewModel::getField) + .map(FieldViewModel::toField) .map(OrFields::new) .collect(Collectors.toList()); List fields = allFields.stream().map(FieldViewModel::toBibField).collect(Collectors.toList()); @@ -144,24 +144,28 @@ public void removeEntryType(EntryTypeViewModel focusedItem) { public void addNewField() { Field field = newFieldToAdd.getValue(); - ObservableList entryFields = this.selectedEntryType.getValue().fields(); - boolean fieldExists = entryFields.stream().anyMatch(fieldViewModel -> - fieldViewModel.nameProperty().getValue().equals(field.getDisplayName())); + boolean fieldExists = displayNameExists(field.getDisplayName()); - if (!fieldExists) { + if (fieldExists) { + dialogService.showWarningDialogAndWait( + Localization.lang("Duplicate fields"), + Localization.lang("Warning: You added field \"%0\" twice. Only one will be kept.", field.getDisplayName())); + } else { this.selectedEntryType.getValue().addField(new FieldViewModel( field, FieldViewModel.Mandatory.REQUIRED, FieldPriority.IMPORTANT, false)); - } else { - dialogService.showWarningDialogAndWait( - Localization.lang("Duplicate fields"), - Localization.lang("Warning: You added field \"%0\" twice. Only one will be kept.", field.getDisplayName())); } newFieldToAddProperty().setValue(null); } + public boolean displayNameExists(String displayName) { + ObservableList entryFields = this.selectedEntryType.getValue().fields(); + return entryFields.stream().anyMatch(fieldViewModel -> + fieldViewModel.displayNameProperty().getValue().equals(displayName)); + } + public void removeField(FieldViewModel focusedItem) { selectedEntryType.getValue().removeField(focusedItem); } diff --git a/src/main/java/org/jabref/gui/preferences/customentrytypes/EntryTypeViewModel.java b/src/main/java/org/jabref/gui/preferences/customentrytypes/EntryTypeViewModel.java index 4083de4f1ec..11c99161151 100644 --- a/src/main/java/org/jabref/gui/preferences/customentrytypes/EntryTypeViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/customentrytypes/EntryTypeViewModel.java @@ -22,7 +22,6 @@ public class EntryTypeViewModel { public EntryTypeViewModel(BibEntryType entryType, Predicate isMultiline) { this.entryType.set(entryType); - List allFieldsForType = entryType.getAllBibFields() .stream().map(bibField -> new FieldViewModel(bibField.field(), entryType.isRequired(bibField.field()) ? Mandatory.REQUIRED : Mandatory.OPTIONAL, diff --git a/src/main/java/org/jabref/gui/preferences/customentrytypes/FieldViewModel.java b/src/main/java/org/jabref/gui/preferences/customentrytypes/FieldViewModel.java index 2f9fb0741fd..2894d83f681 100644 --- a/src/main/java/org/jabref/gui/preferences/customentrytypes/FieldViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/customentrytypes/FieldViewModel.java @@ -10,15 +10,13 @@ import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.field.BibField; import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; import org.jabref.model.entry.field.FieldPriority; import org.jabref.model.entry.field.FieldProperty; -import com.tobiasdiez.easybind.EasyBind; - public class FieldViewModel { - private final Field field; - private final StringProperty fieldName = new SimpleStringProperty(""); + private final StringProperty displayName = new SimpleStringProperty(""); private final BooleanProperty required = new SimpleBooleanProperty(); private final BooleanProperty multiline = new SimpleBooleanProperty(); private final ObjectProperty priorityProperty = new SimpleObjectProperty<>(); @@ -27,27 +25,14 @@ public FieldViewModel(Field field, Mandatory required, FieldPriority priorityProperty, boolean multiline) { - this.field = field; - this.fieldName.setValue(field.getDisplayName()); + this.displayName.setValue(field.getDisplayName()); this.required.setValue(required == Mandatory.REQUIRED); this.priorityProperty.setValue(priorityProperty); this.multiline.setValue(multiline); - - EasyBind.subscribe(this.multiline, multi -> { - if (multi) { - this.field.getProperties().add(FieldProperty.MULTILINE_TEXT); - } else { - this.field.getProperties().remove(FieldProperty.MULTILINE_TEXT); - } - }); - } - - public Field getField() { - return field; } - public StringProperty nameProperty() { - return fieldName; + public StringProperty displayNameProperty() { + return displayName; } public BooleanProperty requiredProperty() { @@ -70,21 +55,26 @@ public FieldPriority getPriority() { return priorityProperty.getValue(); } - public void setField(String field) { - this.fieldName.setValue(field); + public Field toField() { + // If the field name is known by JabRef, JabRef's casing will win. + // If the field is not known by JabRef (UnknownField), the new casing will be taken. + Field field = FieldFactory.parseField(displayName.getValue()); + if (multiline.getValue()) { + field.getProperties().add(FieldProperty.MULTILINE_TEXT); + } + return field; } public BibField toBibField() { - return new BibField(field, priorityProperty.getValue()); + return new BibField(toField(), priorityProperty.getValue()); } @Override public String toString() { - return field.getDisplayName(); + return displayName.getValue(); } public enum Mandatory { - REQUIRED(Localization.lang("Required")), OPTIONAL(Localization.lang("Optional")); diff --git a/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java b/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java index 6a9a679c622..37c447017fc 100644 --- a/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java +++ b/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java @@ -12,7 +12,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; +import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -42,6 +42,8 @@ import org.jabref.model.metadata.SaveOrder; import org.jabref.model.strings.StringUtil; +import org.jooq.lambda.Unchecked; + /** * A generic writer for our database. This is independent of the concrete serialization format. * For instance, we could also write out YAML or XML by subclassing this class. @@ -180,13 +182,12 @@ public void saveDatabase(BibDatabaseContext bibDatabaseContext) throws IOExcepti /** * Saves the database, including only the specified entries. + * + * @param entries A list of entries to save. The list itself is not modified in this code */ public void savePartOfDatabase(BibDatabaseContext bibDatabaseContext, List entries) throws IOException { Optional sharedDatabaseIDOptional = bibDatabaseContext.getDatabase().getSharedDatabaseID(); - if (sharedDatabaseIDOptional.isPresent()) { - // may throw an IOException. Thus, we do not use "ifPresent", but the "old" isPresent way - writeDatabaseID(sharedDatabaseIDOptional.get()); - } + sharedDatabaseIDOptional.ifPresent(Unchecked.consumer(id -> writeDatabaseID(id))); // Some file formats write something at the start of the file (like the encoding) if (saveConfiguration.getSaveType() != SaveType.PLAIN_BIBTEX) { @@ -212,7 +213,7 @@ public void savePartOfDatabase(BibDatabaseContext bibDatabaseContext, List typesToWrite = new TreeSet<>(); + SortedSet typesToWrite = new TreeSet<>(); for (BibEntry entry : sortedEntries) { // Check if we must write the type definition for this @@ -325,7 +326,7 @@ protected void writeString(BibtexString bibtexString, Map protected abstract void writeString(BibtexString bibtexString, int maxKeyLength) throws IOException; - protected void writeEntryTypeDefinitions(Set types) throws IOException { + protected void writeEntryTypeDefinitions(SortedSet types) throws IOException { for (BibEntryType type : types) { writeEntryTypeDefinition(type); } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcher.java b/src/main/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcher.java index 10e134c7b5a..8ca9d967444 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcher.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/GrobidCitationFetcher.java @@ -16,10 +16,10 @@ import org.jabref.model.entry.BibEntry; import org.apache.lucene.queryparser.flexible.core.nodes.QueryNode; +import org.jooq.lambda.Unchecked; import org.jsoup.HttpStatusException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.zalando.fauxpas.FauxPas; public class GrobidCitationFetcher implements SearchBasedFetcher { @@ -73,7 +73,7 @@ public List performSearch(String searchQuery) throws FetcherException collect = Arrays.stream(searchQuery.split("\\r\\r+|\\n\\n+|\\r\\n(\\r\\n)+")) .map(String::trim) .filter(str -> !str.isBlank()) - .map(FauxPas.throwingFunction(this::parseUsingGrobid)) + .map(Unchecked.function(this::parseUsingGrobid)) .flatMap(Optional::stream) .collect(Collectors.toList()); return collect; diff --git a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java index bd5ff1179a0..a81e70bd6a4 100644 --- a/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java +++ b/src/main/java/org/jabref/logic/importer/util/MetaDataParser.java @@ -58,11 +58,12 @@ public static Optional parseCustomEntryType(String comment) { EntryType type = EntryTypeFactory.parse(rest.substring(0, indexEndOfName)); String reqFields = fieldsDescription.substring(4, indexEndOfRequiredFields); String optFields = fieldsDescription.substring(indexEndOfRequiredFields + 6, indexEndOfOptionalFields); - BibEntryTypeBuilder entryTypeBuilder = new BibEntryTypeBuilder() .withType(type) - .withImportantFields(FieldFactory.parseFieldList(optFields)) - .withRequiredFields(FieldFactory.parseOrFieldsList(reqFields)); + .withRequiredFields(FieldFactory.parseOrFieldsList(reqFields)) + // Important fields are optional fields, but displayed first. Thus, they do not need to be separated by "/". + // See org.jabref.model.entry.field.FieldPriority for details on important optional fields. + .withImportantFields(FieldFactory.parseFieldList(optFields)); return Optional.of(entryTypeBuilder.build()); } diff --git a/src/main/java/org/jabref/logic/remote/client/RemoteClient.java b/src/main/java/org/jabref/logic/remote/client/RemoteClient.java index 915ffc5e598..6844547eef0 100644 --- a/src/main/java/org/jabref/logic/remote/client/RemoteClient.java +++ b/src/main/java/org/jabref/logic/remote/client/RemoteClient.java @@ -39,7 +39,7 @@ public boolean ping() { return false; } } catch (IOException e) { - LOGGER.debug("Could not ping server at port " + port, e); + LOGGER.debug("Could not ping server at port {}", port, e); return false; } } @@ -56,7 +56,7 @@ public boolean sendCommandLineArguments(String[] args) { Pair response = protocol.receiveMessage(); return response.getKey() == RemoteMessage.OK; } catch (IOException e) { - LOGGER.debug("Could not send args " + String.join(", ", args) + " to the server at port " + port, e); + LOGGER.debug("Could not send args {} to the server at port {}", String.join(", ", args), port, e); return false; } } diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index 29873c4d9c2..3f6b82ae6b4 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -748,6 +748,16 @@ public void setChanged(boolean changed) { this.changed = changed; } + /** + * Required to trigger new serialization of the entry. + * Reason: We don't have a build() command, we don't want to create a new serialization at each call, + * we need to construct a BibEntry with changed=false (which is the default) and thus we need a workaround. + */ + public BibEntry withChanged(boolean changed) { + this.changed = changed; + return this; + } + public Optional putKeywords(List keywords, Character delimiter) { Objects.requireNonNull(delimiter); return putKeywords(new KeywordList(keywords), delimiter); diff --git a/src/main/java/org/jabref/model/entry/BibEntryType.java b/src/main/java/org/jabref/model/entry/BibEntryType.java index 01d5d452c74..4908aec875c 100644 --- a/src/main/java/org/jabref/model/entry/BibEntryType.java +++ b/src/main/java/org/jabref/model/entry/BibEntryType.java @@ -7,6 +7,8 @@ import java.util.Set; import java.util.stream.Collectors; +import org.jabref.gui.importer.BibEntryTypePrefsAndFileViewModel; +import org.jabref.logic.exporter.MetaDataSerializer; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.field.BibField; import org.jabref.model.entry.field.Field; @@ -24,25 +26,22 @@ public class BibEntryType implements Comparable { /** * Provides an enriched EntryType with information about defined standards as mandatory fields etc. * + * A builder is available at {@link BibEntryTypeBuilder} + * * @param type The EntryType this BibEntryType is wrapped around. * @param fields A BibFields list of all fields, including the required fields * @param requiredFields A OrFields list of just the required fields */ public BibEntryType(EntryType type, Collection fields, Collection requiredFields) { this.type = Objects.requireNonNull(type); - this.requiredFields = new LinkedHashSet<>(requiredFields); this.fields = new LinkedHashSet<>(fields); + this.requiredFields = new LinkedHashSet<>(requiredFields); } public EntryType getType() { return type; } - /** - * Returns all supported optional field names. - * - * @return a Set of optional field name Strings - */ public Set getOptionalFields() { return getAllBibFields().stream() .filter(field -> !isRequired(field.field())) @@ -146,13 +145,21 @@ public int hashCode() { return Objects.hash(type, requiredFields, fields); } + /** + * Generates a **single line** string containing the information. This is used for debugging purposes. + * + * See "Effective Java, Item 10" for a discussion on contracts. + * + * We are sure, we are using this method in a) logs (which should use single lines for output) and b) in the UI. For the UI, we use {@link BibEntryTypePrefsAndFileViewModel}, + * which in turn uses {@link MetaDataSerializer#serializeCustomEntryTypes(BibEntryType)} + */ @Override public String toString() { return "BibEntryType{" + - "type=" + type + - ", requiredFields=" + requiredFields + - ", fields=" + fields + - '}'; + "type=" + type + + ", allFields=" + fields + + ", requiredFields=" + requiredFields + + '}'; } @Override diff --git a/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java b/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java index 0ec4a06f175..eb25ad23972 100644 --- a/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java +++ b/src/main/java/org/jabref/model/entry/BibEntryTypesManager.java @@ -100,7 +100,7 @@ public boolean isDifferentCustomOrModifiedType(BibEntryType type, BibDatabaseMod return true; } else { // different customization - return !EntryTypeFactory.isEqualNameAndFieldBased(type, currentlyStoredType.get()); + return !EntryTypeFactory.nameAndFieldsAreEqual(type, currentlyStoredType.get()); } } diff --git a/src/main/java/org/jabref/model/entry/field/BibField.java b/src/main/java/org/jabref/model/entry/field/BibField.java index 4d0729e4f4a..1453e6fc32d 100644 --- a/src/main/java/org/jabref/model/entry/field/BibField.java +++ b/src/main/java/org/jabref/model/entry/field/BibField.java @@ -24,7 +24,7 @@ public int hashCode() { @Override public String toString() { return "BibField{" + - "field=" + field.getName() + + "field=" + field.getDisplayName() + ", priority=" + priority + '}'; } diff --git a/src/main/java/org/jabref/model/entry/field/Field.java b/src/main/java/org/jabref/model/entry/field/Field.java index abd9b270781..ec3055e493c 100644 --- a/src/main/java/org/jabref/model/entry/field/Field.java +++ b/src/main/java/org/jabref/model/entry/field/Field.java @@ -10,6 +10,8 @@ public interface Field { /** * properties contains mappings to tell the EntryEditor to add a specific function to this field, * for instance a dropdown for selecting the month for the month field. + * + * Note that this set needs to be mutable. This is required, because we allow standard fields to be modifiable via the UI. */ Set getProperties(); diff --git a/src/main/java/org/jabref/model/entry/field/FieldFactory.java b/src/main/java/org/jabref/model/entry/field/FieldFactory.java index 5da3e3fb428..2e8718d6806 100644 --- a/src/main/java/org/jabref/model/entry/field/FieldFactory.java +++ b/src/main/java/org/jabref/model/entry/field/FieldFactory.java @@ -11,6 +11,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.jabref.model.entry.types.EntryType; import org.jabref.model.strings.StringUtil; import org.jabref.model.util.OptionalUtil; @@ -29,7 +30,15 @@ public static String serializeOrFields(Field... fields) { public static String serializeOrFields(OrFields fields) { return fields.stream() - .map(Field::getName) + .map(field -> { + if (field instanceof UnknownField unknownField) { + // In case a user has put a user-defined field, the casing of that field is kept + return unknownField.getDisplayName(); + } else { + // In all fields known to JabRef, the name is used - JabRef knows better than the user how to case the field + return field.getName(); + } + }) .collect(Collectors.joining(FIELD_OR_SEPARATOR)); } @@ -47,9 +56,9 @@ public static List getIdentifierFieldNames() { public static OrFields parseOrFields(String fieldNames) { Set fields = Arrays.stream(fieldNames.split(FieldFactory.FIELD_OR_SEPARATOR)) - .filter(StringUtil::isNotBlank) - .map(FieldFactory::parseField) - .collect(Collectors.toCollection(LinkedHashSet::new)); + .filter(StringUtil::isNotBlank) + .map(FieldFactory::parseField) + .collect(Collectors.toCollection(LinkedHashSet::new)); return new OrFields(fields); } @@ -69,11 +78,23 @@ public static Set parseFieldList(String fieldNames) { public static String serializeFieldsList(Collection fields) { return fields.stream() - .map(Field::getName) + .map(field -> { + if (field instanceof UnknownField unknownField) { + // In case a user has put a user-defined field, the casing of that field is kept + return unknownField.getDisplayName(); + } else { + // In all fields known to JabRef, the name is used - JabRef knows better than the user how to case the field + return field.getName(); + } + }) .collect(Collectors.joining(DELIMITER)); } - public static Field parseField(T type, String fieldName) { + /** + * Type T is an entry type and is used to direct the mapping to the Java field class. + * This somehow acts as filter, BibLaTeX "APA" entry type has field "article", but we want to have StandardField (if not explicitly requested otherwise) + */ + public static Field parseField(T type, String fieldName) { // Check if the field name starts with "comment-" which indicates it's a UserSpecificCommentField if (fieldName.startsWith("comment-")) { String username = fieldName.substring("comment-".length()); @@ -92,7 +113,7 @@ public static Field parseField(T type, String fieldName) { BiblatexSoftwareField.fromName(type, fieldName)), BiblatexApaField.fromName(type, fieldName)), AMSField.fromName(type, fieldName)) - .orElse(new UnknownField(fieldName)); + .orElse(UnknownField.fromDisplayName(fieldName)); } public static Field parseField(String fieldName) { @@ -167,7 +188,7 @@ private static Set getAllFields() { /** * These are the fields JabRef always displays as default {@link org.jabref.preferences.JabRefPreferences#setLanguageDependentDefaultValues()} - * + *

* A user can change them. The change is currently stored in the preferences only and not explicitly exposed as * separate preferences object */ diff --git a/src/main/java/org/jabref/model/entry/field/FieldPriority.java b/src/main/java/org/jabref/model/entry/field/FieldPriority.java index 1c0e1cef4da..199151eeaf0 100644 --- a/src/main/java/org/jabref/model/entry/field/FieldPriority.java +++ b/src/main/java/org/jabref/model/entry/field/FieldPriority.java @@ -1,6 +1,19 @@ package org.jabref.model.entry.field; +import java.util.Locale; + +/** + * Determines whether the field is in the Optional1 or Optional2 tab + * + * See {@link org.jabref.model.entry.BibEntryType#getPrimaryOptionalFields()} + * and {@link org.jabref.model.entry.BibEntryType#getSecondaryOptionalFields()}. + */ public enum FieldPriority { IMPORTANT, - DETAIL + DETAIL; + + @Override + public String toString() { + return this.name().toLowerCase(Locale.ROOT); + } } diff --git a/src/main/java/org/jabref/model/entry/field/UnknownField.java b/src/main/java/org/jabref/model/entry/field/UnknownField.java index 2d9bca32eaf..a18779d3dc8 100644 --- a/src/main/java/org/jabref/model/entry/field/UnknownField.java +++ b/src/main/java/org/jabref/model/entry/field/UnknownField.java @@ -5,20 +5,37 @@ import java.util.Objects; import java.util.Set; +import org.jabref.model.strings.StringUtil; + public class UnknownField implements Field { private String name; private final Set properties; + private final String displayName; public UnknownField(String name) { + this(name, StringUtil.capitalizeFirst(name)); + } + + public UnknownField(String name, String displayName) { this.name = name; + this.displayName = displayName; this.properties = EnumSet.noneOf(FieldProperty.class); } public UnknownField(String name, FieldProperty first, FieldProperty... rest) { + this(name, StringUtil.capitalizeFirst(name), first, rest); + } + + public UnknownField(String name, String displayName, FieldProperty first, FieldProperty... rest) { this.name = name; + this.displayName = displayName; this.properties = EnumSet.of(first, rest); } + public static UnknownField fromDisplayName(String displayName) { + return new UnknownField(displayName.toLowerCase(Locale.ROOT), displayName); + } + @Override public Set getProperties() { return properties; @@ -33,6 +50,11 @@ public void setName(String name) { this.name = name; } + @Override + public String getDisplayName() { + return displayName; + } + @Override public boolean isStandardField() { return false; diff --git a/src/main/java/org/jabref/model/entry/types/EntryType.java b/src/main/java/org/jabref/model/entry/types/EntryType.java index 90d0264c00d..1a40be9a9ae 100644 --- a/src/main/java/org/jabref/model/entry/types/EntryType.java +++ b/src/main/java/org/jabref/model/entry/types/EntryType.java @@ -4,10 +4,11 @@ public interface EntryType { /** * Returns the tag name of the entry type. - * - * @return tag name of the entry type. */ String getName(); + /** + * Returns the name presented in the UI + */ String getDisplayName(); } diff --git a/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java b/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java index 6b40a56c4f4..ae879fabafb 100644 --- a/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java +++ b/src/main/java/org/jabref/model/entry/types/EntryTypeFactory.java @@ -14,14 +14,12 @@ private EntryTypeFactory() { } /** - * Checks whether two EntryTypeFactory are equal or not based on the equality of the type names and on the equality of - * the required and optional field lists + * Checks whether two EntryTypeFactory are equal + * based on the equality of the type names and on the equality of the required and optional field lists * - * @param type1 the first EntryType to compare - * @param type2 the secend EntryType to compare * @return returns true if the two compared entry types have the same name and equal required and optional fields */ - public static boolean isEqualNameAndFieldBased(BibEntryType type1, BibEntryType type2) { + public static boolean nameAndFieldsAreEqual(BibEntryType type1, BibEntryType type2) { if ((type1 == null) && (type2 == null)) { return true; } else if ((type1 == null) || (type2 == null)) { diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 1e8c1e0924d..95a0836e7db 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -1795,6 +1795,7 @@ DOI\ %0\ is\ invalid=DOI %0 is invalid Select\ all\ customized\ types\ to\ be\ stored\ in\ local\ preferences\:=Select all customized types to be stored in local preferences\: Different\ customization,\ current\ settings\ will\ be\ overwritten=Different customization, current settings will be overwritten +%0\ (from\ file)\n%1\ (current\ setting)=%0 (from file)\n%1 (current setting) Entry\ type\ %0\ is\ only\ defined\ for\ Biblatex\ but\ not\ for\ BibTeX=Entry type %0 is only defined for Biblatex but not for BibTeX diff --git a/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java b/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java index 9836c823ddd..dd765793e33 100644 --- a/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java +++ b/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java @@ -395,7 +395,7 @@ void writeCustomizedTypesInAlphabeticalOrder() throws Exception { database.insertEntry(entry); bibtexContext.setMode(BibDatabaseMode.BIBTEX); - databaseWriter.savePartOfDatabase(bibtexContext, Arrays.asList(entry, otherEntry)); + databaseWriter.savePartOfDatabase(bibtexContext, List.of(entry, otherEntry)); assertEquals( "@Customizedtype{," + OS.NEWLINE + "}" + OS.NEWLINE + OS.NEWLINE @@ -801,24 +801,22 @@ void writeEntriesSorted() throws Exception { new SaveOrder.SortCriterion(StandardField.ABSTRACT, false))); metaData.setSaveOrderConfig(saveOrder); - BibEntry firstEntry = new BibEntry(); - firstEntry.setType(StandardEntryType.Article); - firstEntry.setField(StandardField.AUTHOR, "A"); - firstEntry.setField(StandardField.YEAR, "2010"); + BibEntry firstEntry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "A") + .withField(StandardField.YEAR, "2010") + .withChanged(true); - BibEntry secondEntry = new BibEntry(); - secondEntry.setType(StandardEntryType.Article); - secondEntry.setField(StandardField.AUTHOR, "A"); - secondEntry.setField(StandardField.YEAR, "2000"); + BibEntry secondEntry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "A") + .withField(StandardField.YEAR, "2000") + .withChanged(true); - BibEntry thirdEntry = new BibEntry(); - thirdEntry.setType(StandardEntryType.Article); - thirdEntry.setField(StandardField.AUTHOR, "B"); - thirdEntry.setField(StandardField.YEAR, "2000"); + BibEntry thirdEntry = new BibEntry(StandardEntryType.Article) + .withField(StandardField.AUTHOR, "B") + .withField(StandardField.YEAR, "2000") + .withChanged(true); - database.insertEntry(secondEntry); - database.insertEntry(thirdEntry); - database.insertEntry(firstEntry); + database.insertEntries(secondEntry, thirdEntry, firstEntry); databaseWriter.savePartOfDatabase(bibtexContext, database.getEntries()); diff --git a/src/test/java/org/jabref/logic/exporter/MetaDataSerializerTest.java b/src/test/java/org/jabref/logic/exporter/MetaDataSerializerTest.java index f648627acbd..ebff3f7815e 100644 --- a/src/test/java/org/jabref/logic/exporter/MetaDataSerializerTest.java +++ b/src/test/java/org/jabref/logic/exporter/MetaDataSerializerTest.java @@ -1,11 +1,11 @@ package org.jabref.logic.exporter; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; +import java.util.stream.Stream; import org.jabref.logic.citationkeypattern.GlobalCitationKeyPattern; import org.jabref.logic.cleanup.FieldFormatterCleanup; @@ -14,10 +14,12 @@ import org.jabref.logic.importer.util.MetaDataParser; import org.jabref.logic.util.OS; import org.jabref.model.entry.BibEntryType; +import org.jabref.model.entry.BibEntryTypeBuilder; import org.jabref.model.entry.field.BibField; import org.jabref.model.entry.field.FieldPriority; import org.jabref.model.entry.field.OrFields; import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.field.UnknownField; import org.jabref.model.entry.types.EntryType; import org.jabref.model.entry.types.UnknownEntryType; import org.jabref.model.metadata.ContentSelector; @@ -25,6 +27,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -65,11 +70,11 @@ public void serializeSingleSaveAction() { @Test public void serializeSingleContentSelectors() { - List values = new ArrayList<>(4); - values.add("approved"); - values.add("captured"); - values.add("received"); - values.add("status"); + List values = List.of( + "approved", + "captured", + "received", + "status"); metaData.addContentSelector(new ContentSelector(StandardField.PUBSTATE, values)); @@ -96,4 +101,43 @@ void testParsingEmptyOptionalFieldsFieldsReturnsEmptyCollections() { Optional type = MetaDataParser.parseCustomEntryType(serialized); assertEquals(Collections.emptySet(), type.get().getOptionalFields()); } + + /** + * Code clone of {@link org.jabref.logic.importer.util.MetaDataParserTest#parseCustomizedEntryType()} + */ + public static Stream serializeCustomizedEntryType() { + return Stream.of( + Arguments.of( + new BibEntryTypeBuilder() + .withType(new UnknownEntryType("test")) + .withRequiredFields(StandardField.AUTHOR, StandardField.TITLE), + "jabref-entrytype: test: req[author;title] opt[]" + ), + Arguments.of( + new BibEntryTypeBuilder() + .withType(new UnknownEntryType("test")) + .withRequiredFields(StandardField.AUTHOR) + .withImportantFields(StandardField.TITLE), + "jabref-entrytype: test: req[author] opt[title]" + ), + Arguments.of( + new BibEntryTypeBuilder() + .withType(new UnknownEntryType("test")) + .withRequiredFields(UnknownField.fromDisplayName("Test1"), UnknownField.fromDisplayName("Test2")), + "jabref-entrytype: test: req[Test1;Test2] opt[]" + ), + Arguments.of( + new BibEntryTypeBuilder() + .withType(new UnknownEntryType("test")) + .withRequiredFields(UnknownField.fromDisplayName("tEST"), UnknownField.fromDisplayName("tEsT2")), + "jabref-entrytype: test: req[tEST;tEsT2] opt[]" + ) + ); + } + + @ParameterizedTest + @MethodSource + void serializeCustomizedEntryType(BibEntryTypeBuilder bibEntryTypeBuilder, String expected) { + assertEquals(expected, MetaDataSerializer.serializeCustomEntryTypes(bibEntryTypeBuilder.build())); + } } diff --git a/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java b/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java index 12e584e49c8..251b3d10279 100644 --- a/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java +++ b/src/test/java/org/jabref/logic/importer/util/MetaDataParserTest.java @@ -1,11 +1,21 @@ package org.jabref.logic.importer.util; +import java.util.Optional; +import java.util.stream.Stream; + +import org.jabref.logic.exporter.MetaDataSerializerTest; +import org.jabref.model.entry.BibEntryTypeBuilder; +import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.types.UnknownEntryType; + import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; -class MetaDataParserTest { +public class MetaDataParserTest { @ParameterizedTest @CsvSource({ @@ -20,4 +30,30 @@ class MetaDataParserTest { void parseDirectory(String expected, String input) { assertEquals(expected, MetaDataParser.parseDirectory(input)); } + + /** + * In case of any change, copy the content to {@link MetaDataSerializerTest#serializeCustomizedEntryType()} + */ + public static Stream parseCustomizedEntryType() { + return Stream.of( + Arguments.of( + new BibEntryTypeBuilder() + .withType(new UnknownEntryType("test")) + .withRequiredFields(UnknownField.fromDisplayName("Test1"), UnknownField.fromDisplayName("Test2")), + "jabref-entrytype: test: req[Test1;Test2] opt[]" + ), + Arguments.of( + new BibEntryTypeBuilder() + .withType(new UnknownEntryType("test")) + .withRequiredFields(UnknownField.fromDisplayName("tEST"), UnknownField.fromDisplayName("tEsT2")), + "jabref-entrytype: test: req[tEST;tEsT2] opt[]" + ) + ); + } + + @ParameterizedTest + @MethodSource + void parseCustomizedEntryType(BibEntryTypeBuilder expected, String source) { + assertEquals(Optional.of(expected.build()), MetaDataParser.parseCustomEntryType(source)); + } } diff --git a/src/test/java/org/jabref/model/entry/field/FieldFactoryTest.java b/src/test/java/org/jabref/model/entry/field/FieldFactoryTest.java index 33aa2e18555..f218e3f82d7 100644 --- a/src/test/java/org/jabref/model/entry/field/FieldFactoryTest.java +++ b/src/test/java/org/jabref/model/entry/field/FieldFactoryTest.java @@ -15,24 +15,27 @@ class FieldFactoryTest { @Test void testOrFieldsTwoTerms() { - assertEquals("aaa/bbb", FieldFactory.serializeOrFields(new UnknownField("aaa"), new UnknownField("bbb"))); + assertEquals("Aaa/Bbb", FieldFactory.serializeOrFields(new UnknownField("aaa"), new UnknownField("bbb"))); } @Test void testOrFieldsThreeTerms() { - assertEquals("aaa/bbb/ccc", FieldFactory.serializeOrFields(new UnknownField("aaa"), new UnknownField("bbb"), new UnknownField("ccc"))); + assertEquals("Aaa/Bbb/Ccc", FieldFactory.serializeOrFields(new UnknownField("aaa"), new UnknownField("bbb"), new UnknownField("ccc"))); } - private static Stream commentFields() { + private static Stream fieldsWithoutFieldProperties() { return Stream.of( + // comment fields Arguments.of(new UserSpecificCommentField("user1"), "comment-user1"), - Arguments.of(new UserSpecificCommentField("other-user-id"), "comment-other-user-id") + Arguments.of(new UserSpecificCommentField("other-user-id"), "comment-other-user-id"), + // unknown field + Arguments.of(new UnknownField("cased", "cAsEd"), "cAsEd") ); } @ParameterizedTest @MethodSource - void commentFields(Field expected, String name) { + void fieldsWithoutFieldProperties(Field expected, String name) { assertEquals(expected, FieldFactory.parseField(name)); } diff --git a/src/test/java/org/jabref/model/entry/field/UnknownFieldTest.java b/src/test/java/org/jabref/model/entry/field/UnknownFieldTest.java index 63afeef3e41..1b6ca0212c0 100644 --- a/src/test/java/org/jabref/model/entry/field/UnknownFieldTest.java +++ b/src/test/java/org/jabref/model/entry/field/UnknownFieldTest.java @@ -14,4 +14,10 @@ void fieldsConsideredEqualIfSameName() { void fieldsConsideredEqualINameDifferByCapitalization() { assertEquals(new UnknownField("tiTle"), new UnknownField("Title")); } + + @Test + void displayNameConstructor() { + UnknownField cAsED = UnknownField.fromDisplayName("cAsEd"); + assertEquals(new UnknownField("cased", "cAsEd"), cAsED); + } }