Repository: Wikidata/Wikidata-Toolkit
Pull request #908 (2024-09-16 01:54:02)

Bump org.apache.maven.plugins:maven-gpg-plugin from 3.2.5 to 3.2.6
Bumps [org.apache.maven.plugins:maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) from 3.2.5 to 3.2.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/apache/maven-gpg-plugin/releases">org.apache.maven.plugins:maven-gpg-plugin's releases</a>.</em></p>
<blockquote>
<h2>3.2.6</h2>
<h1><a href="https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12317521&amp;version=12354997">Release Notes - Maven GPG Plugin - Version 3.2.6</a></h1>
<!-- raw HTML omitted -->
<!-- raw HTML omitted -->
<hr />
<h2>What's Changed</h2>
<ul>
<li>[MGPG-135] Support Overriding / Enhance the signer in AbstractGpgMojo by <a href="https://github.com/laeubi"><code>@​laeubi</code></a> in <a href="https://redirect.github.com/apache/maven-gpg-plugin/pull/112">apache/maven-gpg-plugin#112</a></li>
<li>Bump org.apache.maven.plugins:maven-invoker-plugin from 3.7.0 to 3.8.0 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/apache/maven-gpg-plugin/pull/114">apache/maven-gpg-plugin#114</a></li>
<li>[MGPG-140] Update Maven to 3.9.9 by <a href="https://github.com/cstamas"><code>@​cstamas</code></a> in <a href="https://redirect.github.com/apache/maven-gpg-plugin/pull/116">apache/maven-gpg-plugin#116</a></li>
<li>[MGPG-138] Drop direct use of plexus-cipher and secdispatcher by <a href="https://github.com/cstamas"><code>@​cstamas</code></a> in <a href="https://redirect.github.com/apache/maven-gpg-plugin/pull/115">apache/maven-gpg-plugin#115</a></li>
<li>[MGPG-141] Remove use of deprecated classes by <a href="https://github.com/cstamas"><code>@​cstamas</code></a> in <a href="https://redirect.github.com/apache/maven-gpg-plugin/pull/117">apache/maven-gpg-plugin#117</a></li>
<li>Add FAQ for &quot;no pinentry&quot; issue by <a href="https://github.com/cstamas"><code>@​cstamas</code></a> in <a href="https://redirect.github.com/apache/maven-gpg-plugin/pull/118">apache/maven-gpg-plugin#118</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/laeubi"><code>@​laeubi</code></a> made their first contribution in <a href="https://redirect.github.com/apache/maven-gpg-plugin/pull/112">apache/maven-gpg-plugin#112</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.2.5...maven-gpg-plugin-3.2.6">https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.2.5...maven-gpg-plugin-3.2.6</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/1c9a14c0eec9dae40baa4a5db674e6a7ec012be4"><code>1c9a14c</code></a> [maven-release-plugin] prepare release maven-gpg-plugin-3.2.6</li>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/bbe6156400212b3fe063d218399ba09c24795f44"><code>bbe6156</code></a> Add FAQ for &quot;no pinentry&quot; issue (<a href="https://redirect.github.com/apache/maven-gpg-plugin/issues/118">#118</a>)</li>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/5b9427333ab4b7b48b297a8b9a4158688211f14a"><code>5b94273</code></a> [MGPG-141] Remove use of deprecated classes (<a href="https://redirect.github.com/apache/maven-gpg-plugin/issues/117">#117</a>)</li>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/afdfd28bab4a0530cfd655397716a36adc1644b6"><code>afdfd28</code></a> [MGPG-138] Drop direct use of plexus-cipher and secdispatcher (<a href="https://redirect.github.com/apache/maven-gpg-plugin/issues/115">#115</a>)</li>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/7516e7c73d6e5c8c3d6c2cbab9d55deb3987f388"><code>7516e7c</code></a> [MGPG-140] Update Maven to 3.9.9 (<a href="https://redirect.github.com/apache/maven-gpg-plugin/issues/116">#116</a>)</li>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/4ec571f561e330209189eb3c4387aaae0b37fb96"><code>4ec571f</code></a> [MGPG-139] Bump org.apache.maven.plugins:maven-invoker-plugin from 3.7.0 to 3...</li>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/1126b7b09cb8a4540e35f62146b1198bcadeb795"><code>1126b7b</code></a> use new Reproducible Central badge endpoint</li>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/1b40a0554b55c1d8a1c5da662e848688aa346aed"><code>1b40a05</code></a> [MGPG-135] Support Overriding / Enhance the signer in AbstractGpgMojo (<a href="https://redirect.github.com/apache/maven-gpg-plugin/issues/112">#112</a>)</li>
<li><a href="https://github.com/apache/maven-gpg-plugin/commit/3a31714e9cbdde86a6b12f1ef05d5c60252fef4a"><code>3a31714</code></a> [maven-release-plugin] prepare for next development iteration</li>
<li>See full diff in <a href="https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.2.5...maven-gpg-plugin-3.2.6">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.apache.maven.plugins:maven-gpg-plugin&package-manager=maven&previous-version=3.2.5&new-version=3.2.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/.github/workflows/release_please.yml b/.github/workflows/release_please.yml
new file mode 100644
index 00000000..833ba1ae
--- /dev/null
+++ b/.github/workflows/release_please.yml
@@ -0,0 +1,24 @@
+on:
+  push:
+    branches:
+      - main
+
+permissions:
+  contents: write
+  pull-requests: write
+
+name: release-please
+
+jobs:
+  release-please:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: googleapis/release-please-action@v4
+        with:
+          # this assumes that you have created a personal access token
+          # (PAT) and configured it as a GitHub action secret named
+          # `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
+          token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
+          # this is a built-in strategy in release-please, see "Action Inputs"
+          # for more options
+          release-type: simple
diff --git a/pom.xml b/pom.xml
index b1902547..ab336fea 100644
--- a/pom.xml
+++ b/pom.xml
@@ -144,7 +144,7 @@
 			<dependency>
 				<groupId>commons-io</groupId>
 				<artifactId>commons-io</artifactId>
