diff --git a/src/main/java/com/CDPrintable/Constants.java b/src/main/java/com/CDPrintable/Constants.java index ad43b98..7714e80 100644 --- a/src/main/java/com/CDPrintable/Constants.java +++ b/src/main/java/com/CDPrintable/Constants.java @@ -12,7 +12,7 @@ public class Constants { // MAJOR MINOR PATCH - public static final String VERSION = "1.5.3"; + public static final String VERSION = "1.6.4"; public static final boolean USER_AGENT_EMAIL_CHANGED = false; } diff --git a/src/main/java/com/CDPrintable/MusicBrainzResources/Gender.java b/src/main/java/com/CDPrintable/MusicBrainzResources/Gender.java index 7810078..1052cbb 100644 --- a/src/main/java/com/CDPrintable/MusicBrainzResources/Gender.java +++ b/src/main/java/com/CDPrintable/MusicBrainzResources/Gender.java @@ -14,6 +14,22 @@ public enum Gender { MALE, FEMALE, NON_BINARY, - OTHER, - UNKNOWN + UNKNOWN; + + /** + * Converts a string to a Gender enum. + * @param genderString The input string. + * @return The corresponding Gender enum, or UNKNOWN if no match is found. + */ + public static Gender fromString(String genderString) { + if (genderString == null) { + return UNKNOWN; + } + return switch (genderString.toLowerCase()) { + case "male" -> MALE; + case "female" -> FEMALE; + case "non-binary", "nonbinary" -> NON_BINARY; + default -> UNKNOWN; + }; + } } \ No newline at end of file diff --git a/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzArtist.java b/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzArtist.java index aec6475..45e44e5 100644 --- a/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzArtist.java +++ b/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzArtist.java @@ -12,34 +12,40 @@ public class MusicBrainzArtist { private String name; - private String dateOrganized; + private String dateOrganized; // May only be present for groups + private String birthDate; // May only be present for people private String id; private String sortName; private Gender gender; private String type; // Band, Person, etc. private String disambiguation; private String lifeSpan; + private String country; - public MusicBrainzArtist(String name, String dateOrganized, String id, String sortName, Gender gender, String type, String disambiguation, String lifeSpan) { + public MusicBrainzArtist(String name, String dateOrganized, String birthDate, String id, String sortName, Gender gender, String type, String disambiguation, String lifeSpan, String country) { this.name = name; this.dateOrganized = dateOrganized; + this.birthDate = birthDate; this.id = id; this.sortName = sortName; this.gender = gender; this.type = type; this.disambiguation = disambiguation; this.lifeSpan = lifeSpan; + this.country = country; } public MusicBrainzArtist() { this.name = ""; this.dateOrganized = ""; + this.birthDate = ""; this.id = ""; this.sortName = ""; this.gender = Gender.UNKNOWN; this.type = ""; this.disambiguation = ""; this.lifeSpan = ""; + this.country = ""; } public String getName() { @@ -105,4 +111,20 @@ public String getLifeSpan() { public void setLifeSpan(String lifeSpan) { this.lifeSpan = lifeSpan; } + + public String getBirthDate() { + return birthDate; + } + + public void setBirthDate(String birthDate) { + this.birthDate = birthDate; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } } \ No newline at end of file diff --git a/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzJSONReader.java b/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzJSONReader.java index f27365e..403c85d 100644 --- a/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzJSONReader.java +++ b/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzJSONReader.java @@ -12,6 +12,8 @@ import com.google.gson.*; +import java.util.Locale; + import javax.swing.table.DefaultTableModel; import java.lang.reflect.Array; @@ -109,6 +111,94 @@ public MusicBrainzCDStub[] getCDStubs() { }, new MusicBrainzCDStub[0]); } + /** + * Gets Artists from the JSON. + * @return An array of the artists. + */ + public MusicBrainzArtist[] getArtists() { + return parseJsonArray("artists", jsonObject -> { + String name = null; + if (jsonHasAndIsNotNull(jsonObject, "name")) { + JsonElement nameElement = jsonObject.get("name"); + if (nameElement != null && !nameElement.isJsonNull()) { + name = nameElement.getAsString(); + } + } + String type = jsonHasAndIsNotNull(jsonObject, "type") ? jsonObject.get("type").getAsString() : null; + + // THE CODE BELOW MAY CAUSE FUTURE BUGS, THOUGH UNLIKELY. + // Keep in mind that the life-span.begin JSON for artists is + // either the date organized or the birthdate, depending on if + // the artist is a group or a person. + String birthDate; + String organizedDate; + if (jsonHasAndIsNotNull(jsonObject, "life-span")) { + JsonObject lifeSpanObject = jsonObject.getAsJsonObject("life-span"); + if("Group".equals(type)) { + organizedDate = jsonHasAndIsNotNull(lifeSpanObject, "begin") ? lifeSpanObject.get("begin").getAsString() : null; + birthDate = null; + } else if ("Person".equals(type)) { + birthDate = jsonHasAndIsNotNull(lifeSpanObject, "begin") ? lifeSpanObject.get("begin").getAsString() : null; + organizedDate = null; + } else { + birthDate = null; + organizedDate = null; + } + } else { + birthDate = null; + organizedDate = null; + } + + String id = jsonHasAndIsNotNull(jsonObject, "id") ? jsonObject.get("id").getAsString() : null; + String sortName = jsonHasAndIsNotNull(jsonObject, "sort-name") ? jsonObject.get("sort-name").getAsString() : null; + // Groups do not have genders in this API. + Gender gender = Gender.fromString(jsonHasAndIsNotNull(jsonObject, "gender") ? jsonObject.get("gender").getAsString() : null); + + String disambiguation; + // The code below MAY also be problematic, although VERY unlikely. + if (jsonHasAndIsNotNull(jsonObject, "tags")) { + JsonArray tagsArray = jsonObject.getAsJsonArray("tags"); + if (!tagsArray.isEmpty()) { + JsonObject firstTagObject = tagsArray.get(0).getAsJsonObject(); + disambiguation = jsonHasAndIsNotNull(firstTagObject, "name") ? firstTagObject.get("name").getAsString() : null; + int highestCount = 0; + for (JsonElement tagElement : tagsArray) { + JsonObject tagObject = tagElement.getAsJsonObject(); + int count = jsonHasAndIsNotNull(tagObject, "count") ? tagObject.get("count").getAsInt() : -1; + if (count > highestCount) { + highestCount = count; + disambiguation = jsonHasAndIsNotNull(tagObject, "name") ? tagObject.get("name").getAsString() : null; + } + } + } else { + disambiguation = null; + } + } else { + disambiguation = null; + } + + String lifeSpan; + if (jsonHasAndIsNotNull(jsonObject, "life-span")) { + JsonObject lifeSpanObject = jsonObject.getAsJsonObject("life-span"); + if (jsonHasAndIsNotNull(lifeSpanObject, "begin") && jsonHasAndIsNotNull(lifeSpanObject, "end")) { + lifeSpan = lifeSpanObject.get("begin").getAsString() + " - " + lifeSpanObject.get("end").getAsString(); + } else { + lifeSpan = null; + } + } else { + lifeSpan = null; + } + + String country = jsonHasAndIsNotNull(jsonObject, "country") ? jsonObject.get("country").getAsString() : null; + if (country != null) { + Locale locale = new Locale.Builder().setRegion(country).build(); + country = locale.getDisplayCountry(); + } + + return new MusicBrainzArtist(name, organizedDate, birthDate, id, sortName, gender, type, disambiguation, lifeSpan, country); + }, new MusicBrainzArtist[0]); + } + /** * Creates a table model from an array of items. * @param items The array of items. Usually a MusicBrainzRelease, MusicBrainzCDStub, etc. @@ -166,6 +256,30 @@ public DefaultTableModel getCDStubsAsTableModel(MusicBrainzCDStub[] cdStubArray) }); } + /** + * Gets the artists as a table model. + * @param artistArray The array of artists. + * @return The table model. + */ + public DefaultTableModel getArtistsAsTableModel(MusicBrainzArtist[] artistArray) { + String[] columnNames = {"Name", "Date Organised", "Birthdate", "Sort Name", "Gender", "Type", "Disambiguation", "Life Span", "Country", ""}; + return createTableModel(artistArray, columnNames, item -> { + MusicBrainzArtist artist = (MusicBrainzArtist) item; + return new String[] { + getOrDefault(artist.getName()), + getOrDefault(artist.getDateOrganized()), + getOrDefault(artist.getBirthDate()), + getOrDefault(artist.getSortName()), + getOrDefault(artist.getGender().toString().toLowerCase(Locale.ROOT)), + getOrDefault(artist.getType()), + getOrDefault(artist.getDisambiguation()), + getOrDefault(artist.getLifeSpan()), + getOrDefault(artist.getCountry()), + "" + }; + }); + } + /** * Functional interface for extracting data from an item. */ @@ -199,7 +313,7 @@ private boolean jsonHasAndIsNotNull(JsonObject jsonObject, String memberName) { * @return The value or "n/a" if the value is null. */ private String getOrDefault(String value) { - return value != null ? value : "n/a"; + return value != null ? value : ""; } @Override diff --git a/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzRequest.java b/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzRequest.java index c25c1c8..f7edc54 100644 --- a/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzRequest.java +++ b/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzRequest.java @@ -70,7 +70,9 @@ public String buildRequestURL() { url.append(otherParams); } - return url.toString().replace(" ", "%20"); + return url.toString() + .replace(" ", "%20") + .replace("\"", "%22"); } /** diff --git a/src/main/java/com/CDPrintable/ProgramWindow.java b/src/main/java/com/CDPrintable/ProgramWindow.java index 7b38284..cc6bee0 100644 --- a/src/main/java/com/CDPrintable/ProgramWindow.java +++ b/src/main/java/com/CDPrintable/ProgramWindow.java @@ -10,15 +10,13 @@ package com.CDPrintable; -import com.CDPrintable.MusicBrainzResources.MusicBrainzCDStub; -import com.CDPrintable.MusicBrainzResources.MusicBrainzJSONReader; -import com.CDPrintable.MusicBrainzResources.MusicBrainzRelease; -import com.CDPrintable.MusicBrainzResources.MusicBrainzRequest; +import com.CDPrintable.MusicBrainzResources.*; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableColumn; import java.awt.*; import java.util.Objects; @@ -111,7 +109,11 @@ private JPanel searchPanel() { MusicBrainzCDStub[] cdStubs = reader.getCDStubs(); searchTable.setModel(reader.getCDStubsAsTableModel(cdStubs)); } else if (searchTypeComboBox.getSelectedItem().equals("Artist")) { - searchTable.setModel(getArtistModel()); // Default model + MusicBrainzJSONReader reader = sendRequest("artist", searchField.getText()); + + // Get Artists and set the table model + MusicBrainzArtist[] artists = reader.getArtists(); + searchTable.setModel(reader.getArtistsAsTableModel(artists)); } else if (searchTypeComboBox.getSelectedItem().equals("Release")) { MusicBrainzJSONReader reader = sendRequest("release", searchField.getText()); @@ -122,6 +124,7 @@ private JPanel searchPanel() { // how does this even happen JOptionPane.showMessageDialog(panel, "Please select a search type."); } + resizeColumnWidths(searchTable); }); cdSearchPanel.setLayout(new FlowLayout()); @@ -175,8 +178,8 @@ private DefaultTableModel getCDStubModel() { * @return A DefaultTableModel with the correct columns. */ private DefaultTableModel getArtistModel() { - String[] columnNames = {"Artist Name", "Date Organised", ""}; - String[][] data = {{"", "", ""}}; + String[] columnNames = {"Name", "Date Organised", "Birthdate", "Sort Name", "Gender", "Type", "Disambiguation", "Life Span", "Country", ""}; + String[][] data = {{"", "", "", "", "", "", "", ""}}; return new javax.swing.table.DefaultTableModel(data, columnNames); } @@ -289,4 +292,25 @@ public void changedUpdate(DocumentEvent e) {} // Not used return panel; } + + /** + * Helper method to resize a tables columns to fit the largest element. + * @param table The table to resize. + */ + private void resizeColumnWidths(JTable table) { + for (int column = 0; column < table.getColumnCount(); column++) { + TableColumn tableColumn = table.getColumnModel().getColumn(column); + int preferredWidth = table.getTableHeader().getDefaultRenderer() + .getTableCellRendererComponent(table, tableColumn.getHeaderValue(), false, false, -1, column) + .getPreferredSize().width; + + for (int row = 0; row < table.getRowCount(); row++) { + Component cellRenderer = table.getCellRenderer(row, column) + .getTableCellRendererComponent(table, table.getValueAt(row, column), false, false, row, column); + preferredWidth = Math.max(preferredWidth, cellRenderer.getPreferredSize().width); + } + + tableColumn.setPreferredWidth(preferredWidth + 2); // Add padding + } + } } \ No newline at end of file diff --git a/src/test/java/MusicBrainzObjectTests.java b/src/test/java/MusicBrainzObjectTests.java index cf0d9b3..89c3f4a 100644 --- a/src/test/java/MusicBrainzObjectTests.java +++ b/src/test/java/MusicBrainzObjectTests.java @@ -1,6 +1,4 @@ -import com.CDPrintable.MusicBrainzResources.MusicBrainzCDStub; -import com.CDPrintable.MusicBrainzResources.MusicBrainzJSONReader; -import com.CDPrintable.MusicBrainzResources.MusicBrainzRelease; +import com.CDPrintable.MusicBrainzResources.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -17,6 +15,7 @@ public class MusicBrainzObjectTests { // Read example JSON files private final String releasesJson = readFile("src/test/resources/ReleaseExample.json"); private final String cdStubsJson = readFile("src/test/resources/CDStubExample.json"); + private final String artistsJson = readFile("src/test/resources/ArtistExample.json"); /** * Test that the JSON reader can handle an invalid JSON string. @@ -41,7 +40,9 @@ public void invalidJsonString() { "{\"invalidKey\": []}, releases", "{\"releases\": []}, releases", "{\"invalidKey\": []}, cdstubs", - "{\"cdstubs\": []}, cdstubs" + "{\"cdstubs\": []}, cdstubs", + "{\"invalidKey\": []}, artists", + "{\"artists\": []}, artists" }) void testGetItemsWithInvalidJsonStructure(String jsonString, String key) { MusicBrainzJSONReader reader = new MusicBrainzJSONReader(jsonString); @@ -65,7 +66,8 @@ void testGetItemsWithInvalidJsonStructure(String jsonString, String key) { @ParameterizedTest @CsvSource({ "{\"releases\": [{\"title\": null, \"date\": null, \"count\": null, \"id\": null, \"artist-credit\": []}]}, releases", - "{\"cdstubs\": [{\"title\": null, \"id\": null, \"count\": null, \"artist\": null}]}, cdstubs" + "{\"cdstubs\": [{\"title\": null, \"id\": null, \"count\": null, \"artist\": null}]}, cdstubs", + "{\"artists\": [{\"name\": null, \"sort-name\": null, \"country\": null, \"gender\": null, \"disambiguation\": null, \"life-span\": null}]}, artists" }) void testGetItemsWithMissingOrNullFields(String jsonString, String key) { MusicBrainzJSONReader reader = new MusicBrainzJSONReader(jsonString); @@ -108,10 +110,14 @@ void testGetItemsWithMissingOrNullFields(String jsonString, String key) { "releases, false, {}", // case 2 "cdstubs, true, null", // case 1 "cdstubs, false, {}", // case 2 + "artists, true, null", // case 1 + "artists, false, {}", // case 2 "releases, false, {\"releases\": null}", // case 3 "cdstubs, false, {\"cdstubs\": null}", // case 3 + "artists, false, {\"artists\": null}", // case 3 "releases, false, {\"invalidKey\": []}", // case 4 - "cdstubs, false, {\"invalidKey\": []}" // case 4 + "cdstubs, false, {\"invalidKey\": []}", // case 4 + "artists, false, {\"invalidKey\": []}" // case 4 }) void testGetTableModelWithArrayIssues(String key, boolean isNull, String readerJson) { MusicBrainzJSONReader reader = new MusicBrainzJSONReader(readerJson != null ? readerJson : ""); @@ -185,6 +191,44 @@ void genericGetCDStubsTest() { assertEquals("KZn02eYalzdXJNbtmuz2xKDzLZU-", cdStubs[1].getId()); } + /** + * Test the JSON reader against the example JSON files. This is effectively + * normal operation for the program. + * This test is for Artists. + */ + @SuppressWarnings("SpellCheckingInspection") + @Test + void genericGetArtistsTest() { + MusicBrainzJSONReader reader = new MusicBrainzJSONReader(artistsJson); + MusicBrainzArtist[] artists = reader.getArtists(); + + assertNotNull(artists); + assertEquals(2, artists.length); + + assertEquals("Tears for Fears", artists[0].getName()); + assertEquals("Tears for Fears", artists[0].getSortName()); + assertEquals("1981", artists[0].getDateOrganized()); + assertNull(artists[0].getBirthDate()); + assertEquals("7c7f9c94-dee8-4903-892b-6cf44652e2de", artists[0].getId()); + assertEquals(Gender.UNKNOWN, artists[0].getGender()); + assertEquals("Group", artists[0].getType()); + assertEquals("new wave", artists[0].getDisambiguation()); + assertNull(artists[0].getLifeSpan()); + assertEquals("United Kingdom", artists[0].getCountry()); + + assertEquals("The Kid LAROI", artists[1].getName()); + assertEquals("Kid Laroi, The", artists[1].getSortName()); + assertNull(artists[1].getDateOrganized()); + assertEquals("2003-08-16", artists[1].getBirthDate()); + assertEquals("80609a00-b394-4a49-975b-2db6b543fa97", artists[1].getId()); + assertEquals(Gender.MALE, artists[1].getGender()); + assertEquals("Person", artists[1].getType()); + assertEquals("hip hop", artists[1].getDisambiguation()); + assertNull(artists[1].getLifeSpan()); + assertEquals("Australia", artists[1].getCountry()); + + } + /** * Tests the JSON reader again, but requests a table model and tests that instead. * This is effectively normal operation for the program. @@ -209,7 +253,7 @@ void genericGetReleasesAsTableModelTest() { assertEquals("Nights Like This", tableModel.getValueAt(1, 0)); assertEquals("Kaylee Bell", tableModel.getValueAt(1, 1)); assertEquals("11", tableModel.getValueAt(1, 2)); - assertEquals("n/a", tableModel.getValueAt(1, 3)); + assertEquals("", tableModel.getValueAt(1, 3)); assertEquals("", tableModel.getValueAt(1, 4)); } @@ -239,6 +283,44 @@ void genericGetCDStubsAsTableModelTest() { assertEquals("", tableModel.getValueAt(1, 3)); } + /** + * Tests the JSON reader again, but requests a table model and tests that instead. + * This is effectively normal operation for the program. + * This test is for Artists. + */ + @Test + void genericGetArtistsAsTableModelTest() { + MusicBrainzJSONReader reader = new MusicBrainzJSONReader(artistsJson); + MusicBrainzArtist[] artists = reader.getArtists(); + DefaultTableModel tableModel = reader.getArtistsAsTableModel(artists); + + assertNotNull(tableModel); + assertEquals(2, tableModel.getRowCount()); + assertEquals(10, tableModel.getColumnCount()); + + assertEquals("Tears for Fears", tableModel.getValueAt(0, 0)); + assertEquals("1981", tableModel.getValueAt(0, 1)); + assertEquals("", tableModel.getValueAt(0, 2)); + assertEquals("Tears for Fears", tableModel.getValueAt(0, 3)); + assertEquals("unknown", tableModel.getValueAt(0, 4)); + assertEquals("Group", tableModel.getValueAt(0, 5)); + assertEquals("new wave", tableModel.getValueAt(0, 6)); + assertEquals("", tableModel.getValueAt(0, 7)); + assertEquals("United Kingdom", tableModel.getValueAt(0, 8)); + assertEquals("", tableModel.getValueAt(0, 9)); + + assertEquals("The Kid LAROI", tableModel.getValueAt(1, 0)); + assertEquals("", tableModel.getValueAt(1, 1)); + assertEquals("2003-08-16", tableModel.getValueAt(1, 2)); + assertEquals("Kid Laroi, The", tableModel.getValueAt(1, 3)); + assertEquals("male", tableModel.getValueAt(1, 4)); + assertEquals("Person", tableModel.getValueAt(1, 5)); + assertEquals("hip hop", tableModel.getValueAt(1, 6)); + assertEquals("", tableModel.getValueAt(1, 7)); + assertEquals("Australia", tableModel.getValueAt(1, 8)); + assertEquals("", tableModel.getValueAt(1, 9)); + } + /** * Read a file and return its contents as a String. * This is a helper method used for the example JSON files.