diff --git a/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzJSONReader.java b/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzJSONReader.java index 4774d9d..f27365e 100644 --- a/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzJSONReader.java +++ b/src/main/java/com/CDPrintable/MusicBrainzResources/MusicBrainzJSONReader.java @@ -17,8 +17,12 @@ public class MusicBrainzJSONReader { private final JsonObject json; - private final String na = "n/a"; + /* + * Creates a MusicBrainzJSONReader from a JSON string. + * @param json The JSON string. + * @throws IllegalArgumentException If the JSON is invalid. + */ public MusicBrainzJSONReader(String json) throws IllegalArgumentException { JsonObject tempJsonObject; try { @@ -30,33 +34,51 @@ public MusicBrainzJSONReader(String json) throws IllegalArgumentException { this.json = tempJsonObject; } + /** + * Parses a JSON array and creates a new array of the same type as the provided array. + * + * @param The type of the array elements. + * @param key The JSON key to look for (e.g., "releases", "cdstubs"). + * @param processor A functional interface to process each {@link JsonObject} in the JSON array + * and convert it into an object of type {@code T}. + * @param array An example array of type {@code T[]} used to determine the type of the output array. + * @return A new array of type {@code T[]} containing the processed elements from the JSON array. + * If the key does not exist or the JSON array is empty, an empty array is returned. + */ @SuppressWarnings("unchecked") private T[] parseJsonArray(String key, JsonArrayProcessor processor, T[] array) { + // Make sure the key exists in the JSON object if (!json.has(key)) { + // Return an empty array if the JSON object does not have the key array = (T[]) Array.newInstance(array.getClass().getComponentType(), 0); return array; } + // Make a JSON array using the provided key JsonArray jsonArray = json.getAsJsonArray(key); array = (T[]) Array.newInstance(array.getClass().getComponentType(), jsonArray.size()); + // Process each JSON object in the array for (int i = 0; i < jsonArray.size(); i++) { JsonObject jsonObject = jsonArray.get(i).getAsJsonObject(); - array[i] = processor.process(jsonObject); + array[i] = processor.process(jsonObject); // Uses provided processor to process the JSON object } return array; } - /* + /** * Gets releases from the JSON. * @return An array of the releases. */ public MusicBrainzRelease[] getReleases() { return parseJsonArray("releases", jsonObject -> { + // Get the title, date, track count, and id from the JSON object + // If the value does not exist in JSON, n/a will be returned String title = jsonHasAndIsNotNull(jsonObject, "title") ? jsonObject.get("title").getAsString() : null; String date = jsonHasAndIsNotNull(jsonObject, "date") ? jsonObject.get("date").getAsString() : null; int trackCount = jsonHasAndIsNotNull(jsonObject, "track-count") ? jsonObject.get("track-count").getAsInt() : -1; String id = jsonHasAndIsNotNull(jsonObject, "id") ? jsonObject.get("id").getAsString() : null; + // Get all artists as a String[] JsonArray artistsArray = jsonObject.getAsJsonArray("artist-credit"); String[] artists = new String[artistsArray.size()]; for (int j = 0; j < artistsArray.size(); j++) { @@ -69,34 +91,49 @@ public MusicBrainzRelease[] getReleases() { }, new MusicBrainzRelease[0]); } + /** + * Gets CD stubs from the JSON. + * @return An array of the CD stubs. + */ public MusicBrainzCDStub[] getCDStubs() { return parseJsonArray("cdstubs", jsonObject -> { + // Read the title, track count, and id from the JSON object + // Also applies n/a should the value not exist in JSON String title = jsonHasAndIsNotNull(jsonObject, "title") ? jsonObject.get("title").getAsString() : null; String id = jsonHasAndIsNotNull(jsonObject, "id") ? jsonObject.get("id").getAsString() : null; int trackCount = jsonHasAndIsNotNull(jsonObject, "count") ? jsonObject.get("count").getAsInt() : -1; String artist = jsonHasAndIsNotNull(jsonObject, "artist") ? jsonObject.get("artist").getAsString() : null; + // Keep in mind that CDStubs only have one artist return new MusicBrainzCDStub(id, title, new String[] {artist}, trackCount); }, new MusicBrainzCDStub[0]); } - /* + /** * Creates a table model from an array of items. * @param items The array of items. Usually a MusicBrainzRelease, MusicBrainzCDStub, etc. * @param columnNames The names of the columns. * @param extractor The extractor that extracts the data from the item. */ private DefaultTableModel createTableModel(Object[] items, String[] columnNames, DataExtractor extractor) { + // Make sure that the array is not null or empty if (items == null || items.length == 0) { + // If so, return an empty table model with column names return new DefaultTableModel(new String[0][0], columnNames); } String[][] data = new String[items.length][columnNames.length]; + // Use the extractor provided to extract the data from the item for (int i = 0; i < items.length; i++) { data[i] = extractor.extractData(items[i]); } return new DefaultTableModel(data, columnNames); } + /** + * Gets the releases as a table model. + * @param releaseArray The array of releases. + * @return The table model. + */ public DefaultTableModel getReleasesAsTableModel(MusicBrainzRelease[] releaseArray) { String[] columnNames = {"Release Name", "Artist", "Track Count", "Date", ""}; return createTableModel(releaseArray, columnNames, item -> { @@ -111,6 +148,11 @@ public DefaultTableModel getReleasesAsTableModel(MusicBrainzRelease[] releaseArr }); } + /** + * Gets the CD stubs as a table model. + * @param cdStubArray The array of CD stubs. + * @return The table model. + */ public DefaultTableModel getCDStubsAsTableModel(MusicBrainzCDStub[] cdStubArray) { String[] columnNames = {"Disc Name", "Artist", "Track Count", ""}; return createTableModel(cdStubArray, columnNames, item -> { @@ -124,22 +166,40 @@ public DefaultTableModel getCDStubsAsTableModel(MusicBrainzCDStub[] cdStubArray) }); } + /** + * Functional interface for extracting data from an item. + */ @FunctionalInterface private interface DataExtractor { String[] extractData(Object item); } + /** + * Functional interface for processing a JSON object. + * @param The type of the object to be processed. + */ @FunctionalInterface private interface JsonArrayProcessor { T process(JsonObject jsonObject); } + /** + * Checks if a JSON object has a member and is not null. + * @param jsonObject The JSON object. + * @param memberName The member name to check for. + * @return True if the member exists and is not null, false otherwise. + */ private boolean jsonHasAndIsNotNull(JsonObject jsonObject, String memberName) { return jsonObject.has(memberName) && !jsonObject.get(memberName).isJsonNull(); } + /** + * Gets a value or returns "n/a" if the value is null. + * @param value The value to check. + * @return The value or "n/a" if the value is null. + */ private String getOrDefault(String value) { - return value != null ? value : na; + return value != null ? value : "n/a"; } @Override diff --git a/src/main/java/com/CDPrintable/ProgramWindow.java b/src/main/java/com/CDPrintable/ProgramWindow.java index 1f5a21f..7b38284 100644 --- a/src/main/java/com/CDPrintable/ProgramWindow.java +++ b/src/main/java/com/CDPrintable/ProgramWindow.java @@ -20,11 +20,15 @@ import javax.swing.event.DocumentListener; import javax.swing.table.DefaultTableModel; import java.awt.*; +import java.util.Objects; public class ProgramWindow { private final UserAgent userAgent; private JLabel fullUserAgentLabel = new JLabel(); + /** + * Creates a new ProgramWindow and sets up the GUI. + */ public ProgramWindow() { userAgent = new UserAgent("CDPrintable/" + Constants.VERSION, "example@example.com"); @@ -47,6 +51,11 @@ public ProgramWindow() { // Set the frame to be visible frame.setVisible(true); } + + /** + * Gets a JPanel for the table panel. This is a helper method. + * @return A JPanel with the table panel. + */ private JPanel tablePanel() { JPanel panel = new JPanel(new BorderLayout()); @@ -60,6 +69,11 @@ private JPanel tablePanel() { return panel; } + + /** + * Gets a JPanel for the search panel. This is a helper method. + * @return A JPanel with the search panel. + */ private JPanel searchPanel() { JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); @@ -79,32 +93,37 @@ private JPanel searchPanel() { // CD Search Panel set up JPanel cdSearchPanel = new JPanel(); cdSearchPanel.setBorder(BorderFactory.createTitledBorder("Search")); + JTextField searchField = new JTextField(15); + JComboBox searchTypeComboBox = new JComboBox<>(new String[] {"CDStub", "Artist", "Release"}); + + // Search button and event listener setup JButton searchButton = new JButton("Search"); - searchButton.addActionListener(e -> { + searchButton.addActionListener(_ -> { if (searchTypeComboBox.getSelectedItem() == null) { return; } if (searchTypeComboBox.getSelectedItem().equals("CDStub")) { - searchTable.setModel(getCDStubModel()); MusicBrainzJSONReader reader = sendRequest("cdstub", searchField.getText()); - + // Get CDStubs and set the table model MusicBrainzCDStub[] cdStubs = reader.getCDStubs(); searchTable.setModel(reader.getCDStubsAsTableModel(cdStubs)); } else if (searchTypeComboBox.getSelectedItem().equals("Artist")) { - searchTable.setModel(getArtistModel()); + searchTable.setModel(getArtistModel()); // Default model } else if (searchTypeComboBox.getSelectedItem().equals("Release")) { - searchTable.setModel(getReleaseModel()); MusicBrainzJSONReader reader = sendRequest("release", searchField.getText()); + // Get Releases and set the table model MusicBrainzRelease[] releases = reader.getReleases(); searchTable.setModel(reader.getReleasesAsTableModel(releases)); } else { + // how does this even happen JOptionPane.showMessageDialog(panel, "Please select a search type."); } }); + cdSearchPanel.setLayout(new FlowLayout()); cdSearchPanel.add(searchTypeComboBox); cdSearchPanel.add(searchField); @@ -116,6 +135,12 @@ private JPanel searchPanel() { return panel; } + /** + * Sends a request to the MusicBrainz API. + * @param queryType The query type to send. E.g. "cdstub", "release", etc. + * @param query The query to send. + * @return a MusicBrainzJSONReader object with the response JSON already in it. + */ private MusicBrainzJSONReader sendRequest(String queryType, String query) { MusicBrainzRequest request = new MusicBrainzRequest(queryType, query); WebRequest webRequest = new WebRequest(request, userAgent); @@ -128,31 +153,47 @@ private MusicBrainzJSONReader sendRequest(String queryType, String query) { There was a fatal error when sending the request. Please try again or submit an issue on GitHub. Here are some things to try: • Check your internet connection. - • Remove any special characters from your query."""); + • Remove any special characters from your query.""", "CDPrintable Severe Error", JOptionPane.ERROR_MESSAGE); } - MusicBrainzJSONReader reader = new MusicBrainzJSONReader(response); - return reader; + return new MusicBrainzJSONReader(Objects.requireNonNullElse(response, "")); + } + /** + * Gets a default table model for CDStubs. + * @return A DefaultTableModel with the correct columns. + */ private DefaultTableModel getCDStubModel() { String[] columnNames = {"Disc Name", "Artist", "Track Count", ""}; String[][] data = {{"", "", "", ""}}; return new javax.swing.table.DefaultTableModel(data, columnNames); } + /** + * Gets a default table model for Artists. + * @return A DefaultTableModel with the correct columns. + */ private DefaultTableModel getArtistModel() { String[] columnNames = {"Artist Name", "Date Organised", ""}; String[][] data = {{"", "", ""}}; return new javax.swing.table.DefaultTableModel(data, columnNames); } + /** + * Gets a default table model for Releases. + * @return A DefaultTableModel with the correct columns. + */ private DefaultTableModel getReleaseModel() { String[] columnNames = {"Release Name", "Artist", "Track Count", "Date", ""}; String[][] data = {{"", "", "", ""}}; return new javax.swing.table.DefaultTableModel(data, columnNames); } + /** + * Gets a JPanel for settings. This is another helper method. + * @return A JPanel with the settings window. + */ private JPanel settingsPanel() { JPanel panel = new JPanel(new GridLayout(1, 2)); GridBagConstraints gbc = new GridBagConstraints(); @@ -163,6 +204,7 @@ private JPanel settingsPanel() { JPanel userAgentPanel = new JPanel(new BorderLayout()); userAgentPanel.setBorder(BorderFactory.createTitledBorder("User Agent")); + // Setup user agent text fields, labels, and document listeners JLabel userAgentLabel = new JLabel("User Agent:"); JTextField userAgentField = new JTextField(15); userAgentField.setText(userAgent.getUserAgent()); @@ -181,11 +223,11 @@ public void removeUpdate(DocumentEvent e) { public void changedUpdate(DocumentEvent e) {} // Not used }); + // Set up the user agent field with labels and document listener. JLabel userAgentEmailLabel = new JLabel("User Agent Email:"); JTextField userAgentEmailField = new JTextField(15); userAgentEmailField.setText(userAgent.getUserAgentEmail()); userAgentEmailField.getDocument().addDocumentListener(new DocumentListener() { - @Override public void insertUpdate(DocumentEvent e) { userAgent.setUserAgentEmail(userAgentEmailField.getText(), fullUserAgentLabel); diff --git a/src/test/java/MusicBrainzObjectTests.java b/src/test/java/MusicBrainzObjectTests.java index 7724c5c..cf0d9b3 100644 --- a/src/test/java/MusicBrainzObjectTests.java +++ b/src/test/java/MusicBrainzObjectTests.java @@ -14,9 +14,13 @@ import static org.junit.jupiter.api.Assertions.*; 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"); + /** + * Test that the JSON reader can handle an invalid JSON string. + */ @Test public void invalidJsonString() { MusicBrainzJSONReader reader = new MusicBrainzJSONReader(""); @@ -28,6 +32,10 @@ public void invalidJsonString() { assertEquals(0, releases.length); } + /** + * Test that the JSON reader can handle an empty JSON string as well as invalid JSON. + * This might happen if the database is down. + */ @ParameterizedTest @CsvSource({ "{\"invalidKey\": []}, releases", @@ -49,6 +57,11 @@ void testGetItemsWithInvalidJsonStructure(String jsonString, String key) { } } + /** + * Test that the JSON reader can handle a JSON string with missing or null fields. + * This is a very common occurrence, however the server typically would return an + * empty String instead of null. + */ @ParameterizedTest @CsvSource({ "{\"releases\": [{\"title\": null, \"date\": null, \"count\": null, \"id\": null, \"artist-credit\": []}]}, releases", @@ -79,16 +92,26 @@ void testGetItemsWithMissingOrNullFields(String jsonString, String key) { } } + /** + * Test the JSON reader against array issues. There are many cases tested here. + * Case 1: (CDStubs and Releases) The array is null. + * Case 2: (CDStubs and Releases) The array is empty. + * Case 3: (CDStubs and Releases) The array is still cooked but contains a null field. + * Case 4: (CDStubs and Releases) The array is still cooked but contains an invalid key. + * @param key The array key to test. (releases or cdstubs) (more to come) + * @param isNull Whether the array is null or not. + * @param readerJson The JSON string to test. + */ @ParameterizedTest @CsvSource({ - "releases, true, null", - "releases, false, {}", - "cdstubs, true, null", - "cdstubs, false, {}", - "releases, false, {\"releases\": null}", - "cdstubs, false, {\"cdstubs\": null}", - "releases, false, {\"invalidKey\": []}", - "cdstubs, false, {\"invalidKey\": []}" + "releases, true, null", // case 1 + "releases, false, {}", // case 2 + "cdstubs, true, null", // case 1 + "cdstubs, false, {}", // case 2 + "releases, false, {\"releases\": null}", // case 3 + "cdstubs, false, {\"cdstubs\": null}", // case 3 + "releases, false, {\"invalidKey\": []}", // case 4 + "cdstubs, false, {\"invalidKey\": []}" // case 4 }) void testGetTableModelWithArrayIssues(String key, boolean isNull, String readerJson) { MusicBrainzJSONReader reader = new MusicBrainzJSONReader(readerJson != null ? readerJson : ""); @@ -107,6 +130,11 @@ void testGetTableModelWithArrayIssues(String key, boolean isNull, String readerJ } } + /** + * Test the JSON reader against the example JSON files. This is effectively + * normal operation for the program. + * This test is for Releases. + */ @Test void genericGetReleasesTest() { MusicBrainzJSONReader reader = new MusicBrainzJSONReader(releasesJson); @@ -130,6 +158,11 @@ void genericGetReleasesTest() { assertEquals("846ee5f9-ad18-4f3a-a883-43ec58ca0805", releases[1].getId()); } + /** + * Test the JSON reader against the example JSON files. This is effectively + * normal operation for the program. + * This test is for CDStubs. + */ @SuppressWarnings("SpellCheckingInspection") @Test void genericGetCDStubsTest() { @@ -152,6 +185,11 @@ void genericGetCDStubsTest() { assertEquals("KZn02eYalzdXJNbtmuz2xKDzLZU-", cdStubs[1].getId()); } + /** + * 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 Releases. + */ @Test void genericGetReleasesAsTableModelTest() { MusicBrainzJSONReader reader = new MusicBrainzJSONReader(releasesJson); @@ -175,6 +213,11 @@ void genericGetReleasesAsTableModelTest() { assertEquals("", tableModel.getValueAt(1, 4)); } + /** + * 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 CDStubs. + */ @Test void genericGetCDStubsAsTableModelTest() { MusicBrainzJSONReader reader = new MusicBrainzJSONReader(cdStubsJson); @@ -196,6 +239,12 @@ void genericGetCDStubsAsTableModelTest() { assertEquals("", tableModel.getValueAt(1, 3)); } + /** + * Read a file and return its contents as a String. + * This is a helper method used for the example JSON files. + * @param filePath The path to the file. + * @return The contents of the file as a String. + */ private String readFile(String filePath) { try { return new String(Files.readAllBytes(Paths.get(filePath)));