-				<version>2.16.1</version>
+				<version>2.17.0</version>
 				<scope>test</scope>
 			</dependency>
 			<dependency>
@@ -356,7 +356,7 @@
 				<!-- Signature -->
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-gpg-plugin</artifactId>
-				<version>3.2.5</version>
+				<version>3.2.6</version>
 				<executions>
 					<execution>
 						<id>sign-artifacts</id>
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/ApiConnection.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/ApiConnection.java
index 8b515e16..3b144e08 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/ApiConnection.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/ApiConnection.java
@@ -42,9 +42,12 @@ import org.wikidata.wdtk.wikibaseapi.apierrors.AssertUserFailedException;
 import org.wikidata.wdtk.wikibaseapi.apierrors.MaxlagErrorException;
 import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorException;
 import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorHandler;
+import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiErrorMessage;
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
@@ -482,13 +485,23 @@ public abstract class ApiConnection {
 			JsonNode errorNode = root.path("error");
 			String code = errorNode.path("code").asText("UNKNOWN");
 			String info = errorNode.path("info").asText("No details provided");
+			
+			List<MediaWikiErrorMessage> messages = Collections.emptyList();
+			if (errorNode.has("messages")) {
+			    try {
+                    messages = this.mapper.treeToValue(errorNode.get("messages"), new TypeReference<List<MediaWikiErrorMessage>>() {});
+                } catch (JsonProcessingException | IllegalArgumentException e) {
+                    logger.warn("Could not parse 'messages' field of API error response");
+                }
+			}
+			
 			// Special case for the maxlag error since we also want to return
 			// the lag value in the exception thrown
 			if (errorNode.has("lag") && MediaWikiApiErrorHandler.ERROR_MAXLAG.equals(code)) {
 				double lag = errorNode.path("lag").asDouble();
 				throw new MaxlagErrorException(info, lag);
 			} else {
-				MediaWikiApiErrorHandler.throwMediaWikiApiErrorException(code, info);
+				MediaWikiApiErrorHandler.throwMediaWikiApiErrorException(code, info, messages);
 			}
 		}
 	}
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/AssertUserFailedException.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/AssertUserFailedException.java
index 0222b97b..5661d360 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/AssertUserFailedException.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/AssertUserFailedException.java
@@ -1,5 +1,7 @@
 package org.wikidata.wdtk.wikibaseapi.apierrors;
 
