[googletts] Improve exception handling (#11925)

* Improve exception handling
* Moved classes to dto package to reduce SAT warning

Signed-off-by: Christoph Weitkamp <github@christophweitkamp.de>
This commit is contained in:
Christoph Weitkamp 2022-01-03 14:12:36 +01:00 committed by GitHub
parent 76855fd81a
commit 0936d97b41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 97 additions and 101 deletions

View File

@ -1,34 +0,0 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.voice.googletts.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Thrown, if an authentication error is given.
*
* @author Christoph Weitkamp - Initial contribution
*
*/
@NonNullByDefault
public class AuthenticationException extends Exception {
private static final long serialVersionUID = 1L;
public AuthenticationException() {
}
public AuthenticationException(String message) {
super(message);
}
}

View File

@ -24,7 +24,6 @@ import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Collections;
import java.util.Dictionary; import java.util.Dictionary;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -37,21 +36,23 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.http.MimeTypes;
import org.openhab.core.audio.AudioFormat; import org.openhab.core.audio.AudioFormat;
import org.openhab.core.auth.AuthenticationException;
import org.openhab.core.auth.client.oauth2.AccessTokenResponse; import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
import org.openhab.core.auth.client.oauth2.OAuthClientService; import org.openhab.core.auth.client.oauth2.OAuthClientService;
import org.openhab.core.auth.client.oauth2.OAuthException; import org.openhab.core.auth.client.oauth2.OAuthException;
import org.openhab.core.auth.client.oauth2.OAuthFactory; import org.openhab.core.auth.client.oauth2.OAuthFactory;
import org.openhab.core.auth.client.oauth2.OAuthResponseException; import org.openhab.core.auth.client.oauth2.OAuthResponseException;
import org.openhab.core.i18n.CommunicationException;
import org.openhab.core.io.net.http.HttpRequestBuilder; import org.openhab.core.io.net.http.HttpRequestBuilder;
import org.openhab.voice.googletts.internal.protocol.AudioConfig; import org.openhab.voice.googletts.internal.dto.AudioConfig;
import org.openhab.voice.googletts.internal.protocol.AudioEncoding; import org.openhab.voice.googletts.internal.dto.AudioEncoding;
import org.openhab.voice.googletts.internal.protocol.ListVoicesResponse; import org.openhab.voice.googletts.internal.dto.ListVoicesResponse;
import org.openhab.voice.googletts.internal.protocol.SsmlVoiceGender; import org.openhab.voice.googletts.internal.dto.SsmlVoiceGender;
import org.openhab.voice.googletts.internal.protocol.SynthesisInput; import org.openhab.voice.googletts.internal.dto.SynthesisInput;
import org.openhab.voice.googletts.internal.protocol.SynthesizeSpeechRequest; import org.openhab.voice.googletts.internal.dto.SynthesizeSpeechRequest;
import org.openhab.voice.googletts.internal.protocol.SynthesizeSpeechResponse; import org.openhab.voice.googletts.internal.dto.SynthesizeSpeechResponse;
import org.openhab.voice.googletts.internal.protocol.Voice; import org.openhab.voice.googletts.internal.dto.Voice;
import org.openhab.voice.googletts.internal.protocol.VoiceSelectionParams; import org.openhab.voice.googletts.internal.dto.VoiceSelectionParams;
import org.osgi.service.cm.Configuration; import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.cm.ConfigurationAdmin;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -59,6 +60,7 @@ import org.slf4j.LoggerFactory;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
/** /**
* Google Cloud TTS API call implementation. * Google Cloud TTS API call implementation.
@ -152,8 +154,8 @@ class GoogleCloudAPI {
getAccessToken(); getAccessToken();
initialized = true; initialized = true;
initVoices(); initVoices();
} catch (AuthenticationException | IOException ex) { } catch (AuthenticationException | CommunicationException e) {
logger.warn("Error initializing Google Cloud TTS service: {}", ex.getMessage()); logger.warn("Error initializing Google Cloud TTS service: {}", e.getMessage());
oAuthService = null; oAuthService = null;
initialized = false; initialized = false;
voices.clear(); voices.clear();
@ -177,17 +179,24 @@ class GoogleCloudAPI {
/** /**
* Fetches the OAuth2 tokens from Google Cloud Platform if the auth-code is set in the configuration. If successful * Fetches the OAuth2 tokens from Google Cloud Platform if the auth-code is set in the configuration. If successful
* the auth-code will be removed from the configuration. * the auth-code will be removed from the configuration.
*
* @throws AuthenticationException
* @throws CommunicationException
*/ */
private void getAccessToken() throws AuthenticationException, IOException { @SuppressWarnings("null")
private void getAccessToken() throws AuthenticationException, CommunicationException {
String authcode = config.authcode; String authcode = config.authcode;
if (authcode != null && !authcode.isEmpty()) { if (authcode != null && !authcode.isEmpty()) {
logger.debug("Trying to get access and refresh tokens."); logger.debug("Trying to get access and refresh tokens.");
try { try {
oAuthService.getAccessTokenResponseByAuthorizationCode(authcode, GCP_REDIRECT_URI); oAuthService.getAccessTokenResponseByAuthorizationCode(authcode, GCP_REDIRECT_URI);
} catch (OAuthException | OAuthResponseException ex) { } catch (OAuthException | OAuthResponseException e) {
logger.debug("Error fetching access token: {}", ex.getMessage(), ex); logger.debug("Error fetching access token: {}", e.getMessage(), e);
throw new AuthenticationException( throw new AuthenticationException(
"Error fetching access token. Invalid authcode? Please generate a new one."); "Error fetching access token. Invalid authcode? Please generate a new one.");
} catch (IOException e) {
throw new CommunicationException(
String.format("An unexpected IOException occurred: %s", e.getMessage()));
} }
config.authcode = null; config.authcode = null;
@ -207,14 +216,17 @@ class GoogleCloudAPI {
} }
} }
private String getAuthorizationHeader() throws AuthenticationException, IOException { @SuppressWarnings("null")
private String getAuthorizationHeader() throws AuthenticationException, CommunicationException {
final AccessTokenResponse accessTokenResponse; final AccessTokenResponse accessTokenResponse;
try { try {
accessTokenResponse = oAuthService.getAccessTokenResponse(); accessTokenResponse = oAuthService.getAccessTokenResponse();
} catch (OAuthException | OAuthResponseException ex) { } catch (OAuthException | OAuthResponseException e) {
logger.debug("Error fetching access token: {}", ex.getMessage(), ex); logger.debug("Error fetching access token: {}", e.getMessage(), e);
throw new AuthenticationException( throw new AuthenticationException(
"Error fetching access token. Invalid authcode? Please generate a new one."); "Error fetching access token. Invalid authcode? Please generate a new one.");
} catch (IOException e) {
throw new CommunicationException(String.format("An unexpected IOException occurred: %s", e.getMessage()));
} }
if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
|| accessTokenResponse.getAccessToken().isEmpty()) { || accessTokenResponse.getAccessToken().isEmpty()) {
@ -255,13 +267,16 @@ class GoogleCloudAPI {
*/ */
Set<GoogleTTSVoice> getVoicesForLocale(Locale locale) { Set<GoogleTTSVoice> getVoicesForLocale(Locale locale) {
Set<GoogleTTSVoice> localeVoices = voices.get(locale); Set<GoogleTTSVoice> localeVoices = voices.get(locale);
return localeVoices != null ? localeVoices : Collections.emptySet(); return localeVoices != null ? localeVoices : Set.of();
} }
/** /**
* Google API call to load locales and voices. * Google API call to load locales and voices.
*
* @throws AuthenticationException
* @throws CommunicationException
*/ */
private void initVoices() throws AuthenticationException, IOException { private void initVoices() throws AuthenticationException, CommunicationException {
if (oAuthService != null) { if (oAuthService != null) {
voices.clear(); voices.clear();
for (GoogleTTSVoice voice : listVoices()) { for (GoogleTTSVoice voice : listVoices()) {
@ -281,25 +296,32 @@ class GoogleCloudAPI {
} }
@SuppressWarnings("null") @SuppressWarnings("null")
private List<GoogleTTSVoice> listVoices() throws AuthenticationException, IOException { private List<GoogleTTSVoice> listVoices() throws AuthenticationException, CommunicationException {
HttpRequestBuilder builder = HttpRequestBuilder.getFrom(LIST_VOICES_URL) HttpRequestBuilder builder = HttpRequestBuilder.getFrom(LIST_VOICES_URL)
.withHeader(HttpHeader.AUTHORIZATION.name(), getAuthorizationHeader()); .withHeader(HttpHeader.AUTHORIZATION.name(), getAuthorizationHeader());
ListVoicesResponse listVoicesResponse = gson.fromJson(builder.getContentAsString(), ListVoicesResponse.class); try {
ListVoicesResponse listVoicesResponse = gson.fromJson(builder.getContentAsString(),
ListVoicesResponse.class);
if (listVoicesResponse == null || listVoicesResponse.getVoices() == null) { if (listVoicesResponse == null || listVoicesResponse.getVoices() == null) {
return Collections.emptyList(); return List.of();
}
List<GoogleTTSVoice> result = new ArrayList<>();
for (Voice voice : listVoicesResponse.getVoices()) {
for (String languageCode : voice.getLanguageCodes()) {
result.add(new GoogleTTSVoice(Locale.forLanguageTag(languageCode), voice.getName(),
voice.getSsmlGender().name()));
} }
}
return result; List<GoogleTTSVoice> result = new ArrayList<>();
for (Voice voice : listVoicesResponse.getVoices()) {
for (String languageCode : voice.getLanguageCodes()) {
result.add(new GoogleTTSVoice(Locale.forLanguageTag(languageCode), voice.getName(),
voice.getSsmlGender().name()));
}
}
return result;
} catch (JsonSyntaxException e) {
// do nothing
} catch (IOException e) {
throw new CommunicationException(String.format("An unexpected IOException occurred: %s", e.getMessage()));
}
return List.of();
} }
/** /**
@ -319,7 +341,7 @@ class GoogleCloudAPI {
} }
} }
byte[] synthesizeSpeech(String text, GoogleTTSVoice voice, String codec) { public byte[] synthesizeSpeech(String text, GoogleTTSVoice voice, String codec) {
String[] format = getFormatForCodec(codec); String[] format = getFormatForCodec(codec);
String fileNameInCache = getUniqueFilenameForText(text, voice.getTechnicalName()); String fileNameInCache = getUniqueFilenameForText(text, voice.getTechnicalName());
File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + format[1]); File audioFileInCache = new File(cacheFolder, fileNameInCache + "." + format[1]);
@ -336,19 +358,17 @@ class GoogleCloudAPI {
saveAudioAndTextToFile(text, audioFileInCache, audio, voice.getTechnicalName()); saveAudioAndTextToFile(text, audioFileInCache, audio, voice.getTechnicalName());
} }
return audio; return audio;
} catch (AuthenticationException ex) { } catch (AuthenticationException | CommunicationException e) {
logger.warn("Error initializing Google Cloud TTS service: {}", ex.getMessage()); logger.warn("Error initializing Google Cloud TTS service: {}", e.getMessage());
oAuthService = null; oAuthService = null;
initialized = false; initialized = false;
voices.clear(); voices.clear();
return null; } catch (FileNotFoundException e) {
} catch (FileNotFoundException ex) { logger.warn("Could not write file {} to cache: {}", audioFileInCache, e.getMessage());
logger.warn("Could not write {} to cache", audioFileInCache, ex); } catch (IOException e) {
return null; logger.debug("An unexpected IOException occurred: {}", e.getMessage());
} catch (IOException ex) {
logger.error("Could not write {} to cache", audioFileInCache, ex);
return null;
} }
return null;
} }
/** /**
@ -358,10 +378,11 @@ class GoogleCloudAPI {
* @param cacheFile Cache entry file. * @param cacheFile Cache entry file.
* @param audio Byte array of the audio. * @param audio Byte array of the audio.
* @param voiceName Used voice * @param voiceName Used voice
* @throws FileNotFoundException
* @throws IOException in case of file handling exceptions * @throws IOException in case of file handling exceptions
*/ */
private void saveAudioAndTextToFile(String text, File cacheFile, byte[] audio, String voiceName) private void saveAudioAndTextToFile(String text, File cacheFile, byte[] audio, String voiceName)
throws IOException { throws IOException, FileNotFoundException {
logger.debug("Caching audio file {}", cacheFile.getName()); logger.debug("Caching audio file {}", cacheFile.getName());
try (FileOutputStream audioFileOutputStream = new FileOutputStream(cacheFile)) { try (FileOutputStream audioFileOutputStream = new FileOutputStream(cacheFile)) {
audioFileOutputStream.write(audio); audioFileOutputStream.write(audio);
@ -405,10 +426,12 @@ class GoogleCloudAPI {
* @param voice Voice parameter * @param voice Voice parameter
* @param audioFormat Audio encoding format * @param audioFormat Audio encoding format
* @return Audio input stream or {@code null} when encoding exceptions occur * @return Audio input stream or {@code null} when encoding exceptions occur
* @throws AuthenticationException
* @throws CommunicationException
*/ */
@SuppressWarnings({ "null", "unused" }) @SuppressWarnings("null")
private byte[] synthesizeSpeechByGoogle(String text, GoogleTTSVoice voice, String audioFormat) private byte[] synthesizeSpeechByGoogle(String text, GoogleTTSVoice voice, String audioFormat)
throws AuthenticationException, IOException { throws AuthenticationException, CommunicationException {
AudioConfig audioConfig = new AudioConfig(AudioEncoding.valueOf(audioFormat), config.pitch, config.speakingRate, AudioConfig audioConfig = new AudioConfig(AudioEncoding.valueOf(audioFormat), config.pitch, config.speakingRate,
config.volumeGainDb); config.volumeGainDb);
SynthesisInput synthesisInput = new SynthesisInput(text); SynthesisInput synthesisInput = new SynthesisInput(text);
@ -422,15 +445,22 @@ class GoogleCloudAPI {
.withHeader(HttpHeader.AUTHORIZATION.name(), getAuthorizationHeader()) .withHeader(HttpHeader.AUTHORIZATION.name(), getAuthorizationHeader())
.withContent(gson.toJson(request), MimeTypes.Type.APPLICATION_JSON.name()); .withContent(gson.toJson(request), MimeTypes.Type.APPLICATION_JSON.name());
SynthesizeSpeechResponse synthesizeSpeechResponse = gson.fromJson(builder.getContentAsString(), try {
SynthesizeSpeechResponse.class); SynthesizeSpeechResponse synthesizeSpeechResponse = gson.fromJson(builder.getContentAsString(),
SynthesizeSpeechResponse.class);
if (synthesizeSpeechResponse == null) { if (synthesizeSpeechResponse == null) {
return null; return null;
}
byte[] encodedBytes = synthesizeSpeechResponse.getAudioContent().getBytes(StandardCharsets.UTF_8);
return Base64.getDecoder().decode(encodedBytes);
} catch (JsonSyntaxException e) {
// do nothing
} catch (IOException e) {
throw new CommunicationException(String.format("An unexpected IOException occurred: %s", e.getMessage()));
} }
return null;
byte[] encodedBytes = synthesizeSpeechResponse.getAudioContent().getBytes(StandardCharsets.UTF_8);
return Base64.getDecoder().decode(encodedBytes);
} }
/** /**
@ -445,9 +475,9 @@ class GoogleCloudAPI {
byte[] bytesOfMessage = (config.toConfigString() + text).getBytes(StandardCharsets.UTF_8); byte[] bytesOfMessage = (config.toConfigString() + text).getBytes(StandardCharsets.UTF_8);
String fileNameHash = String.format("%032x", new BigInteger(1, md.digest(bytesOfMessage))); String fileNameHash = String.format("%032x", new BigInteger(1, md.digest(bytesOfMessage)));
return voiceName + "_" + fileNameHash; return voiceName + "_" + fileNameHash;
} catch (NoSuchAlgorithmException ex) { } catch (NoSuchAlgorithmException e) {
// should not happen // should not happen
logger.error("Could not create MD5 hash for '{}'", text, ex); logger.error("Could not create MD5 hash for '{}'", text, e);
return null; return null;
} }
} }

View File

@ -32,7 +32,7 @@ import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.voice.TTSException; import org.openhab.core.voice.TTSException;
import org.openhab.core.voice.TTSService; import org.openhab.core.voice.TTSService;
import org.openhab.core.voice.Voice; import org.openhab.core.voice.Voice;
import org.openhab.voice.googletts.internal.protocol.AudioEncoding; import org.openhab.voice.googletts.internal.dto.AudioEncoding;
import org.osgi.framework.Constants; import org.osgi.framework.Constants;
import org.osgi.service.cm.ConfigurationAdmin; import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Activate;
@ -330,7 +330,7 @@ public class GoogleTTSService implements TTSService {
// create the audio byte array for given text, locale, format // create the audio byte array for given text, locale, format
byte[] audio = apiImpl.synthesizeSpeech(trimmedText, (GoogleTTSVoice) voice, requestedFormat.getCodec()); byte[] audio = apiImpl.synthesizeSpeech(trimmedText, (GoogleTTSVoice) voice, requestedFormat.getCodec());
if (audio == null) { if (audio == null) {
throw new TTSException("Could not read from Google Cloud TTS Service"); throw new TTSException("Could not synthesize text via Google Cloud TTS Service");
} }
return new ByteArrayAudioStream(audio, requestedFormat); return new ByteArrayAudioStream(audio, requestedFormat);
} }

View File

@ -16,7 +16,7 @@ import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.voice.Voice; import org.openhab.core.voice.Voice;
import org.openhab.voice.googletts.internal.protocol.SsmlVoiceGender; import org.openhab.voice.googletts.internal.dto.SsmlVoiceGender;
/** /**
* Implementation of the Voice interface for Google Cloud TTS Service. * Implementation of the Voice interface for Google Cloud TTS Service.

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
/** /**
* The configuration of the synthesized audio. * The configuration of the synthesized audio.

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
/** /**
* Configuration to set up audio encoder. * Configuration to set up audio encoder.

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
import java.util.List; import java.util.List;

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
/** /**
* Gender of the voice as described in SSML voice element. * Gender of the voice as described in SSML voice element.

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
/** /**
* Contains text input to be synthesized. Either text or ssml must be supplied. Supplying both or neither returns * Contains text input to be synthesized. Either text or ssml must be supplied. Supplying both or neither returns

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
/** /**
* Synthesizes speech synchronously: receive results after all text input has been processed. * Synthesizes speech synchronously: receive results after all text input has been processed.

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
/** /**
* The message returned to the client by the text.synthesize method. * The message returned to the client by the text.synthesize method.

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
import java.util.List; import java.util.List;

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * SPDX-License-Identifier: EPL-2.0
*/ */
package org.openhab.voice.googletts.internal.protocol; package org.openhab.voice.googletts.internal.dto;
/** /**
* Description of which voice to use for a synthesis request. * Description of which voice to use for a synthesis request.