GP-4877 add LibreTranslate string translation service provider

This allows the user to translate strings in a program using an external
LibreTranslate server.
This commit is contained in:
dev747368 2024-08-29 23:46:17 +00:00
parent 9d641ed2da
commit 5738a6c2df
8 changed files with 1274 additions and 15 deletions

View File

@ -381,6 +381,7 @@ src/main/help/help/topics/LanguageProviderPlugin/Languages.htm||GHIDRA||||END|
src/main/help/help/topics/LanguageProviderPlugin/images/Languages.png||GHIDRA||||END|
src/main/help/help/topics/LanguageProviderPlugin/images/PCodeDisplay.png||GHIDRA||||END|
src/main/help/help/topics/LanguageProviderPlugin/images/Warning.png||GHIDRA||||END|
src/main/help/help/topics/LibreTranslatePlugin/LibreTranslatePlugin.htm||GHIDRA||||END|
src/main/help/help/topics/LocationReferencesPlugin/Location_References.html||GHIDRA||||END|
src/main/help/help/topics/LocationReferencesPlugin/images/LabelReferencesSample.png||GHIDRA||||END|
src/main/help/help/topics/LocationReferencesPlugin/images/ReferencesToDialog.png||GHIDRA||||END|

View File

@ -230,7 +230,9 @@
<tocdef id="Data" sortgroup="c" text="Data" target="help/topics/DataPlugin/Data.htm" >
<tocdef id="Create Data" sortgroup="a" text="Create Data" target="help/topics/DataPlugin/Data.htm" />
<tocdef id="Data Types" sortgroup="b" text="Data Types" target="help/topics/DataPlugin/Data.htm#DataTypes" />
<tocdef id="Translate Strings" sortgroup="e" text="Translate Strings" target="help/topics/TranslateStringsPlugin/TranslateStringsPlugin.htm" />
<tocdef id="Translate Strings" sortgroup="e" text="Translate Strings" target="help/topics/TranslateStringsPlugin/TranslateStringsPlugin.htm">
<tocdef id="LibreTranslate" sortgroup="e" text="LibreTranslate" target="help/topics/LibreTranslatePlugin/LibreTranslatePlugin.htm" />
</tocdef>
<tocdef id="Save Image" sortgroup="e" text="Save Image" target="help/topics/ResourceActionsPlugin/ResourceActions.html" />
</tocdef>

View File

@ -0,0 +1,78 @@
<!DOCTYPE doctype PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN">
<HTML>
<HEAD>
<META name="generator" content=
"HTML Tidy for Java (vers. 2009-12-01), see jtidy.sourceforge.net">
<TITLE>LibreTranslate Plugin</TITLE>
<META http-equiv="Content-Type" content="text/html; charset=windows-1252">
<LINK rel="stylesheet" type="text/css" href="help/shared/DefaultStyle.css">
</HEAD>
<BODY lang="EN-US">
<H1><a name="LibreTranslatePlugin"></a>LibreTranslate Plugin</H1>
<P>This plugin adds a string translation service that will appear in the <b>Translate</b>
menu of a string data instance. The <b>Translate</b> menu will appear in the right-click
context menu of data items that are strings.</P>
<P>LibreTranslate (currently hosted at libretranslate.com) is an independant project that
provides an open source translation package that can be self-hosted.</P>
<P>This plugin queries a LibreTranslate server via HTTP to translate each specified string into
a target language. The results of that translation will be determined by the LibreTranslate
server.</P>
<P>A LibreTranslate server can be installed locally by following the instructions provided
on LibreTranslate's website, and then this plugin can connect to it via a URL such as
<b>http://localhost:5000/</b> (when configured with suggested defaults).</P>
<P>It is also possible to use someone else's LibreTranslate server, and typically they
will issue an API key that will authorize the user to connect.</P>
<P>When a string has been translated, the translated value will be shown in place of
the original value, bracketed with <b>&#x00BB;chevrons&#x00AB;</b></P>
<h2><a name="Configuration"></a>Configuration</h2>
<P>See
<b>Edit <IMG src="help/shared/arrow.gif" alt="-&gt;" border="0">
Tool Options <IMG src="help/shared/arrow.gif" alt="-&gt;" border="0">
Strings | LibreTranslate</b>
</P>
<blockquote>
<UL>
<LI><b>URL</b> - required. Example: <b>http://localhost:5000/</b>
(if self hosted and following suggested values)</LI>
<LI><b>API Key</b> - a unique key that authorizes you to connect to the LibreTranslate
server. Can be blank if api keys are not required.</LI>
<LI><b>Source Language</b> - either "auto" or "prompt"</LI>
<LI><b>Target Language</b> - the language code (as defined by LibreTranslate) that
strings should be translated into. This defaults to "en" (English).</LI>
<LI><b>Batch Size</b> - the maximum number of strings to include in a single request
to the LibreTranslate server.</LI>
<LI><b>HTTP Timeout</b> - the maximum number of milliseconds to wait for the
LibreTranslate HTTP server to respond to a request.</LI>
<LI><b>HTTP Timeout [per string]</b> - an additional number of milliseconds,
per string in each request, to wait for the LibreTranslate HTTP server to
respond.</LI>
</UL>
</blockquote>
<P class="providedbyplugin">Provided by: <I>LibreTranslate Plugin</I></P>
<P class="relatedtopic">Related Topics:</P>
<UL>
<LI><P class="relatedtopic"><A href="help/topics/CodeBrowserPlugin/CodeBrowser.htm">Code Browser</A></P></LI>
<LI><P class="relatedtopic"><A href="help/topics/ViewStringsPlugin/ViewStringsPlugin.htm">View Defined Strings</A></P></LI>
<LI><P class="relatedtopic"><A href="help/topics/TranslateStringsPlugin/TranslateStringsPlugin.htm">Translate Strings Plugin</A></P></LI>
<LI><P class="relatedtopic"><A href="help/topics/Search/Search_for_Strings.htm#Encoded_Strings_Dialog">Search For Encoded Strings</A></P></LI>
</UL>
<br>
<br>
<br>
</BODY>
</HTML>

View File