+import java.util.List;
+
 /*
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -32,8 +34,13 @@ public class AssertUserFailedException extends MediaWikiApiErrorException {
 
 	private static final long serialVersionUID = -619052434831693831L;
 
+	@Deprecated(since = "0.16.0")
 	public AssertUserFailedException(String errorMessage) {
 		super(MediaWikiApiErrorHandler.ERROR_ASSERT_USER_FAILED, errorMessage);
 	}
+	
+    public AssertUserFailedException(String errorMessage, List<MediaWikiErrorMessage> detailedMessages) {
+        super(MediaWikiApiErrorHandler.ERROR_ASSERT_USER_FAILED, errorMessage, detailedMessages);
+    }
 
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/EditConflictErrorException.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/EditConflictErrorException.java
index b8c8a24d..18d79007 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/EditConflictErrorException.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/EditConflictErrorException.java
@@ -1,5 +1,7 @@
 package org.wikidata.wdtk.wikibaseapi.apierrors;
 
+import java.util.List;
+
 /*
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -37,8 +39,22 @@ public class EditConflictErrorException extends MediaWikiApiErrorException {
 	 *            the error message reported by MediaWiki, or any other
 	 *            meaningful message for the user
 	 */
+	@Deprecated(since = "0.16.0")
 	public EditConflictErrorException(String errorMessage) {
 		super(MediaWikiApiErrorHandler.ERROR_EDIT_CONFLICT, errorMessage);
 	}
+	
+	/**
+     * Creates a new exception.
+     *
+     * @param errorMessage
+     *            the root error message
+     * @param detailedMessages
+     *            additional details provided by the MediaWiki API
+     */
+    public EditConflictErrorException(String errorMessage, List<MediaWikiErrorMessage> detailedMessages) {
+        super(MediaWikiApiErrorHandler.ERROR_EDIT_CONFLICT, errorMessage, detailedMessages);
+    }
+
 
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorException.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorException.java
index 3a99f0fc..c04f9375 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorException.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorException.java
@@ -1,5 +1,8 @@
 package org.wikidata.wdtk.wikibaseapi.apierrors;
 
+import java.util.Collections;
+import java.util.List;
+
 /*
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -30,11 +33,13 @@ public class MediaWikiApiErrorException extends Exception {
 
 	final String errorCode;
 	final String errorMessage;
+	final List<MediaWikiErrorMessage> detailedMessages;
 
 	private static final long serialVersionUID = 7834254856687745000L;
 
 	/**
-	 * Creates a new exception for the given error code and message.
+	 * Creates a new exception for the given error code and message, without
+	 * any detailed messages.
 	 *
 	 * @param errorCode
 	 *            MediaWiki reported error code
@@ -46,7 +51,27 @@ public class MediaWikiApiErrorException extends Exception {
 		super("[" + errorCode + "] " + errorMessage);
 		this.errorCode = errorCode;
 		this.errorMessage = errorMessage;
+		this.detailedMessages = Collections.emptyList();
 	}
+	
+	/**
+     * Creates a new exception for the given error code and message, together
+     * with detailed messages giving more insights on the error.
+     *
+     * @param errorCode
+     *            MediaWiki reported error code
+     * @param errorMessage
+     *            MediaWiki reported error message, or any other human-readable
+     *            message string generated locally
+     * @param detailedMessages
+     *            list of error messages also returned by MediaWiki (possibly empty)
+     */
+    public MediaWikiApiErrorException(String errorCode, String errorMessage, List<MediaWikiErrorMessage> detailedMessages) {
+        super("[" + errorCode + "] " + errorMessage);
+        this.errorCode = errorCode;
+        this.errorMessage = errorMessage;
+        this.detailedMessages = detailedMessages;
+    }
 
 	/**
 	 * Returns the MediaWiki code of the error that has causes this exception.
@@ -67,5 +92,12 @@ public class MediaWikiApiErrorException extends Exception {
 	public String getErrorMessage() {
 		return this.errorMessage;
 	}
+	
+	/**
+	 * Returns the list of additional error messages returned by MediaWiki.
+	 */
+	public List<MediaWikiErrorMessage> getDetailedMessages() {
+	    return this.detailedMessages;
+	}
 
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandler.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandler.java
index f274998e..f0029f91 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandler.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandler.java
@@ -1,5 +1,7 @@
 package org.wikidata.wdtk.wikibaseapi.apierrors;
 
