diff --git a/src/main/java/org/deepsymmetry/cratedigger/Database.java b/src/main/java/org/deepsymmetry/cratedigger/Database.java
index fcba4ad..572826d 100644
--- a/src/main/java/org/deepsymmetry/cratedigger/Database.java
+++ b/src/main/java/org/deepsymmetry/cratedigger/Database.java
@@ -1,7 +1,5 @@
package org.deepsymmetry.cratedigger;
-import io.kaitai.struct.KaitaiStruct;
-import io.kaitai.struct.RandomAccessFileKaitaiStream;
import org.apiguardian.api.API;
import org.deepsymmetry.cratedigger.pdb.RekordboxPdb;
import org.slf4j.Logger;
@@ -14,7 +12,7 @@
import java.util.*;
/**
- *
Parses rekordbox database export files, providing access to the information they contain.
+ * Parses rekordbox database {@code export.pdb} files, providing access to the information they contain.
*/
@API(status = API.Status.STABLE)
public class Database implements Closeable {
@@ -22,14 +20,9 @@ public class Database implements Closeable {
private static final Logger logger = LoggerFactory.getLogger(Database.class);
/**
- * Tracks whether we were configured to parse an {@code exportExt.pdb} file.
+ * Helper class to parse and interact with the database file conveniently.
*/
- private final boolean isExportExt;
-
- /**
- * Holds a reference to the parser for the file we were constructed with.
- */
- private final RekordboxPdb pdb;
+ private final DatabaseUtil databaseUtil;
/**
* Holds a reference to the file this database was constructed from.
@@ -39,8 +32,7 @@ public class Database implements Closeable {
/**
* Construct a database access instance from the specified recordbox export file.
* The file can obtained either from the SD or USB media, or directly from a player
- * using {@link FileFetcher#fetch(InetAddress, String, String, File)}. This version
- * of the constructor only handles {@code export.pdb} files.
+ * using {@link FileFetcher#fetch(InetAddress, String, String, File)}.
*
* Be sure to call {@link #close()} when you are done using the parsed database
* to close the underlying file or users will be unable to unmount the drive holding
@@ -52,64 +44,44 @@ public class Database implements Closeable {
*/
@API(status = API.Status.STABLE)
public Database(File sourceFile) throws IOException {
- this(sourceFile, false);
- }
-
- /**
- *
Construct a database access instance from the specified recordbox export file.
- * The file can obtained either from the SD or USB media, or directly from a player
- * using {@link FileFetcher#fetch(InetAddress, String, String, File)}.
- *
- * Be sure to call {@link #close()} when you are done using the parsed database
- * to close the underlying file or users will be unable to unmount the drive holding
- * it until they quit your program.
- *
- * @param sourceFile an export.pdb or exportExt.pdb file
- * @param isExportExt indicates which type of file is to be parsed
- *
- * @throws IOException if there is a problem reading the file
- */
- @API(status = API.Status.EXPERIMENTAL)
- public Database(File sourceFile, boolean isExportExt) throws IOException {
this.sourceFile = sourceFile;
- this.isExportExt = isExportExt;
- pdb = new RekordboxPdb(new RandomAccessFileKaitaiStream(sourceFile.getAbsolutePath()), isExportExt);
+ databaseUtil = new DatabaseUtil(sourceFile, false);
final SortedMap> mutableTrackTitleIndex = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
final SortedMap> mutableTrackArtistIndex = new TreeMap<>();
final SortedMap> mutableTrackAlbumIndex = new TreeMap<>();
final SortedMap> mutableTrackGenreIndex = new TreeMap<>();
trackIndex = indexTracks(mutableTrackTitleIndex, mutableTrackArtistIndex, mutableTrackAlbumIndex, mutableTrackGenreIndex);
- trackTitleIndex = freezeSecondaryIndex(mutableTrackTitleIndex);
- trackAlbumIndex = freezeSecondaryIndex(mutableTrackAlbumIndex);
- trackArtistIndex = freezeSecondaryIndex(mutableTrackArtistIndex);
- trackGenreIndex = freezeSecondaryIndex(mutableTrackGenreIndex);
+ trackTitleIndex = databaseUtil.freezeSecondaryIndex(mutableTrackTitleIndex);
+ trackAlbumIndex = databaseUtil.freezeSecondaryIndex(mutableTrackAlbumIndex);
+ trackArtistIndex = databaseUtil.freezeSecondaryIndex(mutableTrackArtistIndex);
+ trackGenreIndex = databaseUtil.freezeSecondaryIndex(mutableTrackGenreIndex);
final SortedMap> mutableArtistNameIndex = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
artistIndex = indexArtists(mutableArtistNameIndex);
- artistNameIndex = freezeSecondaryIndex(mutableArtistNameIndex);
+ artistNameIndex = databaseUtil.freezeSecondaryIndex(mutableArtistNameIndex);
final SortedMap> mutableColorNameIndex = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
colorIndex = indexColors(mutableColorNameIndex);
- colorNameIndex = freezeSecondaryIndex(mutableColorNameIndex);
+ colorNameIndex = databaseUtil.freezeSecondaryIndex(mutableColorNameIndex);
final SortedMap> mutableAlbumNameIndex = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
final SortedMap> mutableAlbumArtistIndex = new TreeMap<>();
albumIndex = indexAlbums(mutableAlbumNameIndex, mutableAlbumArtistIndex);
- albumNameIndex = freezeSecondaryIndex(mutableAlbumNameIndex);
- albumArtistIndex = freezeSecondaryIndex(mutableAlbumArtistIndex);
+ albumNameIndex = databaseUtil.freezeSecondaryIndex(mutableAlbumNameIndex);
+ albumArtistIndex = databaseUtil.freezeSecondaryIndex(mutableAlbumArtistIndex);
final SortedMap> mutableLabelNameIndex = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
labelIndex = indexLabels(mutableLabelNameIndex);
- labelNameIndex = freezeSecondaryIndex(mutableLabelNameIndex);
+ labelNameIndex = databaseUtil.freezeSecondaryIndex(mutableLabelNameIndex);
final SortedMap> mutableMusicalKeyNameIndex = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
musicalKeyIndex = indexKeys(mutableMusicalKeyNameIndex);
- musicalKeyNameIndex = freezeSecondaryIndex(mutableMusicalKeyNameIndex);
+ musicalKeyNameIndex = databaseUtil.freezeSecondaryIndex(mutableMusicalKeyNameIndex);
final SortedMap> mutableGenreNameIndex = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
genreIndex = indexGenres(mutableGenreNameIndex);
- genreNameIndex = freezeSecondaryIndex(mutableGenreNameIndex);
+ genreNameIndex = databaseUtil.freezeSecondaryIndex(mutableGenreNameIndex);
artworkIndex = indexArtwork();
@@ -120,96 +92,6 @@ public Database(File sourceFile, boolean isExportExt) throws IOException {
historyPlaylistNameIndex = indexHistoryPlaylistNames();
}
- /**
- * An interface used to process each row found in a table when traversing them to build our indices.
- * This allows the common code for traversing a table to be reused, while specializing the handling
- * of each kind of table's rows.
- */
- private interface RowHandler {
- /**
- * Each row found in a table being scanned will be passed to this function.
- *
- * @param row the row that has just been found
- */
- void rowFound(KaitaiStruct row);
- }
-
- /**
- * Parse and index all the rows found in a particular table. This method performs a scan of the
- * specified table, passing all rows that are encountered to an interface that knows what to do
- * with them.
- *
- * @param type the type of table to be scanned and parsed
- * @param handler the code that knows how to index that kind of row
- *
- * @throws IllegalStateException if there is more than (or less than) one table of that type in the file
- */
- private void indexRows(RekordboxPdb.PageType type, RowHandler handler) {
- boolean done = false;
- for (RekordboxPdb.Table table : pdb.tables()) {
- if (table.type() == type) {
- if (done) throw new IllegalStateException("More than one table found with type " + type);
- final long lastIndex = table.lastPage().index(); // This is how we know when to stop.
- RekordboxPdb.PageRef currentRef = table.firstPage();
- boolean moreLeft = true;
- do {
- // logger.info("Indexing page " + currentRef.index());
- final RekordboxPdb.Page page = currentRef.body();
-
- // Process only ordinary data pages.
- if (page.isDataPage()) {
- for (RekordboxPdb.RowGroup rowGroup : page.rowGroups()) {
- for (RekordboxPdb.RowRef rowRef : rowGroup.rows()) {
- if (rowRef.present()) {
- // We found a row, pass it to the handler to be indexed appropriately.
- handler.rowFound(isExportExt? rowRef.bodyExt() : rowRef.body());
- }
- }
- }
- }
-
- // Was this the final page in the table? If so, stop, otherwise, move on to the next page.
- if (currentRef.index() == lastIndex) {
- moreLeft = false;
- } else {
- currentRef = page.nextPage();
- }
- } while (moreLeft);
- done = true;
- }
- }
-
- if (!done) throw new IllegalStateException("No table found of type " + type);
- }
-
- /**
- * Adds a row ID to a secondary index which is sorted by some other attribute of the record (for example,
- * add a track id to the title index, so the track can be found by title).
- *
- * @param index the secondary index, which holds all the row IDs that have the specified key
- * @param key the secondary index value by which this row can be looked up
- * @param id the ID of the row to index under the specified key
- * @param the type of the key (often String, but may be Long, e.g. to index tracks by artist ID)
- */
- private void addToSecondaryIndex(SortedMap> index, K key, Long id) {
- SortedSet existingIds = index.computeIfAbsent(key, k -> new TreeSet<>());
- existingIds.add(id);
- }
-
- /**
- * Protects a secondary index against further changes once we have finished indexing all the rows that
- * are going in to it.
- *
- * @param index the index that should no longer be modified.
- * @param the type of the key (often String, but may be Long, e.g. to index tracks by artist ID)
- *
- * @return an unmodifiable top-level view of the unmodifiable children
- */
- private SortedMap> freezeSecondaryIndex(SortedMap> index) {
- index.replaceAll((k, v) -> Collections.unmodifiableSortedSet(index.get(k)));
- return Collections.unmodifiableSortedMap(index);
- }
-
/**
* A map from track ID to the actual track object. If this ends up taking too much space, it would be
* possible to reorganize the Kaitai Struct mapping specification so that rows are parse instances of
@@ -273,7 +155,7 @@ private Map indexTracks(final SortedMap> genreIndex) {
final Map index = new HashMap<>();
- indexRows(RekordboxPdb.PageType.TRACKS, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.TRACKS, row -> {
// We found a track; index it by its ID.
RekordboxPdb.TrackRow trackRow = (RekordboxPdb.TrackRow)row;
final long id = trackRow.id();
@@ -282,25 +164,25 @@ private Map indexTracks(final SortedMap 0) {
- addToSecondaryIndex(artistIndex, trackRow.artistId(), id);
+ databaseUtil.addToSecondaryIndex(artistIndex, trackRow.artistId(), id);
}
if (trackRow.composerId() > 0) {
- addToSecondaryIndex(artistIndex, trackRow.composerId(), id);
+ databaseUtil.addToSecondaryIndex(artistIndex, trackRow.composerId(), id);
}
if (trackRow.originalArtistId() > 0) {
- addToSecondaryIndex(artistIndex, trackRow.originalArtistId(), id);
+ databaseUtil.addToSecondaryIndex(artistIndex, trackRow.originalArtistId(), id);
}
if (trackRow.remixerId() > 0) {
- addToSecondaryIndex(artistIndex, trackRow.remixerId(), id);
+ databaseUtil.addToSecondaryIndex(artistIndex, trackRow.remixerId(), id);
}
if (trackRow.albumId() > 0) {
- addToSecondaryIndex(albumIndex, trackRow.albumId(), id);
+ databaseUtil.addToSecondaryIndex(albumIndex, trackRow.albumId(), id);
}
if (trackRow.genreId() > 0) {
- addToSecondaryIndex(genreIndex, trackRow.genreId(), id);
+ databaseUtil.addToSecondaryIndex(genreIndex, trackRow.genreId(), id);
}
});
@@ -331,7 +213,7 @@ private Map indexTracks(final SortedMap indexArtists(final SortedMap> nameIndex) {
final Map index = new HashMap<>();
- indexRows(RekordboxPdb.PageType.ARTISTS, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.ARTISTS, row -> {
RekordboxPdb.ArtistRow artistRow = (RekordboxPdb.ArtistRow)row;
final long id = artistRow.id();
index.put(id, artistRow);
@@ -339,7 +221,7 @@ private Map indexArtists(final SortedMap indexArtists(final SortedMap indexColors(final SortedMap> nameIndex) {
final Map index = new HashMap<>();
- indexRows(RekordboxPdb.PageType.COLORS, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.COLORS, row -> {
RekordboxPdb.ColorRow colorRow = (RekordboxPdb.ColorRow)row;
final long id = colorRow.id();
index.put(id, colorRow);
@@ -378,7 +260,7 @@ private Map indexColors(final SortedMap indexAlbums(final SortedMap> artistIndex) {
final Map index = new HashMap<>();
- indexRows(RekordboxPdb.PageType.ALBUMS, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.ALBUMS, row -> {
RekordboxPdb.AlbumRow albumRow = (RekordboxPdb.AlbumRow) row;
final long id = albumRow.id();
index.put(id, albumRow);
@@ -424,10 +306,10 @@ private Map indexAlbums(final SortedMap 0) {
- addToSecondaryIndex(artistIndex, albumRow.artistId(), id);
+ databaseUtil.addToSecondaryIndex(artistIndex, albumRow.artistId(), id);
}
});
@@ -458,7 +340,7 @@ private Map indexAlbums(final SortedMap indexLabels(final SortedMap> nameIndex) {
final Map index = new HashMap<>();
- indexRows(RekordboxPdb.PageType.LABELS, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.LABELS, row -> {
RekordboxPdb.LabelRow labelRow = (RekordboxPdb.LabelRow) row;
final long id = labelRow.id();
index.put(id, labelRow);
@@ -466,7 +348,7 @@ private Map indexLabels(final SortedMap indexLabels(final SortedMap indexKeys(final SortedMap> nameIndex) {
final Map index = new HashMap<>();
- indexRows(RekordboxPdb.PageType.KEYS, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.KEYS, row -> {
RekordboxPdb.KeyRow keyRow = (RekordboxPdb.KeyRow) row;
final long id = keyRow.id();
index.put(id, keyRow);
@@ -505,7 +387,7 @@ private Map indexKeys(final SortedMap indexKeys(final SortedMap indexGenres(final SortedMap> nameIndex) {
final Map index = new HashMap<>();
- indexRows(RekordboxPdb.PageType.GENRES, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.GENRES, row -> {
RekordboxPdb.GenreRow genreRow = (RekordboxPdb.GenreRow) row;
final long id = genreRow.id();
index.put(id, genreRow);
@@ -544,7 +426,7 @@ private Map indexGenres(final SortedMap indexGenres(final SortedMap indexArtwork() {
final Map index = new HashMap<>();
- indexRows(RekordboxPdb.PageType.ARTWORK, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.ARTWORK, row -> {
RekordboxPdb.ArtworkRow artworkRow = (RekordboxPdb.ArtworkRow) row;
index.put(artworkRow.id(), artworkRow);
});
@@ -634,7 +516,7 @@ public String toString() {
*/
private Map> indexPlaylists() {
final Map> result = new HashMap<>();
- indexRows(RekordboxPdb.PageType.PLAYLIST_ENTRIES, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.PLAYLIST_ENTRIES, row -> {
RekordboxPdb.PlaylistEntryRow entryRow = (RekordboxPdb.PlaylistEntryRow) row;
ArrayList playlist = (ArrayList) result.get(entryRow.playlistId());
if (playlist == null) {
@@ -659,7 +541,7 @@ private Map> indexPlaylists() {
*/
private Map> indexPlaylistFolders() {
final Map> result = new HashMap<>();
- indexRows(RekordboxPdb.PageType.PLAYLIST_TREE, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.PLAYLIST_TREE, row -> {
RekordboxPdb.PlaylistTreeRow treeRow = (RekordboxPdb.PlaylistTreeRow) row;
ArrayList parent = (ArrayList) result.get(treeRow.parentId());
if (parent == null) {
@@ -685,7 +567,7 @@ private Map> indexPlaylistFolders() {
*/
private SortedMap indexHistoryPlaylistNames() {
final SortedMap result = new TreeMap<>();
- indexRows(RekordboxPdb.PageType.HISTORY_PLAYLISTS, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.HISTORY_PLAYLISTS, row -> {
RekordboxPdb.HistoryPlaylistRow historyRow = (RekordboxPdb.HistoryPlaylistRow) row;
result.put(getText(historyRow.name()), historyRow.id());
});
@@ -700,7 +582,7 @@ private SortedMap indexHistoryPlaylistNames() {
*/
private Map> indexHistoryPlaylists() {
final Map> result = new HashMap<>();
- indexRows(RekordboxPdb.PageType.HISTORY_ENTRIES, row -> {
+ databaseUtil.indexRows(RekordboxPdb.PageType.HISTORY_ENTRIES, row -> {
RekordboxPdb.HistoryEntryRow entryRow = (RekordboxPdb.HistoryEntryRow) row;
ArrayList playList = (ArrayList) result.get(entryRow.playlistId());
if (playList == null) {
@@ -727,7 +609,7 @@ private Map> indexHistoryPlaylists() {
*/
@Override
public void close() throws IOException {
- pdb._io().close();
+ databaseUtil.close();
}
/**
diff --git a/src/main/java/org/deepsymmetry/cratedigger/DatabaseExt.java b/src/main/java/org/deepsymmetry/cratedigger/DatabaseExt.java
new file mode 100644
index 0000000..0e63905
--- /dev/null
+++ b/src/main/java/org/deepsymmetry/cratedigger/DatabaseExt.java
@@ -0,0 +1,56 @@
+package org.deepsymmetry.cratedigger;
+
+import org.apiguardian.api.API;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+
+/**
+ * Parses rekordbox database {@code exportExt.pdb} files, providing access to the information they contain.
+ */
+@API(status = API.Status.EXPERIMENTAL)
+public class DatabaseExt implements Closeable {
+
+ private static final Logger logger = LoggerFactory.getLogger(DatabaseExt.class);
+
+ /**
+ * Helper class to parse and interact with the database file conveniently.
+ */
+ private final DatabaseUtil databaseUtil;
+
+ /**
+ * Construct a database access instance from the specified recordbox export file.
+ * The file can obtained either from the SD or USB media, or directly from a player
+ * using {@link FileFetcher#fetch(InetAddress, String, String, File)}.
+ *
+ * Be sure to call {@link #close()} when you are done using the parsed database
+ * to close the underlying file or users will be unable to unmount the drive holding
+ * it until they quit your program.
+ *
+ * @param sourceFile an export.pdb file
+ *
+ * @throws IOException if there is a problem reading the file
+ */
+ @API(status = API.Status.EXPERIMENTAL)
+ public DatabaseExt(File sourceFile) throws IOException {
+ databaseUtil = new DatabaseUtil(sourceFile, true);
+ // TODO: Gather and index the tag information.
+ }
+
+ /**
+ * Close the file underlying the parsed database. This needs to be called if you want to be able
+ * to unmount the media on which that file resides, but once it is done, you can no longer access
+ * lazy elements within the database which have not already been parsed.
+ *
+ * @throws IOException if there is a problem closing the file
+ */
+ @Override
+ public void close() throws IOException {
+ databaseUtil.close();
+ }
+
+}
diff --git a/src/main/java/org/deepsymmetry/cratedigger/DatabaseUtil.java b/src/main/java/org/deepsymmetry/cratedigger/DatabaseUtil.java
new file mode 100644
index 0000000..b637eba
--- /dev/null
+++ b/src/main/java/org/deepsymmetry/cratedigger/DatabaseUtil.java
@@ -0,0 +1,158 @@
+package org.deepsymmetry.cratedigger;
+
+import io.kaitai.struct.KaitaiStruct;
+import io.kaitai.struct.RandomAccessFileKaitaiStream;
+import org.deepsymmetry.cratedigger.pdb.RekordboxPdb;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * Provides helpful utility functions for working with a rekordbox database export. Used by
+ * {@link Database} and {@link DatabaseExt}
+ */
+class DatabaseUtil implements Closeable {
+ /**
+ * Tracks whether we were configured to parse an {@code exportExt.pdb} file.
+ */
+ final boolean isExportExt;
+
+ /**
+ * Holds a reference to the parser for the file we were constructed with.
+ */
+ final RekordboxPdb pdb;
+
+ /**
+ * Holds a reference to the file this database was constructed from.
+ */
+ final File sourceFile;
+
+ /**
+ * Construct a database access helper from the specified recordbox export file.
+ * The file can obtained either from the SD or USB media, or directly from a player
+ * using {@link FileFetcher#fetch(InetAddress, String, String, File)}.
+ *
+ * Be sure to call {@link #close()} when you are done using the parsed database
+ * to close the underlying file or users will be unable to unmount the drive holding
+ * it until they quit your program.
+ *
+ * @param sourceFile an export.pdb file
+ * @param isExportExt indicates whether this is an ordinary export.pdb file or a newer exportExt.pdb file.
+ *
+ * @throws IOException if there is a problem reading the file
+ */
+ DatabaseUtil(File sourceFile, boolean isExportExt) throws IOException {
+ this.sourceFile = sourceFile;
+ this.isExportExt = isExportExt;
+ pdb = new RekordboxPdb(new RandomAccessFileKaitaiStream(sourceFile.getAbsolutePath()), isExportExt);
+ }
+
+ /**
+ * Close the file underlying the parsed database. This needs to be called if you want to be able
+ * to unmount the media on which that file resides, but once it is done, you can no longer access
+ * lazy elements within the database which have not already been parsed.
+ *
+ * @throws IOException if there is a problem closing the file
+ */
+ @Override
+ public void close() throws IOException {
+ pdb._io().close();
+ }
+
+ /**
+ * An interface used to process each row found in a table when traversing them to build our indices.
+ * This allows the common code for traversing a table to be reused, while specializing the handling
+ * of each kind of table's rows.
+ */
+ interface RowHandler {
+ /**
+ * Each row found in a table being scanned will be passed to this function.
+ *
+ * @param row the row that has just been found
+ */
+ void rowFound(KaitaiStruct row);
+ }
+
+ /**
+ * Parse and index all the rows found in a particular table. This method performs a scan of the
+ * specified table, passing all rows that are encountered to an interface that knows what to do
+ * with them.
+ *
+ * @param type the type of table to be scanned and parsed
+ * @param handler the code that knows how to index that kind of row
+ *
+ * @throws IllegalStateException if there is more than (or less than) one table of that type in the file
+ */
+ void indexRows(RekordboxPdb.PageType type, DatabaseUtil.RowHandler handler) {
+ boolean done = false;
+ for (RekordboxPdb.Table table : pdb.tables()) {
+ if (table.type() == type) {
+ if (done) throw new IllegalStateException("More than one table found with type " + type);
+ final long lastIndex = table.lastPage().index(); // This is how we know when to stop.
+ RekordboxPdb.PageRef currentRef = table.firstPage();
+ boolean moreLeft = true;
+ do {
+ // logger.info("Indexing page " + currentRef.index());
+ final RekordboxPdb.Page page = currentRef.body();
+
+ // Process only ordinary data pages.
+ if (page.isDataPage()) {
+ for (RekordboxPdb.RowGroup rowGroup : page.rowGroups()) {
+ for (RekordboxPdb.RowRef rowRef : rowGroup.rows()) {
+ if (rowRef.present()) {
+ // We found a row, pass it to the handler to be indexed appropriately.
+ handler.rowFound(isExportExt? rowRef.bodyExt() : rowRef.body());
+ }
+ }
+ }
+ }
+
+ // Was this the final page in the table? If so, stop, otherwise, move on to the next page.
+ if (currentRef.index() == lastIndex) {
+ moreLeft = false;
+ } else {
+ currentRef = page.nextPage();
+ }
+ } while (moreLeft);
+ done = true;
+ }
+ }
+
+ if (!done) throw new IllegalStateException("No table found of type " + type);
+ }
+
+ /**
+ * Adds a row ID to a secondary index which is sorted by some other attribute of the record (for example,
+ * add a track id to the title index, so the track can be found by title).
+ *
+ * @param index the secondary index, which holds all the row IDs that have the specified key
+ * @param key the secondary index value by which this row can be looked up
+ * @param id the ID of the row to index under the specified key
+ * @param the type of the key (often String, but may be Long, e.g. to index tracks by artist ID)
+ */
+ void addToSecondaryIndex(SortedMap> index, K key, Long id) {
+ SortedSet existingIds = index.computeIfAbsent(key, k -> new TreeSet<>());
+ existingIds.add(id);
+ }
+
+ /**
+ * Protects a secondary index against further changes once we have finished indexing all the rows that
+ * are going in to it.
+ *
+ * @param index the index that should no longer be modified.
+ * @param the type of the key (often String, but may be Long, e.g. to index tracks by artist ID)
+ *
+ * @return an unmodifiable top-level view of the unmodifiable children
+ */
+ SortedMap> freezeSecondaryIndex(SortedMap> index) {
+ index.replaceAll((k, v) -> Collections.unmodifiableSortedSet(index.get(k)));
+ return Collections.unmodifiableSortedMap(index);
+ }
+
+}