@ -348,13 +348,11 @@
<P class="relatedtopic">Related Topics:</P>
<UL>
<LI><A href="Search_Memory.htm">Search Memory</A></LI>
<LI><A href="Search_Memory.htm">Search Program Memory</A></LI>
<LI><A href="Search_Program_Text.htm">Search Program Text</A></LI>
<LI><A href="Searching.htm">Searching</A></LI>
<LI><P class="relatedtopic"><A href="Search_Memory.htm">Search Memory</A></P></LI>
<LI><P class="relatedtopic"><A href="Search_Memory.htm">Search Program Memory</A></P></LI>
<LI><P class="relatedtopic"><A href="Search_Program_Text.htm">Search Program Text</A></P></LI>
<LI><P class="relatedtopic"><A href="Searching.htm">Searching</A></P></LI>
</UL>
<br>
</BODY>
</HTML>

View File

@ -0,0 +1,187 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.string.translate.libretranslate;
import static ghidra.framework.options.OptionType.*;
import java.net.*;
import java.util.List;
import docking.options.OptionsService;
import ghidra.MiscellaneousPluginPackage;
import ghidra.app.plugin.PluginCategoryNames;
import ghidra.app.services.StringTranslationService;
import ghidra.framework.options.*;
import ghidra.framework.plugintool.*;
import ghidra.framework.plugintool.util.PluginStatus;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.util.*;
//@formatter:off
@PluginInfo(
status = PluginStatus.RELEASED,
packageName = MiscellaneousPluginPackage.NAME,
category = PluginCategoryNames.COMMON,
shortDescription = "String translations using LibreTranslate",
description =
"Uses an external LibreTranslate server to translate strings.\n" +
"See LibreTranslate's website for links to their docs about\n" +
"API keys or instructions for self-hosting your own instance.",
servicesProvided = { StringTranslationService.class }
)
//@formatter:on
public class LibreTranslatePlugin extends Plugin implements OptionsChangeListener {
public static final String LIBRE_TRANSLATE_SERVICE_NAME = "LibreTranslate";
private static final String STRINGS_OPTION = "Strings";
private static final String LT_OPTION = "LibreTranslate";
private static final String SOURCE_LANG_OPTION = "Source Language";
private static final String API_KEY_OPTION = "API Key";
private static final String URL_OPTION = "URL";
private static final String TARGET_LANG_OPTION = "Target Language";
private static final String BATCHSIZE_OPTION = "Batch Size";
private static final String HTTP_TIMEOUT_OPTION = "HTTP Timeout";
private static final String HTTP_TIMEOUT_PER_ELEMENT_OPTION = "HTTP Timeout [per string]";
private static final int BATCHSIZE_DEFAULT = 50;
private static final int BATCHSIZE_MAX = 1000;
private static final int TIMEOUT_DEFAULT = 20 * 1000; // 20 seconds
private static final int TIMEOUT_PERSTRING_DEFAULT = 1 * 1000; // 1 second per batched string
public enum SOURCE_LANGUAGE_OPTION { AUTO, PROMPT }
private ToolOptions stringOptions;
private StringTranslationService serviceInstance;
public LibreTranslatePlugin(PluginTool tool) {
super(tool);
initOptions();
updateServiceInstance();
}
private void initOptions() {
HelpLocation helpLoc = new HelpLocation("LibreTranslatePlugin", "Configuration");
stringOptions = tool.getOptions(STRINGS_OPTION);
stringOptions.registerOption(subOpt(URL_OPTION), STRING_TYPE, "", helpLoc,
"LibreTranslate server URL. Required. Example: http://localhost:5000/");
stringOptions.registerOption(subOpt(API_KEY_OPTION), STRING_TYPE, "", helpLoc,
"LibreTranslate API Key. Optional, but possibly required by the server.");
stringOptions.registerOption(subOpt(SOURCE_LANG_OPTION), OptionType.ENUM_TYPE,
SOURCE_LANGUAGE_OPTION.AUTO, helpLoc,
"Source language code option, either 'auto' or prompted each time.");
stringOptions.registerOption(subOpt(TARGET_LANG_OPTION), STRING_TYPE, "en", helpLoc,
"Target language code. Defaults to 'en'. See LibreTranslate's docs for list.");
stringOptions.registerOption(subOpt(BATCHSIZE_OPTION), INT_TYPE, BATCHSIZE_DEFAULT, helpLoc,
"Maximum number of requests to batch together.");
stringOptions.registerOption(subOpt(HTTP_TIMEOUT_OPTION), INT_TYPE, TIMEOUT_DEFAULT,
helpLoc,
"Time to wait for HTTP requests to the LibreTranslate server to finish. (milliseconds)");
stringOptions.registerOption(subOpt(HTTP_TIMEOUT_PER_ELEMENT_OPTION), INT_TYPE,
TIMEOUT_PERSTRING_DEFAULT, helpLoc,
"Additional time (per translated string) to wait for HTTP requests to finish. (milliseconds)");
stringOptions.addOptionsChangeListener(this);
}
@Override
protected void dispose() {
if (stringOptions != null) {
stringOptions.removeOptionsChangeListener(this);
stringOptions = null;
}
super.dispose();
}
@Override
public void optionsChanged(ToolOptions newOptions, String optionName, Object oldValue,
Object newValue) {
if (optionName.startsWith(subOpt(""))) {
updateServiceInstance();
}
}
private void updateServiceInstance() {
if (serviceInstance != null) {
deregisterService(StringTranslationService.class, serviceInstance);
serviceInstance = null;
}
String urlStr = stringOptions.getString(subOpt(URL_OPTION), "");
String apiKey = stringOptions.getString(subOpt(API_KEY_OPTION), "");
SOURCE_LANGUAGE_OPTION srcLangOption =
stringOptions.getEnum(subOpt(SOURCE_LANG_OPTION), SOURCE_LANGUAGE_OPTION.AUTO);
String targetLangCode = stringOptions.getString(subOpt(TARGET_LANG_OPTION), "en");
int batchSize = stringOptions.getInt(subOpt(BATCHSIZE_OPTION), BATCHSIZE_DEFAULT);
batchSize = Math.clamp(batchSize, 1, BATCHSIZE_MAX);
int timeout = stringOptions.getInt(subOpt(HTTP_TIMEOUT_OPTION), TIMEOUT_DEFAULT);
int timeoutPerString = stringOptions.getInt(subOpt(HTTP_TIMEOUT_PER_ELEMENT_OPTION),
TIMEOUT_PERSTRING_DEFAULT);
timeout = Math.clamp(timeout, 1000, 3600000 /* 1 hour */);
timeoutPerString = Math.clamp(timeoutPerString, 1, 60 * 1000 /* 60 seconds */);
if (urlStr != null && !urlStr.isBlank()) {
try {
URI serverURI = URI.create(urlStr).toURL().toURI(); // round trip to URL to make sure it is valid
serviceInstance = new LibreTranslateStringTranslationService(serverURI, apiKey,
srcLangOption, targetLangCode, batchSize, timeout, timeoutPerString);
}
catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) {
Msg.warn(this, "Invalid URL for LibreTranslate option: " + urlStr);
tool.setStatusInfo("Invalid URL for LibreTranslate option: " + urlStr);
// fall thru
}
}
if (serviceInstance == null) {
// Create a non-functional stub instance that only displays the
// Tool options for this LibreTranslate Service
serviceInstance = new StringTranslationService() {
@Override
public void translate(Program program, List<ProgramLocation> stringLocations,
TranslateOptions options) {
OptionsService optionService = tool.getService(OptionsService.class);
if (optionService != null) {
optionService.showOptionsDialog(STRINGS_OPTION + "." + LT_OPTION, null);
Swing.runLater(() -> {
// if the serviceInstance was changed to a valid obj, re-try to translate the strings
if (serviceInstance instanceof LibreTranslateStringTranslationService) {
serviceInstance.translate(program, stringLocations, options);
}
});
}
}
@Override
public String getTranslationServiceName() {
return LIBRE_TRANSLATE_SERVICE_NAME;
}
};
}
registerDynamicServiceProvided(StringTranslationService.class, serviceInstance);
}
private String subOpt(String optName) {
return LT_OPTION + Options.DELIMITER + optName;
}
}