+import java.util.List;
+
 /*
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -36,6 +38,40 @@ public class MediaWikiApiErrorHandler {
 	public final static String ERROR_ASSERT_USER_FAILED = "assertuserfailed";
 	public final static String ERROR_TAGS_APPLY_NOT_ALLOWED = "tags-apply-not-allowed-one";
 
+	/**
+     * Creates and throws a suitable {@link MediaWikiApiErrorException} for the
+     * given error code and message.
+     *
+     * @param errorCode
+     *            the error code reported by MediaWiki
+     * @param errorMessage
+     *            the error message reported by MediaWiki, or any other
+     *            meaningful message for the user
+     * @throws MediaWikiApiErrorException
+     *             in all cases, but may throw a subclass for some errors
+     */
+    public static void throwMediaWikiApiErrorException(String errorCode,
+            String errorMessage, List<MediaWikiErrorMessage> detailedMessages) throws MediaWikiApiErrorException {
+        switch (errorCode) {
+        case ERROR_NO_TOKEN:
+        case ERROR_INVALID_TOKEN:
+            throw new TokenErrorException(errorCode, errorMessage, detailedMessages);
+        case ERROR_EDIT_CONFLICT:
+            throw new EditConflictErrorException(errorMessage, detailedMessages);
+        case ERROR_NO_SUCH_ENTITY:
+            throw new NoSuchEntityErrorException(errorMessage, detailedMessages);
+        case ERROR_MAXLAG:
+            throw new MaxlagErrorException(errorMessage);
+        case ERROR_ASSERT_USER_FAILED:
+            throw new AssertUserFailedException(errorMessage, detailedMessages);
+        case ERROR_TAGS_APPLY_NOT_ALLOWED:
+            throw new TagsApplyNotAllowedException(errorMessage, detailedMessages);
+        default:
+            throw new MediaWikiApiErrorException(errorCode, errorMessage, detailedMessages);
+        }
+    }
+        
+            
 	/**
 	 * Creates and throws a suitable {@link MediaWikiApiErrorException} for the
 	 * given error code and message.
@@ -48,6 +84,7 @@ public class MediaWikiApiErrorHandler {
 	 * @throws MediaWikiApiErrorException
 	 *             in all cases, but may throw a subclass for some errors
 	 */
