idea插件开发--服务-翻译插件

idea插件开发--服务-翻译插件,第1张

idea插件开发--服务-翻译插件

idea插件开发--服务-翻译插件
  • 介绍
  • 服务
  • 轻量级服务
  • 服务定义
  • 服务获取
  • 实例
    • 目标
    • 分解
    • 准备在线翻译信息
      • 有道翻译
      • 必应翻译
      • 百度翻译
    • 创建插件项目
    • 创建配置界面
    • 引入第三方依赖
    • 定义存储的服务
    • 定义配置界面
    • 创建Action
    • 封装抽象RestAPI
      • 有道翻译
      • 百度翻译
      • 厂商扩展
    • 编写Action后续 *** 作
    • 效果
    • 打包
    • 最后的最后
  • 总结

gitee地址:https://gitee.com/jyq_18792721831/studyplugin.git
idea插件开发入门
idea插件开发–配置
idea插件开发–服务-翻译插件

介绍

本次主要介绍idea中服务的相关内容,包括服务的种类,服务的定义,服务的获取,以及服务的使用。

之后综合idea插件的Action和简单配置,实现一个较为实用的翻译小插件,以此复习和巩固idea插件的Action和简单配置。

服务

在spring中,服务一般是单例的,使用起来也比较方便,自动注入。

idea插件平台也提供了类似的解决方案,允许我们创建单例的服务,然后在使用的时候获取。

在idea插件项目中,通过com.intellij.openapi.components.ComponentManager接口获取,服务会在第一次调用的时候创建一个实例,而且在作用域范围内,保证只有一个实例。服务应该实现Disposable接口,用于注销服务。

ComponentManager接口有这几个实现类

最长用的也就是Application了

idea提供三种类型的服务:application,prject和module级别的服务。其作用域分别是全局,项目和模块。模块级别的服务需要慎用,因为当项目中模块比较多的时候,会占用较多的内存和资源。

对于project和module级别的服务,可以注入project和module的对象,注入方式为在构造函数中增加Project和Module参数。因为这个注入的构造函数主要是用于参数注入,所以在使用的时候,尽可能避免在自己的代码中调用。

轻量级服务

在2019.3版本之后,增加了另一种轻量级的服务,轻量级服务不需要在plugin.xml中定义,只需要增加@Service注解即可。

  • 轻量级服务必须是final修饰
  • 轻量级服务不推荐使用构造函数注入(根据文档给出的示例,project对象还是能够使用构造函数注入)
  • 如果服务用于存储(PersistentStateComponent,那么需要增加参数roamingType=RoamingtType.DISABLED)
服务定义

如果不是轻量级服务,那么需要在plugin.xml中定义,定义需要在extensions节点下定义。定义不同作用域的服务,使用不同的标签:applicationService,projectService,moduleService

定义服务,接口不是必须的,如果没有接口,把接口和实例属性设置成实现类就行。

服务获取

可以使用ComponentManager接口的实例获取接口,常使用ApplicationManager获取。

实例 目标

实现一个翻译插件,说实话,本人英语水平是在有限,所以在开发编码的时候,有时候给变量起名字,就需要翻译好,在拷贝过来。

当然,现在在插件市场上也有许许多多的翻译插件,做的功能齐全,使用方便。

我们这主要是学习插件开发,翻译插件逻辑也不复杂,正好作为一个练手的项目。

需求:在编辑窗口,选中需要翻译的中文,按下快捷键,翻译为英文,并转为驼峰形式,替换选中的中文。

分解
  1. 我们需要增加编辑窗口的Action,而且需要有快捷键
  2. 需要获取选中的中文
  3. 需要翻译接口
  4. 配置在线翻译接口的参数
  5. 得到翻译的英文,处理为驼峰形式
  6. 替换编辑窗口选中的中文
准备在线翻译信息 有道翻译

在有道智云AI开放平台 (youdao.com)注册账号,注册送50人民币,自己玩足够了。

然后创建文本翻译的应用

在个人信息里能看到应用id和秘钥

必应翻译

在Bing for Partners helps businesses and developers succeed注册账号,必应在线文本翻译每月有免费的数量,个人使用完全足够

有了账号后,根据快速入门:Translator 入门 - Azure Cognitive Services | Microsoft Docs选择文本翻译即可

注册需要visa卡等进行验证,如果没有就跳过(我就没有)

百度翻译

在百度翻译开放平台 (baidu.com)注册账号,选择通用翻译

这里需要进行实名认证,并注册为个人开发者,然后在控制台就能看到自己选择的服务了

在最下面有应用id和秘钥

创建插件项目

创建如下项目