View File

@ -0,0 +1,468 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.string.translate.libretranslate;
import static ghidra.program.model.data.TranslationSettingsDefinition.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodyHandlers;
import java.time.Duration;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import com.google.gson.*;
import docking.widgets.SelectFromListDialog;
import ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslatePlugin.SOURCE_LANGUAGE_OPTION;
import ghidra.app.services.StringTranslationService;
import ghidra.net.HttpClients;
import ghidra.program.model.data.DataUtilities;
import ghidra.program.model.data.StringDataInstance;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.Program;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.*;
/**
* Connects to an external LibreTranslate server via HTTP.
*/
public class LibreTranslateStringTranslationService implements StringTranslationService {
static final String CONTENT_TYPE_JSON = "application/json";
static final String CONTENT_TYPE_HEADER = "Content-Type";
private static final String GHIDRA_USER_AGENT = "Ghidra";
private URI serverURI;
private String apiKey;
private SOURCE_LANGUAGE_OPTION sourceLanguageOption;
private String targetLanguageCode;
private List<SupportedLanguage> supportedLanguages;
private int batchSize;
private int maxRetryCount = 3;
private int httpTimeout;
private int httpTimeoutPerString;
/**
* Creates an instance of {@link LibreTranslateStringTranslationService}
*
* @param serverURI URL of the LibreTranslate server
* @param apiKey optional string, api key required to submit requests to the server
* @param sourceLanguageOption {@link SOURCE_LANGUAGE_OPTION} enum
* @param targetLanguageCode language code that the server should translate each string into
* @param batchSize max number of strings to submit to the server per request
* @param httpTimeout time to wait for a http request to finish
* @param httpTimeoutPerString additional time per string element to wait for http request to finish
*/
public LibreTranslateStringTranslationService(URI serverURI, String apiKey,
SOURCE_LANGUAGE_OPTION sourceLanguageOption, String targetLanguageCode, int batchSize,
int httpTimeout, int httpTimeoutPerString) {
String path = serverURI.getPath();
this.serverURI = path.endsWith("/") ? serverURI : serverURI.resolve(path + "/");
this.apiKey = Objects.requireNonNullElse(apiKey, "");
this.sourceLanguageOption = sourceLanguageOption;
this.targetLanguageCode = targetLanguageCode;
this.batchSize = Math.clamp(batchSize, 1, batchSize);
this.httpTimeout = Math.clamp(httpTimeout, 1, httpTimeout);
this.httpTimeoutPerString = Math.clamp(httpTimeoutPerString, 1, httpTimeoutPerString);
}
@Override
public String getTranslationServiceName() {
return LibreTranslatePlugin.LIBRE_TRANSLATE_SERVICE_NAME;
}
@Override
public HelpLocation getHelpLocation() {
return new HelpLocation("LibreTranslatePlugin", "LibreTranslatePlugin");
}
@Override
public void translate(Program program, List<ProgramLocation> stringLocations,
TranslateOptions options) {
Msg.info(this, "LibreTranslate translate %d strings using %s"
.formatted(stringLocations.size(), serverURI));
TaskLauncher.launchModal("Translate Strings", monitor -> {
monitor.initialize(stringLocations.size(), "Gathering strings");
List<Entry<ProgramLocation, String>> sourceStrings = stringLocations.stream()
.map(progLoc -> Map.entry(progLoc, DataUtilities.getDataAtLocation(progLoc)))
.map(entry -> {
monitor.incrementProgress();
// convert progLoc->Data into progLoc->String
StringDataInstance sdi =
StringDataInstance.getStringDataInstance(entry.getValue());
String s = sdi.getStringValue();
return s != null ? Map.entry(entry.getKey(), s) : null;
})
.filter(Objects::nonNull)
.toList();
String langCode;
switch (sourceLanguageOption) {
case PROMPT:
try {
ensureSupportedLanguages(monitor);
SupportedLanguage selectedLang = SelectFromListDialog.selectFromList(
supportedLanguages, "Choose Source Language", "Choose",
SupportedLanguage::getDescription);
if (selectedLang == null) {
return;
}
langCode = selectedLang.langCode;
}
catch (IOException e) {
showError("Error Fetching Supported Languages",
"Failed to retrieve list of supported languages", e);
return;
}
break;
case AUTO:
default:
langCode = "auto";
break;
}
try {
List<ProgramLocation> successfulStrings =
translate(program, sourceStrings, langCode, monitor);
int failCount = sourceStrings.size() - successfulStrings.size();
if (failCount > 0) {
Msg.showWarn(this, null, "Translation Incomplete",
"%d of %d strings not translated".formatted(failCount,
stringLocations.size()));
}
}
catch (IOException e) {
showError("LibreTranslate Error", "Error when translating strings", e);
}
});
}
private List<ProgramLocation> translate(Program program,
List<Entry<ProgramLocation, String>> srcStrings, String srcLangCode,
TaskMonitor monitor) throws IOException {
List<ProgramLocation> successfulStrings = new ArrayList<>();
program.withTransaction("Translate strings", () -> {
try {
monitor.initialize(srcStrings.size(), "Translating strings");
for (int srcIndex = 0; srcIndex < srcStrings.size(); srcIndex += batchSize) {
monitor.checkCancelled();
List<Entry<ProgramLocation, String>> subList = srcStrings.subList(srcIndex,
Math.min(srcStrings.size(), srcIndex + batchSize));
List<String> subListStrs = subList.stream().map(e -> e.getValue()).toList();
long start_ts = System.currentTimeMillis();
String jsonResponseStr = null;
for (int retryCount = 0; retryCount < maxRetryCount; retryCount++) {
// NOTE: some strings cause the LibreTranslate server to take much longer
// to respond, which would be the most likely cause of timeout
// issues. Repeating the same request will cause the same timeout error,
// so instead the retryCount is used to scale the timeout.
long timeout =
(this.httpTimeout + (subListStrs.size() * httpTimeoutPerString)) *
(retryCount + 1);
HttpRequest request =
createTranslateRequest(subListStrs, srcLangCode, timeout);
if (retryCount != 0) {
monitor.setMessage(
"Retrying translate request (%d)".formatted(retryCount));
}
try {
jsonResponseStr =
asyncRequest(request, BodyHandlers.ofString(), monitor);
break;
}
catch (HttpTimeoutException e) {
if (retryCount == maxRetryCount - 1) {
throw new IOException(
"Timeout during translate request, %d of %d strings completed"
.formatted(successfulStrings.size(), srcStrings.size()),
e);
}
Msg.error(this, "LibreTranslate timeout on translate request for: %s"
.formatted(subListStrs));
}
}
List<String> subResults = parseTranslateResponse(jsonResponseStr, subListStrs);
for (int resultIndex = 0; resultIndex < subResults.size(); resultIndex++) {
ProgramLocation progLoc = subList.get(resultIndex).getKey();
// FUTURE feature: we could attempt to detect if the original string wasn't
// translated by comparing the original string subListStrs.get(resultIndex)
// with the xlatedValue.
String xlatedValue = subResults.get(resultIndex);
if (xlatedValue != null && !xlatedValue.trim().isEmpty()) {
Data data = DataUtilities.getDataAtLocation(progLoc);
TRANSLATION.setTranslatedValue(data, xlatedValue);
TRANSLATION.setShowTranslated(data, true);
monitor.increment();
successfulStrings.add(progLoc);
}
}
long elapsed = System.currentTimeMillis() - start_ts;
int sps = subList.size() / Math.max(1, (int) (elapsed / 1000));
Msg.debug(this, "LibreTranslate translate batch %d/%d strings, %dms"
.formatted(successfulStrings.size(), srcStrings.size(), elapsed));
monitor.setMessage(
"Translating strings (%d strings per second)".formatted(sps));
}
}
catch (CancelledException e) {
// stop loop without error
}
});
Msg.info(this, "Finished LibreTranslate, %d/%d strings".formatted(srcStrings.size(),
successfulStrings.size()));
return successfulStrings;
}
private HttpRequest createTranslateRequest(List<String> sourceStrings, String sourceLangCode,
long timeout) throws IOException {
Map<String, Object> requestParams = Map.of( // see libretranslate's website for api params
"q", sourceStrings, // query strings
"source", sourceLangCode, // source lang code, eg "yy", or "auto"
"target", targetLanguageCode, // target lang code, eg. "en"
"format", "text", // "text" or "html", we always want text
"alternatives", 0, // TODO: not using alternative answers yet
"api_key", apiKey);
HttpRequest request = request("translate", timeout) // build request
.header(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON)
.POST(ofJsonEncodedParams(requestParams))
.build();
return request;
}
private List<String> parseTranslateResponse(String jsonResponseStr, List<String> requestedStrs)
throws IOException {
List<String> results = new ArrayList<>();
try {
JsonObject json = JsonParser.parseString(jsonResponseStr).getAsJsonObject();
JsonElement xlatedTextEle = json.get("translatedText");
if (xlatedTextEle == null) {
throw new JsonSyntaxException("Bad json data for translatedText value");
}
JsonArray xlatedTexts = xlatedTextEle.getAsJsonArray();
if (xlatedTexts.size() != requestedStrs.size()) {
throw new IllegalStateException("LibreTranslate response size mismatch");
}
// NOTE: if translate request was marked as "auto" source lang, the response will have
// a "detectedLanguage" : { "confidence": int, "language": str } property.
for (int resultIndex = 0; resultIndex < xlatedTexts.size(); resultIndex++) {
results.add(xlatedTexts.get(resultIndex).getAsString());
}
return results;
}
catch (IllegalStateException | JsonSyntaxException e) {
Msg.error(this, "Error parsing translate result: " + resultToSafeStr(jsonResponseStr));
throw new IOException("Bad data in json response", e);
}
catch (Throwable th) {
Msg.error(this, "Error parsing translate result: " + resultToSafeStr(jsonResponseStr));
throw th;
}
}
/**
* Information about a language supported by LibreTranslate
* @param name language name
* @param langCode 2 digit code
* @param targets list of other languages that this language can be translated into
*/
public record SupportedLanguage(String name, String langCode, List<String> targets) {
public String getDescription() {
return "%s (%s)".formatted(name, langCode);
}
}
private void ensureSupportedLanguages(TaskMonitor monitor) throws IOException {
try {
if (supportedLanguages == null || supportedLanguages.isEmpty()) {
supportedLanguages = getSupportedLanguages(monitor);
if (supportedLanguages != null && !supportedLanguages.isEmpty()) {
supportedLanguages.add(0,
new SupportedLanguage("Autodetect", "auto", List.of()));
}
}
}
catch (CancelledException e) {
throw new IOException("Failed to get supported language list: request cancelled");
}
}
/**
* Returns a list of languages that the LibreTranslate server supports.
*
* @param monitor {@link TaskMonitor}
* @return list of {@link SupportedLanguage} records
* @throws IOException if error connecting or excessive time to respond
* @throws CancelledException if cancelled
*/
public List<SupportedLanguage> getSupportedLanguages(TaskMonitor monitor)
throws IOException, CancelledException {
HttpRequest request =
request("languages").header(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON).GET().build();
String jsonResponseStr = asyncRequest(request, BodyHandlers.ofString(), monitor);
try {
// [ { "code": "en", "name": "English", "targets": ["ar", ...
JsonArray json = JsonParser.parseString(jsonResponseStr).getAsJsonArray();
List<SupportedLanguage> results = new ArrayList<>();
for (int i = 0; i < json.size(); i++) {
SupportedLanguage supportedLang =
parseSupportedLangJson(json.get(i).getAsJsonObject());
results.add(supportedLang);
}
Collections.sort(results, (o1, o2) -> o1.langCode.compareTo(o2.langCode));
return results;
}
catch (IllegalStateException | JsonSyntaxException e) {
throw new IOException("Bad data in json response: " + jsonResponseStr, e);
}
}
private SupportedLanguage parseSupportedLangJson(JsonObject obj) {
if (obj.get("code") == null || obj.get("name") == null || obj.get("targets") == null) {
throw new JsonSyntaxException("Bad json data for supported language");
}
String srcLangCode = obj.get("code").getAsString();
String srcLangName = obj.get("name").getAsString();
JsonArray targets = obj.get("targets").getAsJsonArray();
List<String> targetLangCodes = new ArrayList<>();
targets.forEach(targetEle -> {
targetLangCodes.add(targetEle.getAsString());
});
return new SupportedLanguage(srcLangName, srcLangCode, targetLangCodes);
}
private <T> T asyncRequest(HttpRequest request, BodyHandler<T> bodyHandler, TaskMonitor monitor)
throws CancelledException, IOException {
CompletableFuture<HttpResponse<T>> futureResponse =
HttpClients.getHttpClient().sendAsync(request, bodyHandler);
CancelledListener l = () -> futureResponse.cancel(true);
monitor.addCancelledListener(l);
try {
HttpResponse<T> response = futureResponse.get();
int statusCode = response.statusCode();
if (statusCode != HttpURLConnection.HTTP_OK) {
Msg.debug(this, "HTTP request [%s], response: %d, body: %s".formatted(request.uri(),
statusCode, resultToSafeStr(response.body().toString())));
throw new IOException(
"Bad HTTP result [%d] for request [%s]".formatted(statusCode, request.uri()));
}
String responseContentType =
response.headers().firstValue(CONTENT_TYPE_HEADER).orElse("missing");
if (!CONTENT_TYPE_JSON.equals(responseContentType)) {
throw new IOException(
"Bad content-type in result: [%s]".formatted(responseContentType));
}
return response.body();
}
catch (InterruptedException e) {
throw new CancelledException("Request canceled");
}
catch (ExecutionException e) {
// if possible, unwrap the exception that happened inside the future
Throwable cause = e.getCause();
Msg.error(this, "Error during HTTP request [%s]".formatted(request.uri()), cause);
throw (cause instanceof IOException)
? (IOException) cause
: new IOException("Error during HTTP request", cause);
}
finally {
monitor.removeCancelledListener(l);
}
}
private static BodyPublisher ofJsonEncodedParams(Map<String, Object> params) {
JsonObject obj = new JsonObject();
params.forEach((k, v) -> {
if (v instanceof String str) {
obj.addProperty(k, str);
}
else if (v instanceof Number num) {
obj.addProperty(k, num);
}
else if (v instanceof Boolean bool) {
obj.addProperty(k, bool);
}
else if (v instanceof List list) {
JsonArray jsonArray = new JsonArray();
for (Object listEle : list) {
jsonArray.add(listEle.toString());
}
obj.add(k, jsonArray);
}
});
return BodyPublishers.ofString(obj.toString());
}
private HttpRequest.Builder request(String str) throws IOException {
return request(str, httpTimeout);
}
private HttpRequest.Builder request(String str, long timeoutMS) throws IOException {
try {
return HttpRequest.newBuilder(serverURI.resolve(str))
.timeout(Duration.ofMillis(timeoutMS))
.setHeader("User-Agent", GHIDRA_USER_AGENT);
}
catch (IllegalArgumentException e) {
throw new IOException(e);
}
}
private void showError(String title, String msg, Throwable th) {
String summary = th.getMessage();
if (summary == null || summary.isBlank()) {
summary = th.getClass().getSimpleName();
}
Msg.showError(this, null, title,
"%s: %s\n\nLibreTranslate server URL: %s".formatted(msg, summary, serverURI), th);
}
private String resultToSafeStr(String s) {
if (s.length() > 200) {
s = s.substring(0, 200) + "....";
}
return s;
}
}

