diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 00000000..30d494d8
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @jenkinsci/logstash-plugin-developers
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 00000000..fdc58d1e
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "maven"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
deleted file mode 100644
index 0d0b1c99..00000000
--- a/.github/release-drafter.yml
+++ /dev/null
@@ -1 +0,0 @@
-_extends: .github
diff --git a/.github/workflows/jenkins-security-scan.yml b/.github/workflows/jenkins-security-scan.yml
new file mode 100644
index 00000000..35c0807b
--- /dev/null
+++ b/.github/workflows/jenkins-security-scan.yml
@@ -0,0 +1,24 @@
+# Jenkins Security Scan
+# For more information, see: https://www.jenkins.io/doc/developer/security/scan/
+
+name: Jenkins Security Scan
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ types: [opened, synchronize, reopened]
+ workflow_dispatch:
+
+permissions:
+ security-events: write
+ contents: read
+ actions: read
+
+jobs:
+ security-scan:
+ uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2
+ with:
+ java-cache: 'maven'
+ java-version: 17
diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index a65d82e1..1f363640 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -2,6 +2,6 @@
io.jenkins.tools.incrementals
git-changelist-maven-extension
- 1.3
+ 1.7
diff --git a/Jenkinsfile b/Jenkinsfile
index ef2645f9..19c014aa 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,6 +1,5 @@
// Builds a module using https://github.com/jenkins-infra/pipeline-library
-def configurations = [
- [ platform: "linux", jdk: "8", jenkins: null ],
- [ platform: "linux", jdk: "11", jenkins: null ]
-]
-buildPlugin(configurations: configurations, useContainerAgent: true)
+buildPlugin(useContainerAgent: true, configurations: [
+ [platform: 'linux', jdk: 21],
+ [platform: 'windows', jdk: 17],
+])
diff --git a/pom.xml b/pom.xml
index 747c625c..2e5fcdaa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.jenkins-ci.plugins
plugin
- 4.46
+ 4.72
logstash
@@ -64,17 +64,16 @@
-SNAPSHOT
jenkinsci/logstash-plugin
true
- 2.332.4
+ 2.387.3
2.0
- 3.12.4
io.jenkins.tools.bom
- bom-2.332.x
- 1595.v8c71c13cc3a_9
+ bom-2.387.x
+ 2357.v1043f8578392
pom
import
@@ -148,18 +147,6 @@
mockito-core
test
-
- org.powermock
- powermock-api-mockito2
- 2.0.9
- test
-
-
- org.powermock
- powermock-module-junit4
- 2.0.9
- test
-
org.awaitility
awaitility
diff --git a/src/test/java/jenkins/plugins/logstash/LogstashBuildWrapperConversionTest.java b/src/test/java/jenkins/plugins/logstash/LogstashBuildWrapperConversionTest.java
index f569a8f0..c1811c92 100644
--- a/src/test/java/jenkins/plugins/logstash/LogstashBuildWrapperConversionTest.java
+++ b/src/test/java/jenkins/plugins/logstash/LogstashBuildWrapperConversionTest.java
@@ -15,9 +15,9 @@
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
-import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
-import com.gargoylesoftware.htmlunit.HttpMethod;
-import com.gargoylesoftware.htmlunit.WebRequest;
+import org.htmlunit.FailingHttpStatusCodeException;
+import org.htmlunit.HttpMethod;
+import org.htmlunit.WebRequest;
import hudson.model.Project;
diff --git a/src/test/java/jenkins/plugins/logstash/LogstashConfigurationMigrationTest.java b/src/test/java/jenkins/plugins/logstash/LogstashConfigurationMigrationTest.java
index ff575801..c89355b2 100644
--- a/src/test/java/jenkins/plugins/logstash/LogstashConfigurationMigrationTest.java
+++ b/src/test/java/jenkins/plugins/logstash/LogstashConfigurationMigrationTest.java
@@ -3,8 +3,8 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
-import static org.powermock.api.mockito.PowerMockito.mockStatic;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
import java.io.File;
import java.net.MalformedURLException;
@@ -12,15 +12,15 @@
import java.net.URISyntaxException;
import org.hamcrest.core.IsInstanceOf;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jvnet.hudson.test.JenkinsRule;
import org.mockito.Mock;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import org.mockito.MockedStatic;
+import org.mockito.junit.MockitoJUnitRunner;
import com.cloudbees.syslog.MessageFormat;
@@ -33,12 +33,12 @@
import jenkins.plugins.logstash.persistence.LogstashIndexerDao.IndexerType;
import jenkins.plugins.logstash.persistence.LogstashIndexerDao.SyslogFormat;
-@RunWith(PowerMockRunner.class)
-@PrepareForTest({ LogstashInstallation.class, Descriptor.class })
-@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.crypto.*", "javax.xml.*", "org.xml.*"})
+@RunWith(MockitoJUnitRunner.class)
public class LogstashConfigurationMigrationTest extends LogstashConfigurationTestBase
{
+ private MockedStatic mockedLogstashInstallation;
+
@Rule
public JenkinsRule j = new JenkinsRule();
@@ -50,9 +50,9 @@ public class LogstashConfigurationMigrationTest extends LogstashConfigurationTes
@Before
public void setup()
{
- mockStatic(LogstashInstallation.class);
configFile = new File("notExisting.xml");
- when(LogstashInstallation.getLogstashDescriptor()).thenAnswer(invocationOnMock -> descriptor);
+ mockedLogstashInstallation = mockStatic(LogstashInstallation.class);
+ mockedLogstashInstallation.when(LogstashInstallation::getLogstashDescriptor).thenAnswer(invocationOnMock -> descriptor);
when(descriptor.getHost()).thenReturn("localhost");
when(descriptor.getPort()).thenReturn(4567);
when(descriptor.getKey()).thenReturn("logstash");
@@ -61,6 +61,11 @@ public void setup()
configuration = new LogstashConfigurationForTest();
}
+ @After
+ public void after() throws Exception {
+ mockedLogstashInstallation.closeOnDemand();
+ }
+
@Test
public void NoConfigMigration()
{
diff --git a/src/test/java/jenkins/plugins/logstash/LogstashConfigurationTest.java b/src/test/java/jenkins/plugins/logstash/LogstashConfigurationTest.java
index a83c31f4..02cc7d9a 100644
--- a/src/test/java/jenkins/plugins/logstash/LogstashConfigurationTest.java
+++ b/src/test/java/jenkins/plugins/logstash/LogstashConfigurationTest.java
@@ -14,8 +14,8 @@
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
-import com.gargoylesoftware.htmlunit.html.HtmlForm;
-import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import org.htmlunit.html.HtmlForm;
+import org.htmlunit.html.HtmlPage;
import jenkins.plugins.logstash.configuration.ElasticSearch;
import jenkins.plugins.logstash.configuration.RabbitMq;
diff --git a/src/test/java/jenkins/plugins/logstash/LogstashConsoleLogFilterTest.java b/src/test/java/jenkins/plugins/logstash/LogstashConsoleLogFilterTest.java
index 71f303a2..46db843d 100644
--- a/src/test/java/jenkins/plugins/logstash/LogstashConsoleLogFilterTest.java
+++ b/src/test/java/jenkins/plugins/logstash/LogstashConsoleLogFilterTest.java
@@ -1,9 +1,9 @@
package jenkins.plugins.logstash;
import static org.junit.Assert.*;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.powermock.api.mockito.PowerMockito.when;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
import hudson.model.AbstractBuild;
import hudson.model.Project;
@@ -20,16 +20,15 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
-
-@RunWith(PowerMockRunner.class)
-@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.crypto.*", "javax.xml.*", "org.xml.*"})
-@PrepareForTest(LogstashConfiguration.class)
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
public class LogstashConsoleLogFilterTest {
+ private MockedStatic mockedLogstashConfiguration;
+
@Mock
private LogstashConfiguration logstashConfiguration;
@@ -64,8 +63,8 @@ LogstashWriter getLogStashWriter(Run, ?> build, OutputStream errorStream) {
@Before
public void before() throws Exception {
- PowerMockito.mockStatic(LogstashConfiguration.class);
- when(LogstashConfiguration.getInstance()).thenAnswer(invocationOnMock -> logstashConfiguration);
+ mockedLogstashConfiguration = Mockito.mockStatic(LogstashConfiguration.class);
+ mockedLogstashConfiguration.when(LogstashConfiguration::getInstance).thenAnswer(invocationOnMock -> logstashConfiguration);
when(logstashConfiguration.isEnableGlobally()).thenReturn(false);
when(logstashConfiguration.isEnabled()).thenReturn(true);
@@ -81,6 +80,7 @@ public void after() throws Exception {
verifyNoMoreInteractions(mockWriter);
verifyNoMoreInteractions(mockBuildData);
buffer.close();
+ mockedLogstashConfiguration.closeOnDemand();
}
@Test
diff --git a/src/test/java/jenkins/plugins/logstash/LogstashNotifierTest.java b/src/test/java/jenkins/plugins/logstash/LogstashNotifierTest.java
index c9122999..7b68e776 100644
--- a/src/test/java/jenkins/plugins/logstash/LogstashNotifierTest.java
+++ b/src/test/java/jenkins/plugins/logstash/LogstashNotifierTest.java
@@ -2,8 +2,11 @@
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
import hudson.Launcher;
import hudson.model.BuildListener;
@@ -25,20 +28,17 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
-
-@SuppressWarnings("rawtypes")
-@RunWith(PowerMockRunner.class)
-@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.crypto.*", "javax.xml.*", "org.xml.*"})
-@PrepareForTest(LogstashConfiguration.class)
+
+@RunWith(MockitoJUnitRunner.class)
public class LogstashNotifierTest {
+ private MockedStatic mockedLogstashConfiguration;
+
// Extension of the unit under test that avoids making calls to Jenkins.getInstance() to get the DAO singleton
static class MockLogstashNotifier extends LogstashNotifier {
LogstashWriter writer;
@@ -99,8 +99,8 @@ public void before() throws Exception {
errorBuffer = new ByteArrayOutputStream();
errorStream = new PrintStream(errorBuffer, true);
- PowerMockito.mockStatic(LogstashConfiguration.class);
- when(LogstashConfiguration.getInstance()).thenAnswer(invocationOnMock -> logstashConfiguration);
+ mockedLogstashConfiguration = Mockito.mockStatic(LogstashConfiguration.class);
+ mockedLogstashConfiguration.when(LogstashConfiguration::getInstance).thenAnswer(invocationOnMock -> logstashConfiguration);
when(logstashConfiguration.isEnabled()).thenReturn(true);
@@ -123,6 +123,7 @@ public void after() throws Exception {
verifyNoMoreInteractions(mockListener);
verifyNoMoreInteractions(mockWriter);
errorStream.close();
+ mockedLogstashConfiguration.closeOnDemand();
}
@Test
diff --git a/src/test/java/jenkins/plugins/logstash/LogstashWriterTest.java b/src/test/java/jenkins/plugins/logstash/LogstashWriterTest.java
index e3b94ddd..5a0d1b98 100644
--- a/src/test/java/jenkins/plugins/logstash/LogstashWriterTest.java
+++ b/src/test/java/jenkins/plugins/logstash/LogstashWriterTest.java
@@ -12,7 +12,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -30,11 +30,9 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import org.mockito.junit.MockitoJUnitRunner;
import hudson.EnvVars;
import hudson.model.AbstractBuild;
@@ -48,10 +46,11 @@
import jenkins.plugins.logstash.persistence.LogstashIndexerDao;
import net.sf.json.JSONObject;
-@RunWith(PowerMockRunner.class)
-@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.crypto.*", "javax.xml.*", "org.xml.*"})
-@PrepareForTest(LogstashConfiguration.class)
+@RunWith(MockitoJUnitRunner.class)
public class LogstashWriterTest {
+
+ private MockedStatic mockedLogstashConfiguration;
+
// Extension of the unit under test that avoids making calls to getInstance() to get the DAO singleton
static LogstashWriter createLogstashWriter(final AbstractBuild, ?> testBuild,
OutputStream error,
@@ -103,8 +102,8 @@ String getJenkinsUrl() {
@Before
public void before() throws Exception {
- PowerMockito.mockStatic(LogstashConfiguration.class);
- when(LogstashConfiguration.getInstance()).thenAnswer(invocationOnMock -> logstashConfiguration);
+ mockedLogstashConfiguration = Mockito.mockStatic(LogstashConfiguration.class);
+ mockedLogstashConfiguration.when(LogstashConfiguration::getInstance).thenAnswer(invocationOnMock -> logstashConfiguration);
when(logstashConfiguration.getDateFormatter()).thenCallRealMethod();
when(mockBuild.getResult()).thenReturn(Result.SUCCESS);
@@ -150,6 +149,7 @@ public void after() throws Exception {
verifyNoMoreInteractions(mockTestResultAction);
verifyNoMoreInteractions(mockProject);
errorBuffer.close();
+ mockedLogstashConfiguration.closeOnDemand();
}
@Test
diff --git a/src/test/java/jenkins/plugins/logstash/persistence/AbstractLogstashIndexerDaoTest.java b/src/test/java/jenkins/plugins/logstash/persistence/AbstractLogstashIndexerDaoTest.java
index 8213fd72..2bc4db9f 100644
--- a/src/test/java/jenkins/plugins/logstash/persistence/AbstractLogstashIndexerDaoTest.java
+++ b/src/test/java/jenkins/plugins/logstash/persistence/AbstractLogstashIndexerDaoTest.java
@@ -1,7 +1,7 @@
package jenkins.plugins.logstash.persistence;
import static net.sf.json.test.JSONAssert.assertEquals;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.ArrayList;
@@ -9,21 +9,22 @@
import net.sf.json.JSONObject;
+import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
import jenkins.plugins.logstash.LogstashConfiguration;
-@RunWith(PowerMockRunner.class)
-@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.crypto.*", "javax.xml.*", "org.xml.*"})
-@PrepareForTest(LogstashConfiguration.class)
+@RunWith(MockitoJUnitRunner.class)
public class AbstractLogstashIndexerDaoTest {
+
+ private MockedStatic mockedLogstashConfiguration;
+
static final String EMPTY_STRING = "{\"@buildTimestamp\":\"2000-01-01\",\"data\":{},\"message\":[],\"source\":\"jenkins\",\"source_host\":\"http://localhost:8080/jenkins\",\"@version\":1}";
static final String ONE_LINE_STRING = "{\"@buildTimestamp\":\"2000-01-01\",\"data\":{},\"message\":[\"LINE 1\"],\"source\":\"jenkins\",\"source_host\":\"http://localhost:8080/jenkins\",\"@version\":1}";
static final String TWO_LINE_STRING = "{\"@buildTimestamp\":\"2000-01-01\",\"data\":{},\"message\":[\"LINE 1\", \"LINE 2\"],\"source\":\"jenkins\",\"source_host\":\"http://localhost:8080/jenkins\",\"@version\":1}";
@@ -33,14 +34,19 @@ public class AbstractLogstashIndexerDaoTest {
@Before
public void before() throws Exception {
- PowerMockito.mockStatic(LogstashConfiguration.class);
- when(LogstashConfiguration.getInstance()).thenAnswer(invocationOnMock -> logstashConfiguration);
+ mockedLogstashConfiguration = Mockito.mockStatic(LogstashConfiguration.class);
+ mockedLogstashConfiguration.when(LogstashConfiguration::getInstance).thenAnswer(invocationOnMock -> logstashConfiguration);
when(logstashConfiguration.getDateFormatter()).thenCallRealMethod();
when(mockBuildData.toJson()).thenReturn(JSONObject.fromObject("{}"));
when(mockBuildData.getTimestamp()).thenReturn("2000-01-01");
}
+ @After
+ public void after() throws Exception {
+ mockedLogstashConfiguration.closeOnDemand();
+ }
+
@Test
public void buildPayloadSuccessEmpty() throws Exception {
AbstractLogstashIndexerDao dao = getInstance();
diff --git a/src/test/java/jenkins/plugins/logstash/persistence/BuildDataTest.java b/src/test/java/jenkins/plugins/logstash/persistence/BuildDataTest.java
index 8d55d074..d0d3b616 100644
--- a/src/test/java/jenkins/plugins/logstash/persistence/BuildDataTest.java
+++ b/src/test/java/jenkins/plugins/logstash/persistence/BuildDataTest.java
@@ -5,7 +5,7 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.powermock.api.mockito.PowerMockito.when;
+import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Collections;
@@ -21,13 +21,11 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
+import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
-import org.powermock.api.mockito.PowerMockito;
-import org.powermock.core.classloader.annotations.PowerMockIgnore;
-import org.powermock.core.classloader.annotations.PrepareForTest;
-import org.powermock.modules.junit4.PowerMockRunner;
import hudson.EnvVars;
import hudson.model.AbstractBuild;
@@ -46,12 +44,11 @@
import net.sf.json.JSONObject;
import net.sf.json.test.JSONAssert;
-@SuppressWarnings("rawtypes")
-@RunWith(PowerMockRunner.class)
-@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.crypto.*", "javax.xml.*", "org.xml.*"})
-@PrepareForTest(LogstashConfiguration.class)
+@RunWith(MockitoJUnitRunner.class)
public class BuildDataTest {
+ private MockedStatic mockedLogstashConfiguration;
+
static final String FULL_STRING = "{\"id\":\"TEST_JOB_123\",\"result\":\"SUCCESS\",\"fullProjectName\":\"parent/BuildDataTest\","
+ "\"projectName\":\"BuildDataTest\",\"displayName\":\"BuildData Test\",\"fullDisplayName\":\"BuildData Test #123456\","
+ "\"description\":\"Mock project for testing BuildData\",\"url\":\"http://localhost:8080/jenkins/jobs/PROJECT_NAME/123\","
@@ -77,8 +74,8 @@ public class BuildDataTest {
@Before
public void before() throws Exception {
- PowerMockito.mockStatic(LogstashConfiguration.class);
- when(LogstashConfiguration.getInstance()).thenAnswer(invocationOnMock -> logstashConfiguration);
+ mockedLogstashConfiguration = Mockito.mockStatic(LogstashConfiguration.class);
+ mockedLogstashConfiguration.when(LogstashConfiguration::getInstance).thenAnswer(invocationOnMock -> logstashConfiguration);
when(logstashConfiguration.getDateFormatter()).thenCallRealMethod();
when(mockBuild.getResult()).thenReturn(Result.SUCCESS);
@@ -125,6 +122,7 @@ public void after() throws Exception {
verifyNoMoreInteractions(mockDate);
verifyNoMoreInteractions(mockRootBuild);
verifyNoMoreInteractions(mockRootProject);
+ mockedLogstashConfiguration.closeOnDemand();
}
private void verifyMocks() throws Exception