在plugin.xml中定义好插件的信息

创建配置界面

在ui包下创建配置的信息

然后通过拖动的方式增加控件

需要注意,使用密码输入框,而不是文本输入框

记得选择生成源代码

在源代码中,我们增加方法,用于获取数据,这样就不把控件进行暴露了

编译才会生成源代码

然后生成测试ui的main方法(需要给最外层的JPanel设置属性名字)

运行main方法就可以看看我们的界面效果了

需要注意,我们需要将最外层的JPannel暴露到外面,虽然自己生成了一个暴露最外层JPannel的方法,但是不介意使用。

如果用户是修改配置,我们还需要增加方法,用于设置控件的值

引入第三方依赖

我们使用lombok注解进行暴露,要是用lombok就需要在项目中加入lombok的依赖。

还记得我们的项目结构中,有个lib的文件夹。

lib文件夹就是放第三方依赖的jar包的。

首选需要在Maven Central Repository Search搜索lombok插件,然后下载jar包,并将jar拷贝到lib目录下。

然后将增加的jar包加入项目

当然,其他第三方jar包也是这样增加的。

定义存储的服务

我们使用之前说的最简单的存储方式,然后对这种方式进行封装。服务使用轻量级的服务,直接使用注解,也不需要实现注销的方法。

import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.components.Service;

@Service
public final class TranslateAppInfoService {

    private final PropertiesComponent propertiesComponent = PropertiesComponent.getInstance();

    public void save(String key, String value) {
        propertiesComponent.setValue(key, value);
    }

    public String get(String key, String defaultValue) {
        return propertiesComponent.getValue(key, defaultValue);
    }

    public String get(String key) {
        return get(key, "");
    }

}

我们封装三个方法,一个是存储,一个是获取,一个是带有默认值的获取。

很简单,这里定义的存储服务,会在SearchableConfiguable的实现类中使用。

定义配置界面

我们创建好了配置界面的UI后,还需要配置到setting下,idea插件开发–配置_a18792721831的博客-CSDN博客

首先创建SearchableConfigurable接口的实现类,传输配置id,配置名字。

在定义配置界面的时候,首先从存储服务中获取已经保存的配置,然后把配置放入控件中,因为用户可能只想修改一部分,如果不设置,就会被空值覆盖,而且不设置,用户也不知道哪些已经配置过了。所以需要在创建好控件from后,获取已有配置,设置到控件。

如果用户根本无修改,此时给isModified方法返回false,表示应用按钮不可用,无修改,无需保存,无需调用apply方法。

在apply方法中,则是将控件中输入的值,调用存储服务,存储起来。

完整代码如下

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.openapi.util.text.StringUtil;
import com.study.plugin.translate.service.TranslateAppInfoService;
import com.study.plugin.translate.ui.TranslateConfigUI;
import com.study.plugin.translate.utils.PluginAppKeys;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.JComponent;

public class TranslateAppInfoConfig implements SearchableConfigurable, PluginAppKeys {

    private TranslateConfigUI ui = new TranslateConfigUI();

    private TranslateAppInfoService appInfoService = ApplicationManager.getApplication().getService(TranslateAppInfoService.class);

    @Override
    public @NotNull
    @NonNls
    String getId() {
        return PLUGIN_CONFIG_ID;
    }

    @Override
    public @NlsContexts.ConfigurableName String getDisplayName() {
        return PLUGIN_CONFIG_NAME;
    }

    @Override
    public @Nullable
    JComponent createComponent() {
        ui.setYoudaoAppId(appInfoService.get(YOUDAO_APP_ID_SAVE_KEY, ""));
        return ui.getRootJPanel();
    }

    @Override
    public boolean isModified() {
        if (!appInfoService.get(YOUDAO_APP_ID_SAVE_KEY).equals(ui.getYoudaoAppId()) ||
                !appInfoService.get(YOUDAO_APP_SECRET_SAVE_KEY).equals(ui.getYoudaoAppSecret()) ||
                !appInfoService.get(BIYING_APP_ID_SAVE_KEY).equals(ui.getBiyingAppId()) ||
                !appInfoService.get(BIYING_APP_SECRET_SAVE_KEY).equals(ui.getBiyingAppSecret()) ||
                !appInfoService.get(BAIDU_APP_ID_SAVE_KEY).equals(ui.getBaiduAppId()) ||
                !appInfoService.get(BAIDU_APP_SECRET_SAVE_KEY).equals(ui.getBaiduAppSecret())) {
            return true;
        }
        return false;
    }