+    @Deprecated(since = "0.16.0")
 	public static void throwMediaWikiApiErrorException(String errorCode,
 			String errorMessage) throws MediaWikiApiErrorException {
 		switch (errorCode) {
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiErrorMessage.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiErrorMessage.java
new file mode 100644
index 00000000..8d2079d9
--- /dev/null
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiErrorMessage.java
@@ -0,0 +1,105 @@
+package org.wikidata.wdtk.wikibaseapi.apierrors;
+
+import java.util.Objects;
+
+/*-
+ * #%L
+ * Wikidata Toolkit Wikibase API
+ * %%
+ * Copyright (C) 2014 - 2024 Wikidata Toolkit Developers
+ * %%
+ * 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.
+ * #L%
+ */
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * An error message returned as part of the `messages` field of a broader
+ * MediaWiki error, providing more details on the source of the problem.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class MediaWikiErrorMessage {
+    
+    final String name;
+    final String htmlText;
+    
+    /**
+     * Constructor
+     * 
+     * @param name    a code for the error message, such as "spam-blacklisted-link"
+     * @param htmlText the text of the error in HTML
+     */
+    public MediaWikiErrorMessage(String name, String htmlText) {
+        this.name = name;
+        this.htmlText = htmlText;
+    }
+    
+    /**
+     * Constructor for deserialization purposes (via Jackson).
+     */
+    @JsonCreator
+    MediaWikiErrorMessage(@JsonProperty("name") String name, @JsonProperty("html") MessageWrapper wrapper) {
+        this.name = name;
+        this.htmlText = wrapper.message;
+    }
+    
+    // Wrapper class provided for deserialization purposes
+    protected static class MessageWrapper {
+        final String message;
+
+        @JsonCreator
+        public MessageWrapper(@JsonProperty("*") String message) {
+            this.message = message;
+        }
+    }
+
+    /**
+     * A code for the error message, such as "spam-blacklisted-link"
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * A human-readable error text in HTML
+     */
+    public String getHTMLText() {
+        return htmlText;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(htmlText, name);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        MediaWikiErrorMessage other = (MediaWikiErrorMessage) obj;
+        return Objects.equals(htmlText, other.htmlText) && Objects.equals(name, other.name);
+    }
+
+    @Override
+    public String toString() {
+        return "MediaWikiErrorMessage [name=" + name + ", htmlText=" + htmlText + "]";
+    }
+    
+}
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/NoSuchEntityErrorException.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/NoSuchEntityErrorException.java
index 8c4f42d9..8e881dac 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/NoSuchEntityErrorException.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/NoSuchEntityErrorException.java
@@ -1,5 +1,7 @@
 package org.wikidata.wdtk.wikibaseapi.apierrors;
 
+import java.util.List;
+
 /*
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -38,8 +40,22 @@ public class NoSuchEntityErrorException extends MediaWikiApiErrorException {
 	 *            the error message reported by MediaWiki, or any other
 	 *            meaningful message for the user
 	 */
+	@Deprecated(since = "0.16.0")
 	public NoSuchEntityErrorException(String errorMessage) {
 		super(MediaWikiApiErrorHandler.ERROR_NO_SUCH_ENTITY, errorMessage);
 	}
+	
+	/**
+     * Creates a new exception.
+     *
+     * @param errorMessage
+     *            the error message reported by MediaWiki, or any other
+     *            meaningful message for the user
+     * @param detailedMessages
+     *            the more detailed messages returned by MediaWiki
+     */
+    public NoSuchEntityErrorException(String errorMessage, List<MediaWikiErrorMessage> detailedMessages) {
+        super(MediaWikiApiErrorHandler.ERROR_NO_SUCH_ENTITY, errorMessage, detailedMessages);
+    }
 
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TagsApplyNotAllowedException.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TagsApplyNotAllowedException.java
index 17ad1a21..afa8e9a4 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TagsApplyNotAllowedException.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TagsApplyNotAllowedException.java
@@ -1,5 +1,7 @@
 package org.wikidata.wdtk.wikibaseapi.apierrors;
 
+import java.util.List;
+
 /*-
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -36,7 +38,21 @@ public class TagsApplyNotAllowedException extends MediaWikiApiErrorException {
 	 *            the error message reported by MediaWiki, or any other
 	 *            meaningful message for the user
 	 */
+	@Deprecated(since = "0.16.0")
 	public TagsApplyNotAllowedException(String errorMessage) {
 		super(MediaWikiApiErrorHandler.ERROR_TAGS_APPLY_NOT_ALLOWED, errorMessage);
 	}
+	
+	/**
+     * Creates a new exception.
+     *
+     * @param errorMessage
+     *            the error message reported by MediaWiki, or any other
+     *            meaningful message for the user
+     * @param detailedMessages
+     *            the more detailed messages returned by MediaWiki
+     */
+    public TagsApplyNotAllowedException(String errorMessage, List<MediaWikiErrorMessage> detailedMessages) {
+        super(MediaWikiApiErrorHandler.ERROR_TAGS_APPLY_NOT_ALLOWED, errorMessage, detailedMessages);
+    }
 }
diff --git a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TokenErrorException.java b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TokenErrorException.java
index e476303e..98b013e1 100644
--- a/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TokenErrorException.java
+++ b/wdtk-wikibaseapi/src/main/java/org/wikidata/wdtk/wikibaseapi/apierrors/TokenErrorException.java
@@ -1,5 +1,7 @@
 package org.wikidata.wdtk.wikibaseapi.apierrors;
 
+import java.util.List;
+
 /*
  * #%L
  * Wikidata Toolkit Wikibase API
@@ -40,8 +42,24 @@ public class TokenErrorException extends MediaWikiApiErrorException {
 	 *            the error message reported by MediaWiki, or any other
 	 *            meaningful message for the user
 	 */
+	@Deprecated(since = "0.16.0")
 	public TokenErrorException(String errorCode, String errorMessage) {
 		super(errorCode, errorMessage);
 	}
+	
+	/**
+     * Creates a new exception.
+     *
+     * @param errorCode
+     *            the error code reported by MediaWiki
+     * @param errorMessage
+     *            the error message reported by MediaWiki, or any other
+     *            meaningful message for the user
+     * @param detailedMessages
+     *            the more detailed error messages returned by MediaWiki
+     */
+    public TokenErrorException(String errorCode, String errorMessage, List<MediaWikiErrorMessage> detailedMessages) {
+        super(errorCode, errorMessage, detailedMessages);
+    }
 
 }


