From 38ec96a8e63b3f79b332e37a570b119267523872 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 15 May 2023 10:23:54 +0200 Subject: [PATCH 1/5] Remove empty entries automatically --- CHANGELOG.md | 3 +- src/main/java/org/jabref/gui/Globals.java | 1 + src/main/java/org/jabref/gui/JabRefFrame.java | 56 +------------------ src/main/java/org/jabref/gui/LibraryTab.java | 2 +- .../logic/exporter/BibDatabaseWriter.java | 6 +- .../model/database/BibDatabaseContext.java | 9 --- .../java/org/jabref/model/entry/BibEntry.java | 4 ++ src/main/resources/l10n/JabRef_en.properties | 10 ---- .../model/database/BibDatabaseTest.java | 20 ------- .../org/jabref/model/entry/BibEntryTest.java | 28 ++++++++++ 10 files changed, 42 insertions(+), 97 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f28f26a5b63..241d249a730 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We moved the preferences option to open the last edited files on startup to the 'General' tab. [#9808](https://github.com/JabRef/jabref/pull/9808) - We split the 'Import and Export' tab into 'Web Search' and 'Export'. [#9839](https://github.com/JabRef/jabref/pull/9839) - We improved the recognition of DOIs when pasting a link containing a DOI on the maintable [#9864](https://github.com/JabRef/jabref/issues/9864s) +- In case the library contains empty entries, they are not written to disk. ### Fixed @@ -365,7 +366,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve ### Added -- We added confirmation dialog when user wants to close a library where any empty entires are detected. [#8096](https://github.com/JabRef/jabref/issues/8096) +- We added confirmation dialog when user wants to close a library where any empty entries are detected. [#8096](https://github.com/JabRef/jabref/issues/8096) - We added import support for CFF files. [#7945](https://github.com/JabRef/jabref/issues/7945) - We added the option to copy the DOI of an entry directly from the context menu copy submenu. [#7826](https://github.com/JabRef/jabref/issues/7826) - We added a fulltext search feature. [#2838](https://github.com/JabRef/jabref/pull/2838) diff --git a/src/main/java/org/jabref/gui/Globals.java b/src/main/java/org/jabref/gui/Globals.java index 6070283d1d1..15c60e83754 100644 --- a/src/main/java/org/jabref/gui/Globals.java +++ b/src/main/java/org/jabref/gui/Globals.java @@ -43,6 +43,7 @@ public class Globals { public static final BuildInfo BUILD_INFO = new BuildInfo(); public static final RemoteListenerServerManager REMOTE_LISTENER = new RemoteListenerServerManager(); + /** * Manager for the state of the GUI. */ diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index ae9d1fac857..cd2d1e6b8cf 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -472,13 +472,6 @@ public boolean quit() { for (int i = 0; i < tabbedPane.getTabs().size(); i++) { LibraryTab libraryTab = getLibraryTabAt(i); final BibDatabaseContext context = libraryTab.getBibDatabaseContext(); - - if (context.hasEmptyEntries()) { - if (!confirmEmptyEntry(libraryTab, context)) { - return false; - } - } - if (libraryTab.isModified() && (context.getLocation() == DatabaseLocation.LOCAL)) { tabbedPane.getSelectionModel().select(i); if (!confirmClose(libraryTab)) { @@ -1157,7 +1150,7 @@ public void addTab(LibraryTab libraryTab, boolean raisePanel) { /** * Opens a new tab with existing data. - * Asynchronous loading is done at {@link #createLibraryTab(BackgroundTask, Path, PreferencesService, StateManager, JabRefFrame, ThemeManager)}. + * Asynchronous loading is done at {@link org.jabref.gui.LibraryTab#createLibraryTab(BackgroundTask, Path, PreferencesService, StateManager, JabRefFrame, ThemeManager)}. */ public LibraryTab addTab(BibDatabaseContext databaseContext, boolean raisePanel) { Objects.requireNonNull(databaseContext); @@ -1242,48 +1235,6 @@ private boolean confirmClose(LibraryTab libraryTab) { return false; } - /** - * Ask if the user really wants to remove any empty entries - */ - private Boolean confirmEmptyEntry(LibraryTab libraryTab, BibDatabaseContext context) { - String filename = libraryTab.getBibDatabaseContext() - .getDatabasePath() - .map(Path::toAbsolutePath) - .map(Path::toString) - .orElse(Localization.lang("untitled")); - - ButtonType deleteEmptyEntries = new ButtonType(Localization.lang("Delete empty entries"), ButtonBar.ButtonData.YES); - ButtonType keepEmptyEntries = new ButtonType(Localization.lang("Keep empty entries"), ButtonBar.ButtonData.NO); - ButtonType cancel = new ButtonType(Localization.lang("Return to library"), ButtonBar.ButtonData.CANCEL_CLOSE); - - Optional response = dialogService.showCustomButtonDialogAndWait(Alert.AlertType.CONFIRMATION, - Localization.lang("Empty entries"), - Localization.lang("Library '%0' has empty entries. Do you want to delete them?", filename), - deleteEmptyEntries, keepEmptyEntries, cancel); - if (response.isPresent() && response.get().equals(deleteEmptyEntries)) { - // The user wants to delete. - try { - for (BibEntry currentEntry : new ArrayList<>(context.getEntries())) { - if (currentEntry.getFields().isEmpty()) { - context.getDatabase().removeEntries(Collections.singletonList(currentEntry)); - } - } - SaveDatabaseAction saveAction = new SaveDatabaseAction(libraryTab, prefs, Globals.entryTypesManager); - if (saveAction.save()) { - return true; - } - // The action was either canceled or unsuccessful. - dialogService.notify(Localization.lang("Unable to save library")); - } catch (Throwable ex) { - LOGGER.error("A problem occurred when trying to delete the empty entries", ex); - dialogService.showErrorDialogAndWait(Localization.lang("Delete empty entries"), Localization.lang("Could not delete empty entries."), ex); - } - // Save was cancelled or an error occurred. - return false; - } - return !response.get().equals(cancel); - } - private void closeTab(LibraryTab libraryTab) { // empty tab without database if (libraryTab == null) { @@ -1291,11 +1242,6 @@ private void closeTab(LibraryTab libraryTab) { } final BibDatabaseContext context = libraryTab.getBibDatabaseContext(); - if (context.hasEmptyEntries()) { - if (!confirmEmptyEntry(libraryTab, context)) { - return; - } - } if (libraryTab.isModified() && (context.getLocation() == DatabaseLocation.LOCAL)) { if (confirmClose(libraryTab)) { diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index c3a06b8c56b..5b5b5f863f6 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -820,7 +820,7 @@ public void resetChangedProperties() { /** * Creates a new library tab. Contents are loaded by the {@code dataLoadingTask}. Most of the other parameters are required by {@code resetChangeMonitor()}. * - * @param dataLoadingTask The task to execute to load the data. It is executed using {@link Globals.TASK_EXECUTOR}. + * @param dataLoadingTask The task to execute to load the data. It is executed using {@link org.jabref.gui.Globals.TASK_EXECUTOR}. * @param file the path to the file (loaded by the dataLoadingTask) */ public static LibraryTab createLibraryTab(BackgroundTask dataLoadingTask, Path file, PreferencesService preferencesService, StateManager stateManager, JabRefFrame frame, ThemeManager themeManager) { diff --git a/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java b/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java index 7cb6affd9af..6a9a679c622 100644 --- a/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java +++ b/src/main/java/org/jabref/logic/exporter/BibDatabaseWriter.java @@ -171,7 +171,11 @@ public List getSaveActionsFieldChanges() { * Saves the complete database. */ public void saveDatabase(BibDatabaseContext bibDatabaseContext) throws IOException { - savePartOfDatabase(bibDatabaseContext, bibDatabaseContext.getDatabase().getEntries()); + List entries = bibDatabaseContext.getDatabase().getEntries() + .stream() + .filter(entry -> !entry.isEmpty()) + .toList(); + savePartOfDatabase(bibDatabaseContext, entries); } /** diff --git a/src/main/java/org/jabref/model/database/BibDatabaseContext.java b/src/main/java/org/jabref/model/database/BibDatabaseContext.java index 48c7262ce02..d6a800fc120 100644 --- a/src/main/java/org/jabref/model/database/BibDatabaseContext.java +++ b/src/main/java/org/jabref/model/database/BibDatabaseContext.java @@ -239,15 +239,6 @@ public List getEntries() { return database.getEntries(); } - /** - * check if the database has any empty entries - * - * @return true if the database has any empty entries; otherwise false - */ - public boolean hasEmptyEntries() { - return this.getEntries().stream().anyMatch(entry -> entry.getFields().isEmpty()); - } - public static Path getFulltextIndexBasePath() { return Path.of(AppDirsFactory.getInstance().getUserDataDir(OS.APP_DIR_APP_NAME, SearchFieldConstants.VERSION, OS.APP_DIR_APP_AUTHOR)); } diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index 9fb07029cf1..bdd8d10d43f 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -1157,4 +1157,8 @@ public void mergeWith(BibEntry other, Set otherPrioritizedFields) { } } } + + public boolean isEmpty() { + return this.fields.isEmpty(); + } } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 1ff18de0240..ab5677b5990 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -1,15 +1,5 @@ Proxy\ requires\ password=Proxy requires password -Could\ not\ delete\ empty\ entries.=Could not delete empty entries. - -Delete\ empty\ entries=Delete empty entries - -Empty\ entries=Empty entries - -Keep\ empty\ entries=Keep empty entries - -Library\ '%0'\ has\ empty\ entries.\ Do\ you\ want\ to\ delete\ them?=Library '%0' has empty entries. Do you want to delete them? - Unable\ to\ monitor\ file\ changes.\ Please\ close\ files\ and\ processes\ and\ restart.\ You\ may\ encounter\ errors\ if\ you\ continue\ with\ this\ session.=Unable to monitor file changes. Please close files and processes and restart. You may encounter errors if you continue with this session. %0\ contains\ the\ regular\ expression\ %1=%0 contains the regular expression %1 diff --git a/src/test/java/org/jabref/model/database/BibDatabaseTest.java b/src/test/java/org/jabref/model/database/BibDatabaseTest.java index 29bb885a9c3..28e9db06298 100644 --- a/src/test/java/org/jabref/model/database/BibDatabaseTest.java +++ b/src/test/java/org/jabref/model/database/BibDatabaseTest.java @@ -16,7 +16,6 @@ import org.jabref.model.entry.field.UnknownField; import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.event.EventListenerTest; -import org.jabref.model.metadata.MetaData; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,25 +36,6 @@ void setUp() { database = new BibDatabase(); } - @Test - void noEmptyEntry() { - BibEntry entry = new BibEntry(); - entry.setField(StandardField.AUTHOR, "#AAA#"); - database.insertEntry(entry); - BibDatabaseContext bibDatabaseContext = new BibDatabaseContext(database, new MetaData()); - assertEquals(false, bibDatabaseContext.hasEmptyEntries()); - } - - @Test - void withEmptyEntry() { - BibEntry entry = new BibEntry(); - database.insertEntry(entry); - BibDatabaseContext bibDatabaseContext = new BibDatabaseContext(database, new MetaData()); - assertEquals(true, bibDatabaseContext.hasEmptyEntries()); - bibDatabaseContext.getDatabase().removeEntries(Collections.singletonList(entry)); - assertEquals(Collections.emptyList(), bibDatabaseContext.getEntries()); - } - @Test void insertEntryAddsEntryToEntriesList() { BibEntry entry = new BibEntry(); diff --git a/src/test/java/org/jabref/model/entry/BibEntryTest.java b/src/test/java/org/jabref/model/entry/BibEntryTest.java index 784f5f03431..a6c5f734e0d 100644 --- a/src/test/java/org/jabref/model/entry/BibEntryTest.java +++ b/src/test/java/org/jabref/model/entry/BibEntryTest.java @@ -9,6 +9,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.jabref.model.FieldChange; import org.jabref.model.database.BibDatabase; @@ -25,6 +26,8 @@ import com.google.common.collect.Sets; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -809,4 +812,29 @@ void mergeEntriesWithOverlapAndPriorityGivenToOverlappingField() { copyEntry.mergeWith(otherEntry, otherPrioritizedFields); assertEquals(expected.getFields(), copyEntry.getFields()); } + + @Test + void isEmptyPlain() { + BibEntry entry = new BibEntry(); + assertTrue(entry.isEmpty()); + } + + @Test + void isEmptyTypeSet() { + BibEntry entry = new BibEntry(StandardEntryType.Book); + assertTrue(entry.isEmpty()); + } + + public static Stream isNotEmpty() { + return Stream.of( + new BibEntry().withCitationKey("test"), + new BibEntry().withField(StandardField.AUTHOR, "test") + ); + } + + @ParameterizedTest + @MethodSource + void isNotEmpty(BibEntry entry) { + assertFalse(entry.isEmpty()); + } } From 6937c0a7122cf37b0a74e215da31e6f1753f4272 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 15 May 2023 10:42:46 +0200 Subject: [PATCH 2/5] Make test customized entry non-empty --- .../org/jabref/logic/exporter/BibtexDatabaseWriterTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java b/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java index 72740b6d6c7..a66f7db6d86 100644 --- a/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java +++ b/src/test/java/org/jabref/logic/exporter/BibtexDatabaseWriterTest.java @@ -362,13 +362,15 @@ void writeEntryWithCustomizedTypeAlsoWritesTypeDeclaration() throws Exception { new OrFields(StandardField.AUTHOR), new OrFields(StandardField.DATE))); entryTypesManager.addCustomOrModifiedType(customizedBibType, BibDatabaseMode.BIBTEX); - BibEntry entry = new BibEntry(customizedType); + BibEntry entry = new BibEntry(customizedType).withCitationKey("key"); + // needed to get a proper serialization + entry.setChanged(true); database.insertEntry(entry); bibtexContext.setMode(BibDatabaseMode.BIBTEX); databaseWriter.saveDatabase(bibtexContext); - assertEquals("@Customizedtype{," + OS.NEWLINE + "}" + OS.NEWLINE + OS.NEWLINE + assertEquals("@Customizedtype{key," + OS.NEWLINE + "}" + OS.NEWLINE + OS.NEWLINE + "@Comment{jabref-meta: databaseType:bibtex;}" + OS.NEWLINE + OS.NEWLINE + "@Comment{jabref-entrytype: customizedtype: req[title;author;date] opt[year;month;publisher]}" + OS.NEWLINE, From 300c511c73b0390c3951e2dd227a6dc40f59e8f9 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 15 May 2023 11:05:14 +0200 Subject: [PATCH 3/5] Empty entries are ignored when comparing libraries --- .../jabref/logic/bibtex/comparator/BibDatabaseDiff.java | 9 +++++++-- src/main/java/org/jabref/model/database/BibDatabase.java | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java b/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java index cad1eea0a5b..7645ed85d1c 100644 --- a/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java +++ b/src/main/java/org/jabref/logic/bibtex/comparator/BibDatabaseDiff.java @@ -21,7 +21,7 @@ public class BibDatabaseDiff { private final List bibStringDiffs; private final List entryDiffs; - private BibDatabaseDiff(BibDatabaseContext originalDatabase, BibDatabaseContext newDatabase) { + private BibDatabaseDiff(BibDatabaseContext originalDatabase, BibDatabaseContext newDatabase, boolean includeEmptyEntries) { metaDataDiff = MetaDataDiff.compare(originalDatabase.getMetaData(), newDatabase.getMetaData()).orElse(null); preambleDiff = PreambleDiff.compare(originalDatabase, newDatabase).orElse(null); bibStringDiffs = BibStringDiff.compare(originalDatabase.getDatabase(), newDatabase.getDatabase()); @@ -31,6 +31,11 @@ private BibDatabaseDiff(BibDatabaseContext originalDatabase, BibDatabaseContext List originalEntriesSorted = originalDatabase.getDatabase().getEntriesSorted(comparator); List newEntriesSorted = newDatabase.getDatabase().getEntriesSorted(comparator); + if (!includeEmptyEntries) { + originalEntriesSorted.removeIf(BibEntry::isEmpty); + newEntriesSorted.removeIf(BibEntry::isEmpty); + } + entryDiffs = compareEntries(originalEntriesSorted, newEntriesSorted, originalDatabase.getMode()); } @@ -110,7 +115,7 @@ private static boolean hasEqualCitationKey(BibEntry oneEntry, BibEntry twoEntry) } public static BibDatabaseDiff compare(BibDatabaseContext base, BibDatabaseContext changed) { - return new BibDatabaseDiff(base, changed); + return new BibDatabaseDiff(base, changed, false); } public Optional getMetaDataDifferences() { diff --git a/src/main/java/org/jabref/model/database/BibDatabase.java b/src/main/java/org/jabref/model/database/BibDatabase.java index 301a9e7f7ce..a9bd3af43a0 100644 --- a/src/main/java/org/jabref/model/database/BibDatabase.java +++ b/src/main/java/org/jabref/model/database/BibDatabase.java @@ -112,7 +112,7 @@ public boolean hasEntries() { /** * Returns the list of entries sorted by the given comparator. */ - public synchronized List getEntriesSorted(Comparator comparator) { + public List getEntriesSorted(Comparator comparator) { List entriesSorted = new ArrayList<>(entries); entriesSorted.sort(comparator); From 557a014edbc53b3cacf053f28e17815c547177af Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 15 May 2023 11:32:46 +0200 Subject: [PATCH 4/5] Also treat entries containing automatic fields as empty --- .../java/org/jabref/model/entry/BibEntry.java | 5 +++- .../model/entry/field/StandardField.java | 2 ++ .../org/jabref/model/entry/BibEntryTest.java | 23 +++++++++++++------ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index bdd8d10d43f..54650cf95cf 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -1159,6 +1159,9 @@ public void mergeWith(BibEntry other, Set otherPrioritizedFields) { } public boolean isEmpty() { - return this.fields.isEmpty(); + if (this.fields.isEmpty()) { + return true; + } + return StandardField.AUTOMATIC_FIELDS.containsAll(this.getFields()); } } diff --git a/src/main/java/org/jabref/model/entry/field/StandardField.java b/src/main/java/org/jabref/model/entry/field/StandardField.java index 6012b38b596..2ca594674fa 100644 --- a/src/main/java/org/jabref/model/entry/field/StandardField.java +++ b/src/main/java/org/jabref/model/entry/field/StandardField.java @@ -137,6 +137,8 @@ public enum StandardField implements Field { CREATIONDATE("creationdate", FieldProperty.DATE), MODIFICATIONDATE("modificationdate", FieldProperty.DATE); + public static Set AUTOMATIC_FIELDS = Set.of(OWNER, TIMESTAMP, CREATIONDATE, MODIFICATIONDATE); + private final String name; private final String displayName; private final Set properties; diff --git a/src/test/java/org/jabref/model/entry/BibEntryTest.java b/src/test/java/org/jabref/model/entry/BibEntryTest.java index a6c5f734e0d..b913030cc73 100644 --- a/src/test/java/org/jabref/model/entry/BibEntryTest.java +++ b/src/test/java/org/jabref/model/entry/BibEntryTest.java @@ -813,15 +813,24 @@ void mergeEntriesWithOverlapAndPriorityGivenToOverlappingField() { assertEquals(expected.getFields(), copyEntry.getFields()); } - @Test - void isEmptyPlain() { - BibEntry entry = new BibEntry(); - assertTrue(entry.isEmpty()); + public static Stream isEmpty() { + return Stream.of( + new BibEntry(), + new BibEntry(StandardEntryType.Book), + new BibEntry().withField(StandardField.OWNER, "test"), + new BibEntry().withField(StandardField.CREATIONDATE, "test"), + new BibEntry() + .withField(StandardField.OWNER, "test") + .withField(StandardField.CREATIONDATE, "test"), + // source: https://github.com/JabRef/jabref/issues/8645 + new BibEntry() + .withField(StandardField.OWNER, "mlep") + .withField(StandardField.CREATIONDATE, "2022-04-05T10:41:54")); } - @Test - void isEmptyTypeSet() { - BibEntry entry = new BibEntry(StandardEntryType.Book); + @ParameterizedTest + @MethodSource + void isEmpty(BibEntry entry) { assertTrue(entry.isEmpty()); } From 6507265d90eb3f71e369e0a7c7ddd145e5da1362 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Mon, 15 May 2023 12:16:46 +0200 Subject: [PATCH 5/5] Add link to issue --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 241d249a730..cae8a596d99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,7 +48,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We moved the preferences option to open the last edited files on startup to the 'General' tab. [#9808](https://github.com/JabRef/jabref/pull/9808) - We split the 'Import and Export' tab into 'Web Search' and 'Export'. [#9839](https://github.com/JabRef/jabref/pull/9839) - We improved the recognition of DOIs when pasting a link containing a DOI on the maintable [#9864](https://github.com/JabRef/jabref/issues/9864s) -- In case the library contains empty entries, they are not written to disk. +- In case the library contains empty entries, they are not written to disk. [#8645](https://github.com/JabRef/jabref/issues/8645) ### Fixed