    @Override
    public void apply() throws ConfigurationException {
        String youdaoAppId = ui.getYoudaoAppId();
        String youdaoAppSecret = ui.getYoudaoAppSecret();
        if (StringUtil.isNotEmpty(youdaoAppId) && StringUtil.isNotEmpty(youdaoAppSecret)) {
            appInfoService.save(YOUDAO_APP_ID_SAVE_KEY, youdaoAppId);
            appInfoService.save(YOUDAO_APP_SECRET_SAVE_KEY, youdaoAppSecret);
        }
        String biyingAppId = ui.getBiyingAppId();
        String biyingAppSecret = ui.getBiyingAppSecret();
        if (StringUtil.isNotEmpty(biyingAppId) && StringUtil.isNotEmpty(biyingAppSecret)) {
            appInfoService.save(BIYING_APP_ID_SAVE_KEY, biyingAppId);
            appInfoService.save(BIYING_APP_SECRET_SAVE_KEY, biyingAppSecret);
        }
        String baiduAppId = ui.getBaiduAppId();
        String baiduAppSecret = ui.getBaiduAppSecret();
        if (StringUtil.isNotEmpty(baiduAppId) && StringUtil.isNotEmpty(baiduAppSecret)) {
            appInfoService.save(BAIDU_APP_ID_SAVE_KEY, baiduAppId);
            appInfoService.save(BAIDU_APP_SECRET_SAVE_KEY, baiduAppSecret);
        }
    }
}

最后别忘记在plugin.xml中注册

  
    
    
    
  

当做好这些后,就可以调试一下之前写的代码了

还是很不错的,简单明了,记得测试下存储服务是否正常。

创建Action

创建Action很简单,之前就用过:idea插件开发入门_a18792721831的博客-CSDN博客

我们创建Action,快捷键还是使用ctrl+alt+;

在触发后,首先获取选中的文本,然后调用翻译的服务(假设我们已经写好了一个翻译的RestAPI)

以此来触发翻译,等翻译至少有一个可用时,在回头基础开发这里的 *** 作。

封装抽象RestAPI

因为我们使用的都是在线API的方式请求的,所以需要使用URL请求。

为了使用更加方便,我们使用spring的restTemplate接口进行请求。

首先从maven仓库下载spring-beans,spring-context,spring-web,spring-core四个依赖,并加入项目。

然后封装Rest请求的抽象类。

抽象类中主要是restTemplate的对象和存储服务的对象,因为对所有的各个厂商的在线翻译平台来说,我们的restTemplate和存储服务使用同一个就可以了,而且我们定义子类必须实现翻译方法,翻译方法传入待翻译的中文,返回翻译后的英文或者空串。

一些公共的工具方法,也可以放在抽象类中,比如加密

import com.intellij.openapi.application.ApplicationManager;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

public abstract class TranslateRestService {

    protected RestTemplate restTemplate;

    protected volatile AtomicBoolean isInit = new AtomicBoolean(Boolean.FALSE);

    protected TranslateAppInfoService appInfoService = ApplicationManager.getApplication().getService(TranslateAppInfoService.class);

    protected synchronized void init() {
        // 如果已经初始化了,直接结束
        if (isInit.get()) {
            return;
        }
        // 连接池
        PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager();
        poolingHttpClientConnectionManager.setMaxTotal(4);
        poolingHttpClientConnectionManager.setDefaultMaxPerRoute(2);
        // 我们目前只有2个在线翻译可用,每个翻译2个线程用于Rest请求,所以设置最大连接4,每个翻译api是2个并发
        // 客户端构造器
        HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
        httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
        // 创建restTemplate
        HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory();
        httpRequestFactory.setHttpClient(httpClientBuilder.build());
        httpRequestFactory.setConnectTimeout(6000);
        httpRequestFactory.setConnectTimeout(6000);
        httpRequestFactory.setReadTimeout(12000);
        RestTemplate restTemplate = new RestTemplate(httpRequestFactory);
        this.restTemplate = restTemplate;
        isInit.compareAndSet(Boolean.FALSE, Boolean.TRUE);
    }

    
    protected static String getDigest(String string, String key) {
        if (string == null) {
            return null;
        }
        char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        byte[] btInput = string.getBytes(StandardCharsets.UTF_8);
        try {
            MessageDigest mdInst = MessageDigest.getInstance(key);
            mdInst.update(btInput);
            byte[] md = mdInst.digest();
            int j = md.length;
            char str[] = new char[j * 2];
            int k = 0;
            for (byte byte0 : md) {
                str[k++] = hexDigits[byte0 >>> 4 & 0xf];
                str[k++] = hexDigits[byte0 & 0xf];
            }
            return new String(str);
        } catch (NoSuchAlgorithmException e) {
            return null;
        }
    }

