Skip to content

Commit 33a9894

Browse files
authored
[bosesoundtouch] Fix parsing of metadata fields (openhab#16898)
XML text content was not processed correctly in case the value contained XML entities. Signed-off-by: David Pace <[email protected]>
1 parent b8c04ae commit 33a9894

File tree

4 files changed

+156
-29
lines changed

4 files changed

+156
-29
lines changed

bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/XMLResponseHandler.java

+61-29
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ public class XMLResponseHandler extends DefaultHandler {
7272

7373
private Map<Integer, ContentItem> playerPresets;
7474

75+
/**
76+
* String builder to collect text content.
77+
* <p>
78+
* Background: {@code characters()} may be called multiple times for the same
79+
* text content in case there are entities like {@code &apos;} inside the
80+
* content.
81+
*/
82+
private StringBuilder textContent = new StringBuilder();
83+
7584
/**
7685
* Creates a new instance of this class
7786
*
@@ -96,6 +105,7 @@ public void startElement(@Nullable String uri, @Nullable String localName, @Null
96105
state = XMLHandlerState.Unprocessed; // set default value; we avoid default in select to have the compiler
97106
// showing a
98107
// warning for unhandled states
108+
textContent = new StringBuilder();
99109

100110
switch (curState) {
101111
case INIT:
@@ -475,6 +485,27 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
475485
case Group:
476486
handler.handleGroupUpdated(masterDeviceId);
477487
break;
488+
case NowPlayingAlbum:
489+
updateNowPlayingAlbum(new StringType(textContent.toString()));
490+
break;
491+
case NowPlayingArtist:
492+
updateNowPlayingArtist(new StringType(textContent.toString()));
493+
break;
494+
case NowPlayingDescription:
495+
updateNowPlayingDescription(new StringType(textContent.toString()));
496+
break;
497+
case NowPlayingGenre:
498+
updateNowPlayingGenre(new StringType(textContent.toString()));
499+
break;
500+
case NowPlayingStationLocation:
501+
updateNowPlayingStationLocation(new StringType(textContent.toString()));
502+
break;
503+
case NowPlayingStationName:
504+
updateNowPlayingStationName(new StringType(textContent.toString()));
505+
break;
506+
case NowPlayingTrack:
507+
updateNowPlayingTrack(new StringType(textContent.toString()));
508+
break;
478509
default:
479510
// no actions...
480511
break;
@@ -483,8 +514,11 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
483514

484515
@Override
485516
public void characters(char[] ch, int start, int length) throws SAXException {
486-
logger.trace("{}: Text data during {}: '{}'", handler.getDeviceName(), state, new String(ch, start, length));
517+
String string = new String(ch, start, length);
518+
logger.trace("{}: Text data during {}: '{}'", handler.getDeviceName(), state, string);
519+
487520
super.characters(ch, start, length);
521+
488522
switch (state) {
489523
case INIT:
490524
case Msg:
@@ -507,8 +541,7 @@ public void characters(char[] ch, int start, int length) throws SAXException {
507541
case Zone:
508542
case ZoneUpdated:
509543
case Sources:
510-
logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state,
511-
new String(ch, start, length));
544+
logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state, string);
512545
break;
513546
case BassMin: // @TODO - find out how to dynamically change "channel-type" bass configuration
514547
case BassMax: // based on these values...
@@ -518,38 +551,37 @@ public void characters(char[] ch, int start, int length) throws SAXException {
518551
// this are currently unprocessed values.
519552
break;
520553
case BassCapabilities:
521-
logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state,
522-
new String(ch, start, length));
554+
logger.debug("{}: Unexpected text data during {}: '{}'", handler.getDeviceName(), state, string);
523555
break;
524556
case Unprocessed:
525557
// drop quietly..
526558
break;
527559
case BassActual:
528-
commandExecutor.updateBassLevelGUIState(new DecimalType(new String(ch, start, length)));
560+
commandExecutor.updateBassLevelGUIState(new DecimalType(string));
529561
break;
530562
case InfoName:
531-
setConfigOption(DEVICE_INFO_NAME, new String(ch, start, length));
563+
setConfigOption(DEVICE_INFO_NAME, string);
532564
break;
533565
case InfoType:
534-
setConfigOption(DEVICE_INFO_TYPE, new String(ch, start, length));
535-
setConfigOption(PROPERTY_MODEL_ID, new String(ch, start, length));
566+
setConfigOption(DEVICE_INFO_TYPE, string);
567+
setConfigOption(PROPERTY_MODEL_ID, string);
536568
break;
537569
case InfoModuleType:
538-
setConfigOption(PROPERTY_HARDWARE_VERSION, new String(ch, start, length));
570+
setConfigOption(PROPERTY_HARDWARE_VERSION, string);
539571
break;
540572
case InfoFirmwareVersion:
541-
String[] fwVersion = new String(ch, start, length).split(" ");
573+
String[] fwVersion = string.split(" ");
542574
setConfigOption(PROPERTY_FIRMWARE_VERSION, fwVersion[0]);
543575
break;
544576
case BassAvailable:
545-
boolean bassAvailable = Boolean.parseBoolean(new String(ch, start, length));
577+
boolean bassAvailable = Boolean.parseBoolean(string);
546578
commandExecutor.setBassAvailable(bassAvailable);
547579
break;
548580
case NowPlayingAlbum:
549-
updateNowPlayingAlbum(new StringType(new String(ch, start, length)));
581+
textContent.append(string);
550582
break;
551583
case NowPlayingArt:
552-
String url = new String(ch, start, length);
584+
String url = string;
553585
if (url.startsWith("http")) {
554586
// We download the cover art in a different thread to not delay the other operations
555587
handler.getScheduler().submit(() -> {
@@ -565,60 +597,60 @@ public void characters(char[] ch, int start, int length) throws SAXException {
565597
}
566598
break;
567599
case NowPlayingArtist:
568-
updateNowPlayingArtist(new StringType(new String(ch, start, length)));
600+
textContent.append(string);
569601
break;
570602
case ContentItemItemName:
571-
contentItem.setItemName(new String(ch, start, length));
603+
contentItem.setItemName(string);
572604
break;
573605
case ContentItemContainerArt:
574-
contentItem.setContainerArt(new String(ch, start, length));
606+
contentItem.setContainerArt(string);
575607
break;
576608
case NowPlayingDescription:
577-
updateNowPlayingDescription(new StringType(new String(ch, start, length)));
609+
textContent.append(string);
578610
break;
579611
case NowPlayingGenre:
580-
updateNowPlayingGenre(new StringType(new String(ch, start, length)));
612+
textContent.append(string);
581613
break;
582614
case NowPlayingPlayStatus:
583-
String playPauseState = new String(ch, start, length);
615+
String playPauseState = string;
584616
if ("PLAY_STATE".equals(playPauseState) || "BUFFERING_STATE".equals(playPauseState)) {
585617
commandExecutor.updatePlayerControlGUIState(PlayPauseType.PLAY);
586618
} else if ("STOP_STATE".equals(playPauseState) || "PAUSE_STATE".equals(playPauseState)) {
587619
commandExecutor.updatePlayerControlGUIState(PlayPauseType.PAUSE);
588620
}
589621
break;
590622
case NowPlayingStationLocation:
591-
updateNowPlayingStationLocation(new StringType(new String(ch, start, length)));
623+
textContent.append(string);
592624
break;
593625
case NowPlayingStationName:
594-
updateNowPlayingStationName(new StringType(new String(ch, start, length)));
626+
textContent.append(string);
595627
break;
596628
case NowPlayingTrack:
597-
updateNowPlayingTrack(new StringType(new String(ch, start, length)));
629+
textContent.append(string);
598630
break;
599631
case VolumeActual:
600-
commandExecutor.updateVolumeGUIState(new PercentType(Integer.parseInt(new String(ch, start, length))));
632+
commandExecutor.updateVolumeGUIState(new PercentType(Integer.parseInt(string)));
601633
break;
602634
case VolumeMuteEnabled:
603-
volumeMuteEnabled = Boolean.parseBoolean(new String(ch, start, length));
635+
volumeMuteEnabled = Boolean.parseBoolean(string);
604636
commandExecutor.setCurrentMuted(volumeMuteEnabled);
605637
break;
606638
case MasterDeviceId:
607639
if (masterDeviceId != null) {
608-
masterDeviceId.macAddress = new String(ch, start, length);
640+
masterDeviceId.macAddress = string;
609641
}
610642
break;
611643
case GroupName:
612644
if (masterDeviceId != null) {
613-
masterDeviceId.groupName = new String(ch, start, length);
645+
masterDeviceId.groupName = string;
614646
}
615647
break;
616648
case DeviceId:
617-
deviceId = new String(ch, start, length);
649+
deviceId = string;
618650
break;
619651
case DeviceIp:
620652
if (masterDeviceId != null && Objects.equals(masterDeviceId.macAddress, deviceId)) {
621-
masterDeviceId.host = new String(ch, start, length);
653+
masterDeviceId.host = string;
622654
}
623655
break;
624656
default:

bundles/org.openhab.binding.bosesoundtouch/src/main/java/org/openhab/binding/bosesoundtouch/internal/XMLResponseProcessor.java

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ private void init() {
123123
nowPlayingMap.put("artist", XMLHandlerState.NowPlayingArtist);
124124
nowPlayingMap.put("ContentItem", XMLHandlerState.ContentItem);
125125
nowPlayingMap.put("description", XMLHandlerState.NowPlayingDescription);
126+
nowPlayingMap.put("genre", XMLHandlerState.NowPlayingGenre);
126127
nowPlayingMap.put("playStatus", XMLHandlerState.NowPlayingPlayStatus);
127128
nowPlayingMap.put("rateEnabled", XMLHandlerState.NowPlayingRateEnabled);
128129
nowPlayingMap.put("skipEnabled", XMLHandlerState.NowPlayingSkipEnabled);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Copyright (c) 2010-2024 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.binding.bosesoundtouch.internal;
14+
15+
import static org.mockito.Mockito.mock;
16+
import static org.mockito.Mockito.verify;
17+
import static org.mockito.Mockito.when;
18+
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.CHANNEL_NOWPLAYING_ALBUM;
19+
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.CHANNEL_NOWPLAYING_ARTIST;
20+
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.CHANNEL_NOWPLAYING_GENRE;
21+
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.CHANNEL_NOWPLAYING_STATIONLOCATION;
22+
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.CHANNEL_NOWPLAYING_STATIONNAME;
23+
import static org.openhab.binding.bosesoundtouch.internal.BoseSoundTouchBindingConstants.CHANNEL_NOWPLAYING_TRACK;
24+
25+
import java.io.File;
26+
import java.io.IOException;
27+
import java.nio.file.Files;
28+
29+
import javax.xml.parsers.ParserConfigurationException;
30+
31+
import org.eclipse.jdt.annotation.NonNullByDefault;
32+
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.Test;
34+
import org.openhab.binding.bosesoundtouch.internal.handler.BoseSoundTouchHandler;
35+
import org.openhab.core.library.types.StringType;
36+
import org.xml.sax.SAXException;
37+
38+
/**
39+
* Unit tests for {@link XMLResponseProcessor}.
40+
*
41+
* @author David Pace - Initial contribution
42+
*
43+
*/
44+
@NonNullByDefault
45+
class XMLResponseProcessorTest {
46+
47+
private @NonNullByDefault({}) XMLResponseProcessor fixture;
48+
private @NonNullByDefault({}) BoseSoundTouchHandler handler;
49+
50+
@BeforeEach
51+
protected void setUp() throws Exception {
52+
handler = mock(BoseSoundTouchHandler.class);
53+
when(handler.getMacAddress()).thenReturn("5065834D198B");
54+
55+
CommandExecutor commandExecutor = mock(CommandExecutor.class);
56+
when(handler.getCommandExecutor()).thenReturn(commandExecutor);
57+
58+
fixture = new XMLResponseProcessor(handler);
59+
}
60+
61+
@Test
62+
void testParseNowPlayingUpdate() throws SAXException, IOException, ParserConfigurationException {
63+
String updateXML = Files.readString(new File("src/test/resources/NowPlayingUpdate.xml").toPath());
64+
65+
fixture.handleMessage(updateXML);
66+
67+
verify(handler).updateState(CHANNEL_NOWPLAYING_ALBUM, new StringType("\"Appetite for Destruction\""));
68+
verify(handler).updateState(CHANNEL_NOWPLAYING_ARTIST, new StringType("Guns N' Roses"));
69+
verify(handler).updateState(CHANNEL_NOWPLAYING_TRACK, new StringType("Sweet Child O' Mine"));
70+
verify(handler).updateState(CHANNEL_NOWPLAYING_GENRE, new StringType("Rock 'n' Roll"));
71+
verify(handler).updateState(CHANNEL_NOWPLAYING_STATIONNAME, new StringType("Jammin'"));
72+
verify(handler).updateState(CHANNEL_NOWPLAYING_STATIONLOCATION, new StringType("All o'er the world"));
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<updates deviceID="5065834D198B">
2+
<nowPlayingUpdated>
3+
<nowPlaying deviceID="5065834D198B" source="BLUETOOTH" sourceAccount="">
4+
<ContentItem source="BLUETOOTH" location="" sourceAccount="" isPresetable="false">
5+
<itemName>iPhone</itemName>
6+
</ContentItem>
7+
<track>Sweet Child O&apos; Mine</track>
8+
<artist>Guns N&apos; Roses</artist>
9+
<album>&quot;Appetite for Destruction&quot;</album>
10+
<stationName>Jammin&apos;</stationName>
11+
<stationLocation>All o&apos;er the world</stationLocation>
12+
<art artImageStatus="SHOW_DEFAULT_IMAGE" />
13+
<skipEnabled />
14+
<playStatus>PLAY_STATE</playStatus>
15+
<skipPreviousEnabled />
16+
<genre>Rock &apos;n&apos; Roll</genre>
17+
<connectionStatusInfo status="CONNECTED" deviceName="iPhone" />
18+
</nowPlaying>
19+
</nowPlayingUpdated>
20+
</updates>

0 commit comments

Comments
 (0)