Skip to content
This repository was archived by the owner on Dec 4, 2023. It is now read-only.

Commit 1eb5a85

Browse files
Added SetSpeakMiddleware.java and Unit tests (#1100)
Co-authored-by: tracyboehrer <tracyboehrer@users.noreply.github.com>
1 parent 918b55c commit 1eb5a85

File tree

2 files changed

+264
-0
lines changed

2 files changed

+264
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MT License.
3+
package com.microsoft.bot.builder;
4+
5+
import java.io.IOException;
6+
import java.util.concurrent.CompletableFuture;
7+
8+
import javax.xml.parsers.DocumentBuilder;
9+
import javax.xml.parsers.DocumentBuilderFactory;
10+
import javax.xml.parsers.ParserConfigurationException;
11+
12+
import com.microsoft.bot.connector.Channels;
13+
import com.microsoft.bot.schema.Activity;
14+
import com.microsoft.bot.schema.ActivityTypes;
15+
16+
import org.apache.commons.lang3.StringUtils;
17+
import org.w3c.dom.Document;
18+
import org.xml.sax.SAXException;
19+
20+
/**
21+
* Support the DirectLine speech and telephony channels to ensure the
22+
* appropriate SSML tags are set on the Activity Speak property.
23+
*/
24+
public class SetSpeakMiddleware implements Middleware {
25+
26+
private final String voiceName;
27+
private final boolean fallbackToTextForSpeak;
28+
private final String lang;
29+
30+
/**
31+
* Initializes a new instance of the {@link SetSpeakMiddleware} class.
32+
*
33+
* @param voiceName The SSML voice name attribute value.
34+
* @param lang The xml:lang value.
35+
* @param fallbackToTextForSpeak true if an empt Activity.Speak is populated
36+
* with Activity.getText().
37+
*/
38+
public SetSpeakMiddleware(String voiceName, String lang, boolean fallbackToTextForSpeak) {
39+
this.voiceName = voiceName;
40+
this.fallbackToTextForSpeak = fallbackToTextForSpeak;
41+
if (lang == null) {
42+
throw new IllegalArgumentException("lang cannot be null.");
43+
} else {
44+
this.lang = lang;
45+
}
46+
}
47+
48+
/**
49+
* Processes an incoming activity.
50+
*
51+
* @param turnContext The context Object for this turn.
52+
* @param next The delegate to call to continue the bot middleware
53+
* pipeline.
54+
*
55+
* @return A task that represents the work queued to execute.
56+
*/
57+
public CompletableFuture<Void> onTurn(TurnContext turnContext, NextDelegate next) {
58+
turnContext.onSendActivities((ctx, activities, nextSend) -> {
59+
for (Activity activity : activities) {
60+
if (activity.getType().equals(ActivityTypes.MESSAGE)) {
61+
if (fallbackToTextForSpeak && StringUtils.isBlank(activity.getSpeak())) {
62+
activity.setSpeak(activity.getText());
63+
}
64+
65+
if (StringUtils.isNotBlank(activity.getSpeak()) && StringUtils.isNotBlank(voiceName)
66+
&& (StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(),
67+
Channels.DIRECTLINESPEECH) == 0
68+
|| StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(),
69+
Channels.EMULATOR) == 0
70+
|| StringUtils.compareIgnoreCase(turnContext.getActivity().getChannelId(),
71+
"telephony") == 0)) {
72+
if (!hasTag("speak", activity.getSpeak()) && !hasTag("voice", activity.getSpeak())) {
73+
activity.setSpeak(
74+
String.format("<voice name='%s'>%s</voice>", voiceName, activity.getSpeak()));
75+
}
76+
77+
activity.setSpeak(String
78+
.format("<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' "
79+
+ "xml:lang='%s'>%s</speak>", lang, activity.getSpeak()));
80+
}
81+
}
82+
}
83+
return nextSend.get();
84+
});
85+
return next.next();
86+
}
87+
88+
private boolean hasTag(String tagName, String speakText) {
89+
try {
90+
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
91+
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
92+
Document doc = dBuilder.parse(speakText);
93+
94+
if (doc.getElementsByTagName(tagName).getLength() > 0) {
95+
return true;
96+
}
97+
98+
return false;
99+
} catch (SAXException | ParserConfigurationException | IOException ex) {
100+
return false;
101+
}
102+
}
103+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MT License.
3+
4+
package com.microsoft.bot.builder;
5+
6+
import com.microsoft.bot.builder.adapters.TestAdapter;
7+
import com.microsoft.bot.builder.adapters.TestFlow;
8+
import com.microsoft.bot.connector.Channels;
9+
import com.microsoft.bot.schema.Activity;
10+
import com.microsoft.bot.schema.ChannelAccount;
11+
import com.microsoft.bot.schema.ConversationAccount;
12+
import com.microsoft.bot.schema.ConversationReference;
13+
14+
import org.junit.Assert;
15+
import org.junit.Test;
16+
17+
public class SetSpeakMiddlewareTests {
18+
19+
@Test
20+
public void ConstructorValidation() {
21+
// no 'lang'
22+
Assert.assertThrows(IllegalArgumentException.class, () -> new SetSpeakMiddleware("voice", null, false));
23+
}
24+
25+
@Test
26+
public void NoFallback() {
27+
TestAdapter adapter = new TestAdapter(createConversation("NoFallback"))
28+
.use(new SetSpeakMiddleware("male", "en-us", false));
29+
30+
new TestFlow(adapter, turnContext -> {
31+
Activity activity = MessageFactory.text("OK");
32+
33+
return turnContext.sendActivity(activity).thenApply(result -> null);
34+
}).send("foo").assertReply(obj -> {
35+
Activity activity = (Activity) obj;
36+
Assert.assertNull(activity.getSpeak());
37+
}).startTest().join();
38+
}
39+
40+
// fallback instanceof true, for any ChannelId other than emulator,
41+
// directlinespeech, or telephony should
42+
// just set Activity.Speak to Activity.Text if Speak instanceof empty.
43+
@Test
44+
public void FallbackNullSpeak() {
45+
TestAdapter adapter = new TestAdapter(createConversation("NoFallback"))
46+
.use(new SetSpeakMiddleware("male", "en-us", true));
47+
48+
new TestFlow(adapter, turnContext -> {
49+
Activity activity = MessageFactory.text("OK");
50+
51+
return turnContext.sendActivity(activity).thenApply(result -> null);
52+
}).send("foo").assertReply(obj -> {
53+
Activity activity = (Activity) obj;
54+
Assert.assertEquals(activity.getText(), activity.getSpeak());
55+
}).startTest().join();
56+
}
57+
58+
// fallback instanceof true, for any ChannelId other than emulator,
59+
// directlinespeech, or telephony should
60+
// leave a non-empty Speak unchanged.
61+
@Test
62+
public void FallbackWithSpeak() {
63+
TestAdapter adapter = new TestAdapter(createConversation("Fallback"))
64+
.use(new SetSpeakMiddleware("male", "en-us", true));
65+
66+
new TestFlow(adapter, turnContext -> {
67+
Activity activity = MessageFactory.text("OK");
68+
activity.setSpeak("speak value");
69+
70+
return turnContext.sendActivity(activity).thenApply(result -> null);
71+
}).send("foo").assertReply(obj -> {
72+
Activity activity = (Activity) obj;
73+
Assert.assertEquals("speak value", activity.getSpeak());
74+
}).startTest().join();
75+
}
76+
77+
@Test
78+
public void AddVoiceEmulator() {
79+
AddVoice(Channels.EMULATOR);
80+
}
81+
82+
@Test
83+
public void AddVoiceDirectlineSpeech() {
84+
AddVoice(Channels.DIRECTLINESPEECH);
85+
}
86+
87+
@Test
88+
public void AddVoiceTelephony() {
89+
AddVoice("telephony");
90+
}
91+
92+
93+
// Voice instanceof added to Speak property.
94+
public void AddVoice(String channelId) {
95+
TestAdapter adapter = new TestAdapter(createConversation("Fallback", channelId))
96+
.use(new SetSpeakMiddleware("male", "en-us", true));
97+
98+
new TestFlow(adapter, turnContext -> {
99+
Activity activity = MessageFactory.text("OK");
100+
101+
return turnContext.sendActivity(activity).thenApply(result -> null);
102+
}).send("foo").assertReply(obj -> {
103+
Activity activity = (Activity) obj;
104+
Assert.assertEquals("<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' "
105+
+ "xml:lang='en-us'><voice name='male'>OK</voice></speak>",
106+
activity.getSpeak());
107+
}).startTest().join();
108+
}
109+
110+
@Test
111+
public void AddNoVoiceEmulator() {
112+
AddNoVoice(Channels.EMULATOR);
113+
}
114+
115+
@Test
116+
public void AddNoVoiceDirectlineSpeech() {
117+
AddNoVoice(Channels.DIRECTLINESPEECH);
118+
}
119+
120+
@Test
121+
public void AddNoVoiceTelephony() {
122+
AddNoVoice("telephony");
123+
}
124+
125+
126+
// With no 'voice' specified, the Speak property instanceof unchanged.
127+
public void AddNoVoice(String channelId) {
128+
TestAdapter adapter = new TestAdapter(createConversation("Fallback", channelId))
129+
.use(new SetSpeakMiddleware(null, "en-us", true));
130+
131+
new TestFlow(adapter, turnContext -> {
132+
Activity activity = MessageFactory.text("OK");
133+
134+
return turnContext.sendActivity(activity).thenApply(result -> null);
135+
}).send("foo").assertReply(obj -> {
136+
Activity activity = (Activity) obj;
137+
Assert.assertEquals("OK",
138+
activity.getSpeak());
139+
}).startTest().join();
140+
}
141+
142+
143+
private static ConversationReference createConversation(String name) {
144+
return createConversation(name, "User1", "Bot", "test");
145+
}
146+
147+
private static ConversationReference createConversation(String name, String channelId) {
148+
return createConversation(name, "User1", "Bot", channelId);
149+
}
150+
151+
private static ConversationReference createConversation(String name, String user, String bot, String channelId) {
152+
ConversationReference conversationReference = new ConversationReference();
153+
conversationReference.setChannelId(channelId);
154+
conversationReference.setServiceUrl("https://test.com");
155+
conversationReference.setConversation(new ConversationAccount(false, name, name));
156+
conversationReference.setUser(new ChannelAccount(user.toLowerCase(), user));
157+
conversationReference.setBot(new ChannelAccount(bot.toLowerCase(), bot));
158+
conversationReference.setLocale("en-us");
159+
return conversationReference;
160+
}
161+
}

0 commit comments

Comments
 (0)