Test patch:
diff --git a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnectionTest.java b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnectionTest.java
index ad5fad10..f4279f40 100644
--- a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnectionTest.java
+++ b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/BasicApiConnectionTest.java
@@ -52,6 +52,7 @@ import org.wikidata.wdtk.testing.MockStringContentFactory;
 import org.wikidata.wdtk.wikibaseapi.apierrors.AssertUserFailedException;
 import org.wikidata.wdtk.wikibaseapi.apierrors.MaxlagErrorException;
 import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiApiErrorException;
+import org.wikidata.wdtk.wikibaseapi.apierrors.MediaWikiErrorMessage;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -328,6 +329,22 @@ public class BasicApiConnectionTest {
 			assertEquals(3.45, e.getLag(), 0.001);
 		}
 	}
+	   
+    @Test
+    public void testAbuseFilterError() throws IOException, MediaWikiApiErrorException {
+        JsonNode root;
+        URL path = this.getClass().getResource("/error-spam-filter.json");
+        root = mapper.readTree(path.openStream());
+        try {
+            connection.checkErrors(root);
+        } catch(MediaWikiApiErrorException e) {
+            List<MediaWikiErrorMessage> expectedMessages = Arrays.asList(
+                    new MediaWikiErrorMessage("wikibase-api-failed-save", "The save has failed."),
+                    new MediaWikiErrorMessage("spam-blacklisted-link", "The text you wanted to publish was blocked by the spam filter.")
+            );
+            assertEquals(expectedMessages, e.getDetailedMessages());
+        }
+    }
 
 	@Test
 	public void testClearCookies() throws LoginFailedException, IOException, MediaWikiApiErrorException {
diff --git a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandlerTest.java b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandlerTest.java
index 90096911..8301b5f5 100644
--- a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandlerTest.java
+++ b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiApiErrorHandlerTest.java
@@ -22,6 +22,8 @@ package org.wikidata.wdtk.wikibaseapi.apierrors;
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.Collections;
+
 import org.junit.Test;
 
 public class MediaWikiApiErrorHandlerTest {
@@ -32,7 +34,7 @@ public class MediaWikiApiErrorHandlerTest {
 		String message = "";
 		try {
 			MediaWikiApiErrorHandler.throwMediaWikiApiErrorException("unknown",
-					"some message");
+					"some message", Collections.emptyList());
 		} catch (MediaWikiApiErrorException e) {
 			code = e.getErrorCode();
 			message = e.getErrorMessage();
@@ -45,32 +47,32 @@ public class MediaWikiApiErrorHandlerTest {
 	@Test(expected = TokenErrorException.class)
 	public void testNoTokenError() throws MediaWikiApiErrorException {
 		MediaWikiApiErrorHandler.throwMediaWikiApiErrorException(
-				MediaWikiApiErrorHandler.ERROR_NO_TOKEN, "some message");
+				MediaWikiApiErrorHandler.ERROR_NO_TOKEN, "some message", Collections.emptyList());
 	}
 
 	@Test(expected = TokenErrorException.class)
 	public void testBadTokenError() throws MediaWikiApiErrorException {
 		MediaWikiApiErrorHandler.throwMediaWikiApiErrorException(
-				MediaWikiApiErrorHandler.ERROR_INVALID_TOKEN, "some message");
+				MediaWikiApiErrorHandler.ERROR_INVALID_TOKEN, "some message", Collections.emptyList());
 	}
 
 	@Test(expected = EditConflictErrorException.class)
 	public void testEditConflictError() throws MediaWikiApiErrorException {
 		MediaWikiApiErrorHandler.throwMediaWikiApiErrorException(
-				MediaWikiApiErrorHandler.ERROR_EDIT_CONFLICT, "some message");
+				MediaWikiApiErrorHandler.ERROR_EDIT_CONFLICT, "some message", Collections.emptyList());
 	}
 
 	@Test(expected = NoSuchEntityErrorException.class)
 	public void testNoSuchEntityError() throws MediaWikiApiErrorException {
 		MediaWikiApiErrorHandler.throwMediaWikiApiErrorException(
-				MediaWikiApiErrorHandler.ERROR_NO_SUCH_ENTITY, "some message");
+				MediaWikiApiErrorHandler.ERROR_NO_SUCH_ENTITY, "some message", Collections.emptyList());
 	}
 
 	@Test(expected = MaxlagErrorException.class)
 	public void testMaxlagError() throws MediaWikiApiErrorException {
 		MediaWikiApiErrorHandler.throwMediaWikiApiErrorException(
 				MediaWikiApiErrorHandler.ERROR_MAXLAG,
-				"Waiting for 10.64.16.27: 2 seconds lagged");
+				"Waiting for 10.64.16.27: 2 seconds lagged", Collections.emptyList());
 	}
 
 }