    public abstract String translate(String word);
}

当抽象方法完成后,就需要针对各个厂商实现翻译的子类。

有道翻译

根据有道翻译的api产品文档-自然语言翻译服务 (youdao.com),根据里面的示例程序,拷贝相关的请求参数封装的代码到子类中,然后调用父类的存储服务,进行请求app_id,app_secret的读取,并使用父类的restTemplate进行请求,并返回。

import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.YoudaoTranslateResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.util.HashMap;
import java.util.Map;

@Service
public final class YoudaoTranslateRestService extends TranslateRestService implements PluginAppKeys {

    private String HOST = "https://openapi.youdao.com/api";

    private String APP_ID = appInfoService.get(YOUDAO_APP_ID_SAVE_KEY);

    private String APP_SECRET = appInfoService.get(YOUDAO_APP_SECRET_SAVE_KEY);

    private String DIGEST_KEY = "SHA-256";

    public YoudaoTranslateRestService() {
        super();
        if (!isInit.get()) {
            super.init();
        }
    }


    @Override
    public String translate(String word) {
        Map params = getParams(word);
        StringBuilder builder = new StringBuilder(HOST + "?");
        params.entrySet().forEach(ent -> {
            builder.append(ent.getKey() + "=" + ent.getValue() + "&");
        });
        String requestUrl = builder.toString();
        requestUrl = requestUrl.substring(0, requestUrl.length() - 1);
        YoudaoTranslateResult result = restTemplate.getForObject(requestUrl, YoudaoTranslateResult.class);
        if (result.getErrorCode().equals("0")) {
            return result.getTranslation().get(0);
        }
        return null;
    }

    private Map getParams(String word) {
        Map params = new HashMap<>();
        String salt = String.valueOf(System.currentTimeMillis());
        params.put("from", "auto");
        params.put("to", "en");
        params.put("signType", "v3");
        String curtime = String.valueOf(System.currentTimeMillis() / 1000);
        params.put("curtime", curtime);
        String signStr = APP_ID + truncate(word) + salt + curtime + APP_SECRET;
        String sign = getDigest(signStr, DIGEST_KEY);
        params.put("appKey", APP_ID);
        params.put("q", word);
        params.put("salt", salt);
        params.put("sign", sign);
        return params;
    }

    public static String truncate(String q) {
        if (q == null) {
            return null;
        }
        int len = q.length();
        return len <= 20 ? q : (q.substring(0, 10) + len + q.substring(len - 10, len));
    }
}

不要忘记把子类定义为轻量级的服务。这里我们还没有做英文单词的驼峰化。

这里需要将返回值封装为对象,根据文档中给出的返回信息,我们只需要处理一定返回的项目即可。

所以,增加有道返回的对象:

调用这些接口,可能出现各种问题,需要找厂商的客服进行调试。

百度翻译

百度翻译也差不多,根据百度翻译开放平台 (baidu.com)文档,找到示例程序,拷贝到子类,进行调用。

import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.BaiduTranslateResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.springframework.util.CollectionUtils;

@Service
public final class BaiduTranslateRestService extends TranslateRestService implements PluginAppKeys {

    private String HOST = "http://api.fanyi.baidu.com/api/trans/vip/translate";

    private String APP_ID = appInfoService.get(BAIDU_APP_ID_SAVE_KEY);

    private String APP_SECRET = appInfoService.get(BAIDU_APP_SECRET_SAVE_KEY);

    private String DIGEST_KEY = "MD5";

    public BaiduTranslateRestService() {
        super();
        if (!isInit.get()) {
            super.init();
        }
    }

    @Override
    public String translate(String word) {
        Map params = getParams(word);
        StringBuilder builder = new StringBuilder(HOST + "?");
        params.entrySet().forEach(ent -> {
            builder.append(ent.getKey() + "=" + ent.getValue() + "&");
        });
        String requestUrl = builder.toString();
        requestUrl = requestUrl.substring(0, requestUrl.length() - 1);
        BaiduTranslateResult result = restTemplate.getForObject(requestUrl, BaiduTranslateResult.class);
        if (Objects.isNull(result.getError_code()) && !CollectionUtils.isEmpty(result.getTrans_result())) {
            return result.getTrans_result().get(0).getDst();
        }
        return null;
    }

    private Map getParams(String word) {
        Map params = new HashMap();
        params.put("q", word);
        params.put("from", "zh");
        params.put("to", "en");
        params.put("appid", APP_ID);
        // 随机数
        String salt = String.valueOf(System.currentTimeMillis());
        params.put("salt", salt);
        // 签名
        String src = APP_ID + word + salt + APP_SECRET; // 加密前的原文
        params.put("sign", getDigest(src, DIGEST_KEY).toLowerCase());
        return params;
    }

}