View File

@ -29,6 +29,11 @@ import ghidra.util.HelpLocation;
* <p>
* Implementations of this interface are usually done via a Plugin
* and then registered via {@link Plugin}'s registerServiceProvided().
* <p>
* Consumers of this service should expect multiple instance types to be returned from
* {@link PluginTool#getServices(Class)}, and should add a service listener via
* {@link PluginTool#addServiceListener(ghidra.framework.plugintool.util.ServiceListener)}
* if service instances are retained to be notified when service instances are changed.
*/
public interface StringTranslationService {
/**
@ -37,10 +42,10 @@ public interface StringTranslationService {
* @param tool {@link PluginTool}
* @return sorted list of currently enabled StringTranslationServices
*/
public static List<StringTranslationService> getCurrentStringTranslationServices(
static List<StringTranslationService> getCurrentStringTranslationServices(
PluginTool tool) {
List<StringTranslationService> translationServices =
new ArrayList<>(Arrays.asList(tool.getServices(StringTranslationService.class)));
new ArrayList<>(List.of(tool.getServices(StringTranslationService.class)));
Collections.sort(translationServices,
(s1, s2) -> s1.getTranslationServiceName().compareTo(s2.getTranslationServiceName()));
return translationServices;
@ -52,7 +57,7 @@ public interface StringTranslationService {
*
* @return string name.
*/
public String getTranslationServiceName();
String getTranslationServiceName();
/**
* Returns the {@link HelpLocation} instance that describes where to direct the user
@ -60,7 +65,7 @@ public interface StringTranslationService {
*
* @return {@link HelpLocation} instance or null.
*/
public default HelpLocation getHelpLocation() {
default HelpLocation getHelpLocation() {
return null;
}
@ -71,13 +76,21 @@ public interface StringTranslationService {
*
* @param program the program containing the data instances.
* @param stringLocations {@link List} of string locations.
* @param options {@link TranslateOptions}
*/
public void translate(Program program, List<ProgramLocation> stringLocations,
void translate(Program program, List<ProgramLocation> stringLocations,
TranslateOptions options);
/**
* Options that are given by the callers of
* {@link StringTranslationService#translate(Program, List, TranslateOptions)}.
*
* @param autoTranslate boolean flag, if true the translation service instance should try
* to translate the values without user interaction
*/
public record TranslateOptions(boolean autoTranslate) {
public static TranslateOptions NONE = new TranslateOptions(false);
};
}
/**
* Helper that creates a {@link HelpLocation} based on the plugin and sts.
@ -87,7 +100,7 @@ public interface StringTranslationService {
* @return HelpLocation with topic equal to the plugin name and anchor something like
* "MyTranslationServiceName_String_Translation_Service".
*/
public static HelpLocation createStringTranslationServiceHelpLocation(
static HelpLocation createStringTranslationServiceHelpLocation(
Class<? extends Plugin> pluginClass, StringTranslationService sts) {
return new HelpLocation(PluginDescription.getPluginDescription(pluginClass).getName(),
sts.getTranslationServiceName() + "_String_Translation_Service");

View File

@ -0,0 +1,512 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.app.plugin.core.string.translate.libretranslate;
import static ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslatePlugin.SOURCE_LANGUAGE_OPTION.*;
import static ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslateStringTranslationService.*;
import static java.net.HttpURLConnection.*;
import static org.junit.Assert.*;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import com.google.gson.*;
import com.sun.net.httpserver.*;
import docking.AbstractErrDialog;
import docking.widgets.SelectFromListDialog;
import ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslatePlugin.SOURCE_LANGUAGE_OPTION;
import ghidra.app.plugin.core.string.translate.libretranslate.LibreTranslateStringTranslationService.SupportedLanguage;
import ghidra.app.services.StringTranslationService.TranslateOptions;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.data.*;
import ghidra.program.model.listing.Data;
import ghidra.program.util.ProgramLocation;
import ghidra.test.AbstractProgramBasedTest;
import ghidra.test.ToyProgramBuilder;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* Tests the LibreTranslateStringTranslationService by creating a mock HTTP server that responds
* to the minimal requests the service needs.
*/
public class LibreTranslateStringTranslationServiceTest extends AbstractProgramBasedTest {
private static int LAST_SERVER_PORT_NUM = 8000 + 5000;
private int supportedLanguageCount = 10;
private AtomicInteger translateRequestCount = new AtomicInteger(); // number of times translate handler has been invoked
private AtomicInteger translateStringCount = new AtomicInteger(); // number of strings that translate handler has processed
private List<String> translateSourceLangs = Collections.synchronizedList(new ArrayList<>());
private List<Data> strings = new ArrayList<>();
@Test
public void testWrongServerURL() throws IOException {
// test what happens when the server returns 404's for the REST api requests
HttpServer server = createMockHttpServer();
try {
server.start();
LibreTranslateStringTranslationService sts =
new LibreTranslateStringTranslationService(getURI(server.getAddress()), null,
AUTO, "en", 100, 1000, 1000);
setErrorsExpected(true); // don't kill the test because Msg.showError() was called somewhere
Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE));
setErrorsExpected(false);
waitForTasks();
// we SHOULD get an error dialog because got bad http status result
AbstractErrDialog errDlg = waitForErrorDialog();
errDlg.dispose();
}
finally {
server.stop(0);
}
}
@Test
public void testIncompatibleJSonResponse() throws IOException {
// test what happens when the server accepts requests on the REST api endpoint URL, but
// returns unexpected json values
HttpServer server = createMockHttpServer(false);
server.createContext("/", this::mockUnexpectedJsonResultHandler);
try {
server.start();
LibreTranslateStringTranslationService sts =
new LibreTranslateStringTranslationService(getURI(server.getAddress()), null,
AUTO, "en", 100, 1000, 1000);
setErrorsExpected(true); // don't kill the test because Msg.showError() was called somewhere
Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE));
setErrorsExpected(false);
waitForTasks();
// we SHOULD get an error dialog because got bad http status result
AbstractErrDialog errDlg = waitForErrorDialog();
errDlg.dispose();
}
finally {
server.stop(0);
}
}
@Test
public void testIncompatibleServerURL() throws IOException {
// test what happens when the server accepts requests on the REST api endpoint URL, but its
// not json
HttpServer server = createMockHttpServer(false);
server.createContext("/", this::mockUnexpectedTextResultHandler);
try {
server.start();
LibreTranslateStringTranslationService sts =
new LibreTranslateStringTranslationService(getURI(server.getAddress()), null,
AUTO, "en", 100, 1000, 1000);
setErrorsExpected(true); // don't kill the test because Msg.showError() was called somewhere
Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE));
setErrorsExpected(false);
waitForTasks();
// we SHOULD get an error dialog because got bad http status result
AbstractErrDialog errDlg = waitForErrorDialog();
errDlg.dispose();
}
finally {
server.stop(0);
}
}
@Test
public void testNoResponseFromURL() {
// test what happens when the URL doesn't point to active server
LibreTranslateStringTranslationService sts = new LibreTranslateStringTranslationService(
getURI(nextUnusedAddr()), null, AUTO, "en", 100, 1000, 1000);
setErrorsExpected(true); // don't kill the test because Msg.showError() was called somewhere
Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE));
setErrorsExpected(false);
waitForTasks();
// we SHOULD get an error dialog because got bad http status result
AbstractErrDialog errDlg = waitForErrorDialog();
errDlg.dispose();
}
@Test
public void testTranslateRequest() throws IOException {
HttpServer server = createMockHttpServer();
server.createContext("/translate", this::mockTranslateHandler);
try {
server.start();
LibreTranslateStringTranslationService sts =
new LibreTranslateStringTranslationService(getURI(server.getAddress()), null,
AUTO, "en", 100, 1000, 1000);
Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE));
assertEquals(translateRequestCount.get(), 1);
String xlatedStr =
TranslationSettingsDefinition.TRANSLATION.getTranslatedValue(strings.get(0));
assertEquals("result0", xlatedStr);
}
finally {
server.stop(0);
}
}
@SuppressWarnings("unchecked")
@Test
public void testPromptForSourceLangTranslateRequest() throws IOException, CancelledException {
HttpServer server = createMockHttpServer();
server.createContext("/translate", this::mockTranslateHandler);
server.createContext("/languages", this::mockLangHandler);
try {
server.start();
LibreTranslateStringTranslationService sts =
new LibreTranslateStringTranslationService(getURI(server.getAddress()), null,
SOURCE_LANGUAGE_OPTION.PROMPT, "en", 100, 1000, 1000);
List<SupportedLanguage> langs = sts.getSupportedLanguages(TaskMonitor.DUMMY);
SupportedLanguage langToPick = langs.get(1);
Swing.runLater(
() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE));
@SuppressWarnings({ "rawtypes" })
SelectFromListDialog selectDlg = waitForDialogComponent(SelectFromListDialog.class);
Swing.runNow(() -> selectDlg.setSelectedObject(langToPick.getDescription()));
Swing.runNow(() -> pressButtonByText(selectDlg, "OK"));
waitForTasks();
assertEquals(translateRequestCount.get(), 1);
assertEquals(1, translateSourceLangs.size());
assertEquals(langToPick.langCode(), translateSourceLangs.get(0));
}
finally {
server.stop(0);
}
}
@Test(timeout = 20000)
public void testTimeoutTranslateRequest() throws IOException {
HttpServer server = createMockHttpServer();
server.createContext("/translate", wrapHandlerWithDelay(this::mockTranslateHandler, 5000));
try {
server.start();
LibreTranslateStringTranslationService sts = new LibreTranslateStringTranslationService(
getURI(server.getAddress()), null, AUTO, "en", 100, 1000, 1);
Swing.runLater(() -> {
setErrorsExpected(true); // don't kill the test because Msg.showError() was called somewhere
sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE);
setErrorsExpected(false);
});
waitForTasks();
// we SHOULD get an error dialog because we forced the http request to timeout
AbstractErrDialog errDlg = waitForErrorDialog();
errDlg.dispose();
}
finally {
server.stop(0);
}
}
@Test(timeout = 20000)
public void testRetryTranslateRequest() throws IOException {
HttpServer server = createMockHttpServer();
server.createContext("/translate", wrapHandlerWithDelay(this::mockTranslateHandler, 2000));
try {
server.start();
LibreTranslateStringTranslationService sts = new LibreTranslateStringTranslationService(
getURI(server.getAddress()), null, AUTO, "en", 100, 1500, 1);
Swing.runNow(() -> sts.translate(program, List.of(progLoc(0)), TranslateOptions.NONE));
assertTrue(translateRequestCount.get() > 1);
waitForTasks();
}
finally {
server.stop(0);
}
}
@Test
public void testBatchTranslateRequest() throws IOException {
int batchSize = 2;
HttpServer server = createMockHttpServer();
server.createContext("/translate", this::mockTranslateHandler);
try {
server.start();
LibreTranslateStringTranslationService sts =
new LibreTranslateStringTranslationService(getURI(server.getAddress()), null, AUTO,
"en", batchSize, 1000, 1000);
List<ProgramLocation> stringLocs = strings.stream()
.map(data -> new ProgramLocation(program, data.getAddress()))
.toList();
int expectedBatchCount = stringLocs.size() / batchSize;
expectedBatchCount += (stringLocs.size() % batchSize != 0 ? 1 : 0);
Swing.runNow(() -> sts.translate(program, stringLocs, TranslateOptions.NONE));
assertEquals(translateRequestCount.get(), expectedBatchCount);
assertEquals(translateStringCount.get(), stringLocs.size());
waitForTasks();
}
finally {
server.stop(0);
}
}
@Test
public void testLanguagesRequest() throws IOException, CancelledException {
HttpServer server = createMockHttpServer();
server.createContext("/languages", this::mockLangHandler);
try {
server.start();
LibreTranslateStringTranslationService sts = new LibreTranslateStringTranslationService(
getURI(server.getAddress()), null, AUTO, "en", 100, 1000, 1000);
List<SupportedLanguage> langs = sts.getSupportedLanguages(TaskMonitor.DUMMY);
assertEquals(supportedLanguageCount, langs.size());
}
finally {
server.stop(0);
}
}
//---------------------------------------------------------------------------------------------
@Before
public void setUp() throws Exception {
initialize();
}
@Override
protected ProgramDB getProgram() throws Exception {
ToyProgramBuilder builder = new ToyProgramBuilder("String Examples", false);
builder.createMemory("RAM", "0x0", 0x500);
strings.add(builder.createString("0x100", "Hello World!\n", StandardCharsets.US_ASCII, true,
StringDataType.dataType));
strings.add(builder.createString("0x10e", "Next string", StandardCharsets.US_ASCII, true,
StringDataType.dataType));
strings.add(builder.createString("0x150", bytes(0, 1, 2, 3, 4, 0x80, 0x81, 0x82, 0x83),
StandardCharsets.US_ASCII, StringDataType.dataType));
strings.add(
builder.createString("0x200", "\u6211\u96bb\u6c23\u588a\u8239\u88dd\u6eff\u6652\u9c54",
StandardCharsets.UTF_16, true, UnicodeDataType.dataType));
strings.add(builder.createString("0x250", "Exception %s\n\tline: %d\n",
StandardCharsets.US_ASCII, true, StringDataType.dataType));
strings.add(builder.createString("0x330",
"A: \u6211\u96bb\u6c23\u588a\u8239\u88dd\u6eff\u6652\u9c54", StandardCharsets.UTF_8,
true, StringUTF8DataType.dataType));
strings.add(builder.createString("0x450",
"Roses are \u001b[0;31mred\u001b[0m, violets are \u001b[0;34mblue. Hope you enjoy terminal hue",
StandardCharsets.US_ASCII, true, StringDataType.dataType));
return builder.getProgram();
}
private URI getURI(InetSocketAddress addr) {
return URI.create("http://%s:%d".formatted(addr.getHostString(), addr.getPort()));
}
private HttpServer createMockHttpServer() throws IOException {
return createMockHttpServer(true);
}
private HttpServer createMockHttpServer(boolean addDefaultHandler) throws IOException {
IOException lastException = null;
for (int retryNum = 0; retryNum < 10; retryNum++) {
LAST_SERVER_PORT_NUM++; // don't try to reuse the same server port num in the same session
InetSocketAddress serverAddress =
new InetSocketAddress(InetAddress.getLoopbackAddress(), LAST_SERVER_PORT_NUM);
try {
HttpServer server = HttpServer.create(serverAddress, 0);
if (addDefaultHandler) {
server.createContext("/", this::mock404Handler);
}
return server;
}
catch (IOException e) {
// ignore, just try again with next port num
lastException = e;
}
}
throw new IOException(
"Could not allocate port for mock http server, last attempted port: " +
LAST_SERVER_PORT_NUM,
lastException);
}
private InetSocketAddress nextUnusedAddr() {
LAST_SERVER_PORT_NUM++;
return new InetSocketAddress(InetAddress.getLoopbackAddress(), LAST_SERVER_PORT_NUM);
}
private void assertContentType(HttpExchange httpExchange, String expectedType) {
String contentType = httpExchange.getRequestHeaders()
.getFirst(LibreTranslateStringTranslationService.CONTENT_TYPE_HEADER);
contentType = Objects.requireNonNullElse(contentType, "missing");
if (!expectedType.equals(contentType)) {
fail("Content type incorrect: expected: %s, actual: %s".formatted(expectedType,
contentType));
}
}
private void log(HttpExchange httpExchange, String msg) {
Msg.info(this, "[%s %s] %s".formatted(httpExchange.getLocalAddress(),
httpExchange.getRequestURI(), msg));
}
private void mockLangHandler(HttpExchange httpExchange) throws IOException {
assertContentType(httpExchange, CONTENT_TYPE_JSON);
try {
JsonArray langsResult = new JsonArray();
for (int i = 0; i < supportedLanguageCount; i++) {
JsonObject obj = new JsonObject();
obj.addProperty("code", "%c%c".formatted('a' + i, 'a' + i));
obj.addProperty("name", "Language " + i);
JsonArray targets = new JsonArray();
obj.add("targets", targets);
langsResult.add(obj);
}
byte[] response = langsResult.toString().getBytes();
httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON);
httpExchange.sendResponseHeaders(HTTP_OK, response.length);
httpExchange.getResponseBody().write(response);
}
finally {
httpExchange.close();
}
}
private void mockTranslateHandler(HttpExchange httpExchange) throws IOException {
try {
translateRequestCount.incrementAndGet();
assertContentType(httpExchange, CONTENT_TYPE_JSON);
String requestBody =
new String(httpExchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
JsonObject request = JsonParser.parseString(requestBody).getAsJsonObject();
JsonArray queryStrs = request.get("q").getAsJsonArray();
String sourceLang = request.get("source").getAsString();
translateSourceLangs.add(sourceLang);
log(httpExchange,
"request src=%s, strs=%s".formatted(sourceLang, queryStrs.toString()));
JsonObject xlateResultObj = new JsonObject();
JsonArray xlatedResults = new JsonArray();
xlateResultObj.add("translatedText", xlatedResults);
for (int i = 0; i < queryStrs.size(); i++) {
xlatedResults.add("result" + translateStringCount.getAndIncrement());
}
log(httpExchange, "response: " + xlateResultObj);
byte[] response = xlateResultObj.toString().getBytes();
httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON);
httpExchange.sendResponseHeaders(HTTP_OK, response.length);
httpExchange.getResponseBody().write(response);
}
catch (Throwable th) {
log(httpExchange, "Error during mockTranslateHandler: " + th.getMessage());
throw th;
}
finally {
httpExchange.close();
}
}
private void mockUnexpectedJsonResultHandler(HttpExchange httpExchange) throws IOException {
JsonObject jsonObj = new JsonObject();
jsonObj.addProperty("something", "an unexpected value");
byte[] response = jsonObj.toString().getBytes();
httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON);
httpExchange.sendResponseHeaders(HTTP_OK, response.length);
httpExchange.getResponseBody().write(response);
httpExchange.close();
}
private void mockUnexpectedTextResultHandler(HttpExchange httpExchange) throws IOException {
// this returns OK for every URL
byte[] response = "Hello world".toString().getBytes();
httpExchange.getResponseHeaders().set(CONTENT_TYPE_HEADER, "text/plain");
httpExchange.sendResponseHeaders(HTTP_OK, response.length);
httpExchange.getResponseBody().write(response);
httpExchange.close();
}
private HttpHandler wrapHandlerWithDelay(HttpHandler delegate, int delayMS) {
return httpExchange -> {
try {
Thread.sleep(delayMS);
}
catch (InterruptedException e) {
// ignore
}
delegate.handle(httpExchange);
};
}
private void mock404Handler(HttpExchange httpExchange) throws IOException {
try {
httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, 0);
}
finally {
httpExchange.close();
}
}
private ProgramLocation progLoc(int stringNum) {
return new ProgramLocation(program, strings.get(stringNum).getAddress());
}
}