diff --git a/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiErrorMessageTest.java b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiErrorMessageTest.java
new file mode 100644
index 00000000..4022d9d3
--- /dev/null
+++ b/wdtk-wikibaseapi/src/test/java/org/wikidata/wdtk/wikibaseapi/apierrors/MediaWikiErrorMessageTest.java
@@ -0,0 +1,43 @@
+package org.wikidata.wdtk.wikibaseapi.apierrors;
+
+/*-
+ * #%L
+ * Wikidata Toolkit Wikibase API
+ * %%
+ * Copyright (C) 2014 - 2024 Wikidata Toolkit Developers
+ * %%
+ * 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.
+ * #L%
+ */
+
+import static org.junit.Assert.*;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+
+public class MediaWikiErrorMessageTest {
+
+    @Test
+    public void testDeserialize() throws JsonMappingException, JsonProcessingException {
+        String json = "{\"name\": \"wikibase-api-failed-save\","
+                + "\"html\": {\"*\": \"The save has failed\"}}";
+        
+        ObjectMapper mapper = new ObjectMapper();
+        MediaWikiErrorMessage message = mapper.readValue(json, MediaWikiErrorMessage.class);
+        
+        assertEquals("The save has failed", message.getHTMLText());
+        assertEquals("wikibase-api-failed-save", message.getName());
+    }
+}
diff --git a/wdtk-wikibaseapi/src/test/resources/error-spam-filter.json b/wdtk-wikibaseapi/src/test/resources/error-spam-filter.json
new file mode 100644
index 00000000..3f52a1fb
--- /dev/null
+++ b/wdtk-wikibaseapi/src/test/resources/error-spam-filter.json
@@ -0,0 +1,31 @@
+{
+  "error": {
+    "code": "failed-save",
+    "info": "The save has failed.",
+    "messages": [
+      {
+        "name": "wikibase-api-failed-save",
+        "parameters": [],
+        "html": {
+          "*": "The save has failed."
+        }
+      },
+      {
+        "name": "spam-blacklisted-link",
+        "parameters": [
+          {
+            "list": [
+              "tinyurl.com/auniresc"
+            ],
+            "type": "text"
+          }
+        ],
+        "html": {
+          "*": "The text you wanted to publish was blocked by the spam filter."
+        }
+      }
+    ],
+    "*": "See https://www.wikidata.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/postorius/lists/mediawiki-api-announce.lists.wikimedia.org/&gt; for notice of API deprecations and breaking changes."
+  },
+  "servedby": "mw-api-ext.eqiad.main-79f56d66b6-6hwm5"
+}