百度翻译的返回对象定义

厂商扩展

上面两个厂商的免费额度有限,或者说因为各种原因,无法使用,那么可以选择另外其他的厂商。

所以厂商的扩展就很有必要。

以DeepL翻译为例,这是一个提供机器翻译的网站,根据介绍是使用机器学习,实现的在线翻译。

当然也需要注册账号信息,获取app_id和app_secret。DeepL翻译API|机器翻译技术

其技术文档在这里:DeepL API

每增加一个厂商,就需要同步增加配置信息。

所以我们根据厂商要求,增加相应的配置界面。

然后在界面中增加数据设置和读取的方法

接着和其他配置相同的处理,在初始化界面时,将已有的值放入,判断是否修改,然后进行保存

然后实现抽象的RestAPI类,定义deepl的子类翻译

import com.intellij.openapi.components.Service;
import com.study.plugin.translate.beans.DeeplResult;
import com.study.plugin.translate.utils.PluginAppKeys;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.groovy.util.Maps;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.util.CollectionUtils;
import org.springframework.util.linkedMultiValueMap;
import org.springframework.util.MultiValueMap;

@Service
public final class DeeplTranslateRestService extends TranslateRestService implements PluginAppKeys {

    private String HOST = "https://api-free.deepl.com/v2/translate";

    private String APP_SECRET = appInfoService.get(DEEPL_APP_SECRET_SAVE_KEY);

    public DeeplTranslateRestService() {
        super();
        if (!isInit.get()) {
            init();
        }
    }

    @Override
    public String translate(String word) {
        Map params = getParams(word);
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add("Content-Type", "application/x-www-form-urlencoded");
        MultiValueMap map = new linkedMultiValueMap<>();
        params.entrySet().forEach(ent -> {
            map.put(ent.getKey(), Collections.singletonList(ent.getValue()));
        });
        RequestEntity> request = new RequestEntity<>(map, httpHeaders, HttpMethod.POST, URI.create(HOST));
        DeeplResult result = restTemplate.postForObject(HOST, request, DeeplResult.class, Maps.of("auth_key", APP_SECRET));
        if (Objects.nonNull(result) && !CollectionUtils.isEmpty(result.getTranslations())) {
            return result.getTranslations().get(0).getText();
        }
        return null;
    }

    private Map getParams(String word) {
        Map params = new HashMap<>();
        params.put("text", word);
        // 非必填
        params.put("source_lang", "ZH");
        params.put("target_lang", "EN-US");
        params.put("auth_key", APP_SECRET);
        return params;
    }
}

需要注意的是我们使用的Service注解是idea-platfrom的,而不是spring的。

然后在Action中调用即可。

暂时我们只是将翻译的结果使用通知输出,实际在Action中还应该对英文单词结果做驼峰化,以及多个厂商之间的调度 *** 作,还有就是需要替换选中的中文。现在还剩下这些未完成,当然这些前提是你至少有一个厂商能进行翻译。

编写Action后续 *** 作

首先我们有多个用于翻译的RestApi,所以我们创建一个调度工具,调度工具也非常简单,就是轮训。

当我们得到了翻译后的英文语句后,需要转为驼峰形式

因为我们翻译可能翻译的是一个词组,当翻译的是词组的时候,返回的就不是单词,而是短语,短语是通过空格分割的,所以我们需要将返回的英文字符串根据空格拆分,然后第一个单词转为小写,取余单词的第一个首字母大写,然后拼接起来就行了

接着我们需要控制什么时候可用翻译功能,当用户没有选中字符的时候,是不能使用字符的

最后一步,我们需要使用翻译后的英文字符串,并且是转为驼峰形式的字符串替换掉选中的中文字符

效果

打包

打包直接使用ide的打包功能即可

打包后的zip包就可以发布给其他人使用了

最后的最后

配置的地方增加点说明,告诉用户该去哪里注册。

增加的文本区不可编辑。

总结

通过这个小插件,学习了idea插件中服务的定义,服务的获取和使用。

通过调用在线翻译API,学习了restTemplate的使用和配置。

通过各个厂商的扩展,进一步理解了抽象,以及抽象类和子类之间的关系,换句话说,这一定程度上增加了我的抽象能力。

而Action的各种逻辑,则是对idea插件平台有了进一步的理解,包括如何替换选中的字符,如何控制插件功能是否可用等等。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5694527.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存