diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/pom.xml b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..9b7452a0993cae37db71e79762c32b817329658f --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/pom.xml @@ -0,0 +1,84 @@ + + + + + dolphinscheduler-alert-plugin + org.apache.dolphinscheduler + ${revision} + + 4.0.0 + + org.apache.dolphinscheduler + dolphinscheduler-alert-slack + dolphinscheduler-plugin + + + + + org.apache.dolphinscheduler + dolphinscheduler-spi + provided + + + + org.apache.httpcomponents + httpclient + + + + com.google.guava + guava + + + + ch.qos.logback + logback-classic + + + + org.slf4j + slf4j-api + + + + com.fasterxml.jackson.core + jackson-annotations + provided + + + + junit + junit + test + + + + org.mockito + mockito-core + jar + test + + + + + dolphinscheduler-alert-slack-${project.version} + + + \ No newline at end of file diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannel.java b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannel.java new file mode 100644 index 0000000000000000000000000000000000000000..6399d8bca9107c763af459ddbd92df6ed5859394 --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannel.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.dolphinscheduler.plugin.alert.slack; + +import org.apache.dolphinscheduler.spi.alert.AlertChannel; +import org.apache.dolphinscheduler.spi.alert.AlertData; +import org.apache.dolphinscheduler.spi.alert.AlertInfo; +import org.apache.dolphinscheduler.spi.alert.AlertResult; + +import java.util.Map; + +/** + * SlackAlertChannel + */ +public class SlackAlertChannel implements AlertChannel { + + @Override + public AlertResult process(AlertInfo alertInfo) { + AlertData alertData = alertInfo.getAlertData(); + Map alertParams = alertInfo.getAlertParams(); + if (alertParams == null || alertParams.size() == 0) { + return new AlertResult("false", "Slack alert params is empty"); + } + SlackSender slackSender = new SlackSender(alertParams); + String response = slackSender.sendMessage(alertData.getTitle(), alertData.getContent()); + return new AlertResult("ok".equals(response) ? "true" : "false", response); + } +} diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannelFactory.java b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannelFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..f3f135ebec306639d33a15f11fc6b060ff717576 --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannelFactory.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.dolphinscheduler.plugin.alert.slack; + +import org.apache.dolphinscheduler.spi.alert.AlertChannel; +import org.apache.dolphinscheduler.spi.alert.AlertChannelFactory; +import org.apache.dolphinscheduler.spi.params.InputParam; +import org.apache.dolphinscheduler.spi.params.base.PluginParams; +import org.apache.dolphinscheduler.spi.params.base.Validate; + +import java.util.LinkedList; +import java.util.List; + +/** + * Slack alert factory, see {@link AlertChannelFactory} + */ +public class SlackAlertChannelFactory implements AlertChannelFactory { + + @Override + public String getName() { + return "Slack"; + } + + @Override + public List getParams() { + List paramsList = new LinkedList<>(); + + InputParam webHookParam = InputParam.newBuilder(SlackParamsConstants.SLACK_WEN_HOOK_URL_NAME, SlackParamsConstants.SLACK_WEB_HOOK_URL) + .addValidate(Validate.newBuilder() + .setRequired(true) + .build()) + .setPlaceholder("Input WebHook Url") + .build(); + + InputParam botName = InputParam.newBuilder(SlackParamsConstants.SLACK_BOT_NAME, SlackParamsConstants.SLACK_BOT) + .addValidate(Validate.newBuilder() + .setRequired(true) + .build()) + .setPlaceholder("Input the bot username") + .build(); + + paramsList.add(webHookParam); + paramsList.add(botName); + return paramsList; + } + + @Override + public AlertChannel create() { + return new SlackAlertChannel(); + } +} diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertPlugin.java b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertPlugin.java new file mode 100644 index 0000000000000000000000000000000000000000..59c45ae4ac96eaa803e4ce5cbd3303448722a0f3 --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertPlugin.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.dolphinscheduler.plugin.alert.slack; + +import org.apache.dolphinscheduler.spi.DolphinSchedulerPlugin; +import org.apache.dolphinscheduler.spi.alert.AlertChannelFactory; + +import com.google.common.collect.ImmutableList; + +/** + * Slack alert plugin + */ +public class SlackAlertPlugin implements DolphinSchedulerPlugin { + + @Override + public Iterable getAlertChannelFactorys() { + return ImmutableList.of(new SlackAlertChannelFactory()); + } +} diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackParamsConstants.java b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackParamsConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..fd191c5d55af1fdd38752bfa086363bb3982193e --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackParamsConstants.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.dolphinscheduler.plugin.alert.slack; + +public class SlackParamsConstants { + + private SlackParamsConstants() { + + } + + public static final String SLACK_WEB_HOOK_URL = "WebHook"; + public static final String SLACK_WEN_HOOK_URL_NAME = "webHook"; + public static final String SLACK_BOT = "Username"; + public static final String SLACK_BOT_NAME = "username"; + public static final String TEXT = "text"; + public static final String ATTACHMENT = "attachments"; + + public static final Integer MAX_SHOW_NUMBER = 100; +} diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSender.java b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSender.java new file mode 100644 index 0000000000000000000000000000000000000000..21a82862116c0fadef10bf1825645b56378779c5 --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/main/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSender.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.dolphinscheduler.plugin.alert.slack; + +import org.apache.dolphinscheduler.spi.utils.JSONUtils; +import org.apache.dolphinscheduler.spi.utils.StringUtils; + +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Preconditions; + +public class SlackSender { + + private static final Logger logger = LoggerFactory.getLogger(SlackSender.class); + + private String webHookUrl; + + private String botName; + + public SlackSender(Map slackAlertParam) { + webHookUrl = slackAlertParam.get(SlackParamsConstants.SLACK_WEN_HOOK_URL_NAME); + botName = slackAlertParam.get(SlackParamsConstants.SLACK_BOT_NAME); + Preconditions.checkArgument(!Objects.isNull(webHookUrl), "SlackWebHookURL can not be null"); + Preconditions.checkArgument(webHookUrl.startsWith("https://hooks.slack.com/services/"), "SlackWebHookURL invalidate"); + Preconditions.checkArgument(!Objects.isNull(botName), "slack bot name can not be null"); + } + + /** + * Send message to slack channel + * + * @param title title + * @param content content + * @return slack response + */ + public String sendMessage(String title, String content) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + Map paramMap = new HashMap<>(); + paramMap.put(SlackParamsConstants.SLACK_BOT_NAME, botName); + paramMap.put(SlackParamsConstants.TEXT, title); + if (StringUtils.isNotEmpty(content)) { + Map attachmentTable = new HashMap<>(); + attachmentTable.put(SlackParamsConstants.TEXT, generateMarkDownTable(content)); + List> attachments = new ArrayList<>(); + attachments.add(attachmentTable); + paramMap.put(SlackParamsConstants.ATTACHMENT, attachments); + } + + HttpPost httpPost = new HttpPost(webHookUrl); + httpPost.setEntity(new StringEntity(JSONUtils.toJsonString(paramMap), "UTF-8")); + CloseableHttpResponse response = httpClient.execute(httpPost); + + HttpEntity entity = response.getEntity(); + return EntityUtils.toString(entity, "UTF-8"); + } catch (Exception e) { + logger.error("Send message to slack error.", e); + return "System Exception"; + } + } + + /** + * Because the slack does not support table we can transform to specific markdown table + * + * @param content sql data content + */ + private String generateMarkDownTable(String content) { + List linkedHashMaps = JSONUtils.toList(content, LinkedHashMap.class); + if (linkedHashMaps.size() > SlackParamsConstants.MAX_SHOW_NUMBER) { + linkedHashMaps = linkedHashMaps.subList(0, SlackParamsConstants.MAX_SHOW_NUMBER); + } + int maxLen = 0; + List headers = new LinkedList<>(); + LinkedHashMap tmp = linkedHashMaps.get(0); + for (Entry entry : tmp.entrySet()) { + maxLen = Math.max(maxLen, entry.getKey().length()); + headers.add(entry.getKey()); + } + List> elements = new ArrayList<>(tmp.size()); + // build header + for (LinkedHashMap linkedHashMap : linkedHashMaps) { + List element = new ArrayList<>(linkedHashMap.size()); + for (Object value : linkedHashMap.values()) { + String valueStr = value.toString(); + maxLen = Math.max(maxLen, valueStr.length()); + element.add(valueStr); + } + elements.add(element); + } + final int elementLen = maxLen; + StringBuilder stringBuilder = new StringBuilder(200); + stringBuilder.append(headers.stream() + .map(header -> generateString(header, elementLen, " ")) + .collect(Collectors.joining("|"))); + stringBuilder.append("\n"); + for (List element : elements) { + stringBuilder.append(element.stream() + .map(lement -> generateString("", elementLen, "-")) + .collect(Collectors.joining("|"))); + stringBuilder.append("\n"); + stringBuilder.append(element.stream() + .map(e -> generateString(e, elementLen, " ")) + .collect(Collectors.joining("|"))); + stringBuilder.append("\n"); + } + return String.format("```%s```", stringBuilder); + } + + private String generateString(String value, int len, String supplement) { + StringBuilder stringBuilder = new StringBuilder(len); + stringBuilder.append(value); + for (int i = 0; i < len - stringBuilder.length(); i++) { + stringBuilder.append(supplement); + } + return stringBuilder.toString(); + } +} diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannelFactoryTest.java b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannelFactoryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e294365da76084ab2a6169502f3c79eb317d6208 --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertChannelFactoryTest.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.dolphinscheduler.plugin.alert.slack; + +import org.apache.dolphinscheduler.spi.alert.AlertChannel; +import org.apache.dolphinscheduler.spi.params.base.PluginParams; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Test; + +public class SlackAlertChannelFactoryTest { + + private SlackAlertChannelFactory slackAlertChannelFactory = new SlackAlertChannelFactory(); + + @Test + public void testTestGetName() { + Assert.assertEquals("Slack", slackAlertChannelFactory.getName()); + } + + @Test + public void testGetParams() { + List params = slackAlertChannelFactory.getParams(); + Assert.assertEquals(2, params.size()); + } + + @Test + public void testCreate() { + AlertChannel alertChannel = slackAlertChannelFactory.create(); + Assert.assertTrue(alertChannel instanceof SlackAlertChannel); + } +} \ No newline at end of file diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertPluginTest.java b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertPluginTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5086352a46a6e4fdd9ba29ca6ce788a7148e4308 --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackAlertPluginTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.dolphinscheduler.plugin.alert.slack; + +import org.apache.dolphinscheduler.spi.alert.AlertChannelFactory; + +import org.junit.Assert; +import org.junit.Test; + +public class SlackAlertPluginTest { + + private SlackAlertPlugin slackAlertPlugin = new SlackAlertPlugin(); + + @Test + public void testGetAlertChannelFactorys() { + Iterable alertChannelFactorys = slackAlertPlugin.getAlertChannelFactorys(); + for (AlertChannelFactory alertChannelFactory : alertChannelFactorys) { + Assert.assertTrue(alertChannelFactory instanceof SlackAlertChannelFactory); + } + } +} \ No newline at end of file diff --git a/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSenderTest.java b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSenderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a488026b42bbdbcbea2f4e4d7e220b20d569740d --- /dev/null +++ b/dolphinscheduler-alert-plugin/dolphinscheduler-alert-slack/src/test/java/org/apache/dolphinscheduler/plugin/alert/slack/SlackSenderTest.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.dolphinscheduler.plugin.alert.slack; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class SlackSenderTest { + + @Test + public void testSendMessage() { + Map alertparam = new HashMap<>(); + alertparam.put(SlackParamsConstants.SLACK_WEN_HOOK_URL_NAME, + "https://hooks.slack.com/services/123456"); + alertparam.put(SlackParamsConstants.SLACK_BOT_NAME, "Dolphinscheduler"); + + SlackSender slackSender = new SlackSender(alertparam); + String response = slackSender.sendMessage("test title", "test content"); + Assert.assertNotEquals("ok", response); + } +} \ No newline at end of file diff --git a/dolphinscheduler-alert-plugin/pom.xml b/dolphinscheduler-alert-plugin/pom.xml index a10f7de254d5e11d04e5da264a0739f1f29b98d5..dba27767595f3105119514857124c1992bf922f3 100644 --- a/dolphinscheduler-alert-plugin/pom.xml +++ b/dolphinscheduler-alert-plugin/pom.xml @@ -36,7 +36,7 @@ dolphinscheduler-alert-script dolphinscheduler-alert-http dolphinscheduler-alert-feishu + dolphinscheduler-alert-slack - \ No newline at end of file diff --git a/pom.xml b/pom.xml index ea58fec804b7e2767d7dcd9f8cd1e1fe432188d3..1d87b0d7578e610c720fb44d3c627f4065e4cdb5 100644 --- a/pom.xml +++ b/pom.xml @@ -1033,16 +1033,18 @@ **/plugin/alert/feishu/FeiShuSenderTest.java **/plugin/alert/http/HttpAlertPluginTest.java **/plugin/alert/http/HttpSenderTest.java + **/plugin/alert/slack/SlackAlertChannelFactoryTest.java + **/plugin/alert/slack/SlackAlertPluginTest.java + **/plugin/alert/slack/SlackSenderTest.java **/spi/params/PluginParamsTransferTest.java **/alert/plugin/EmailAlertPluginTest.java **/alert/plugin/AlertPluginManagerTest.java **/alert/plugin/DolphinPluginLoaderTest.java - **/alert/utils/DingTalkUtilsTest.java - **/alert/utils/EnterpriseWeChatUtilsTest.java **/alert/utils/FuncUtilsTest.java **/alert/processor/AlertRequestProcessorTest.java **/alert/runner/AlertSenderTest.java **/alert/AlertServerTest.java +