Commit 377cbcb6 authored by Florian Schäfer's avatar Florian Schäfer

Merge branch 'validator'

This adds new validation checks, if the language prefix of wikipedia=* tags is valid and if a Wikipedia article redirects to another one (with autofix).
parents cde84b82 fedfa8a9
......@@ -23,6 +23,7 @@ import org.wikipedia.gui.WikidataTagCellRenderer;
import org.wikipedia.gui.WikipediaToggleDialog;
import org.wikipedia.validator.WikidataItemExists;
import org.wikipedia.validator.WikipediaAgainstWikidata;
import org.wikipedia.validator.WikipediaRedirect;
public final class WikipediaPlugin extends Plugin {
public static final ImageIcon LOGO = ImageProvider.get("dialogs/wikipedia");
......@@ -47,6 +48,7 @@ public final class WikipediaPlugin extends Plugin {
OsmValidator.addTest(WikidataItemExists.class);
OsmValidator.addTest(WikipediaAgainstWikidata.class);
OsmValidator.addTest(WikipediaRedirect.class);
}
public static String getVersionInfo() {
......
......@@ -57,7 +57,7 @@ public final class ApiQueryClient {
Logging.log(Level.INFO, "Failed to update the cached API response. Falling back to the cached response.", e);
}
}
Logging.info("API request is served from cache: {0}", query.getCacheKey());
Logging.debug("API request is served from cache: {0}", query.getCacheKey());
stream = new ByteArrayInputStream(cachedValue.getBytes(StandardCharsets.UTF_8));
} else {
stream = getInputStreamForQuery(query);
......@@ -70,7 +70,7 @@ public final class ApiQueryClient {
}
}
private static InputStream getInputStreamForQuery(final ApiQuery query) throws IOException {
private static InputStream getInputStreamForQuery(final ApiQuery<?> query) throws IOException {
final HttpClient.Response response;
try {
response = query.getHttpClient().connect();
......
......@@ -16,6 +16,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import org.wikipedia.api.SerializationSchema;
import org.wikipedia.tools.RegexUtil;
......@@ -97,13 +98,16 @@ public final class SitematrixResult {
@JsonProperty("name") final String name,
@JsonProperty("site") final Collection<Site> sites
) {
this.code = code;
this.code = Objects.requireNonNull(code);
this.name = name;
if (sites != null) {
this.sites.addAll(sites);
}
}
/**
* @return the code representing the language of the Wikimedia site
*/
public String getCode() {
return code;
}
......@@ -135,10 +139,17 @@ public final class SitematrixResult {
this.url = url;
}
/**
* @return the code representing the type of the Wikimedia site (NOT the language).
* Values can be e.g. "wiki", "wikibooks", "wiktionary", "wikidata", …
*/
public String getCode() {
return code;
}
/**
* @return a unique string representing the Wikimedia site
*/
public String getDbName() {
return dbName;
}
......
// License: GPL. For details, see LICENSE file.
package org.wikipedia.api.wikipedia_action;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Objects;
import org.openstreetmap.josm.tools.HttpClient;
import org.openstreetmap.josm.tools.Utils;
import org.wikipedia.api.ApiQuery;
import org.wikipedia.api.ApiUrl;
import org.wikipedia.api.SerializationSchema;
import org.wikipedia.api.wikipedia_action.json.QueryResult;
import org.wikipedia.data.IWikipediaSite;
public class WikipediaActionApiQuery<T> extends ApiQuery<T> {
private static final String FORMAT_PARAMS = "format=json&utf8=1&formatversion=1";
private static final String[] TICKET_KEYWORDS = {"wikipedia", "ActionAPI"};
private final String queryString;
private WikipediaActionApiQuery(final IWikipediaSite site, final String queryString, SerializationSchema<T> schema) {
super(ApiUrl.url(site.getSite().getUrl(), "/w/api.php"), schema, -1);
this.queryString = Objects.requireNonNull(queryString);
}
public String getQueryString() {
return queryString;
}
@Override
public HttpClient getHttpClient() {
return HttpClient.create(getUrl(), "POST")
.setAccept("application/json")
.setHeader("Content-Type", "text/plain; charset=utf-8")
.setHeader("User-Agent", getUserAgent(TICKET_KEYWORDS))
.setReasonForRequest(getQueryString().replace('&', ' '))
.setRequestBody(getQueryString().getBytes(StandardCharsets.UTF_8));
}
public static WikipediaActionApiQuery<QueryResult> query(final IWikipediaSite site, final Collection<String> titles) {
Objects.requireNonNull(site);
Objects.requireNonNull(titles);
return new WikipediaActionApiQuery<QueryResult>(
site,
FORMAT_PARAMS +
"&action=query&redirects=1&titles=" +
Utils.encodeUrl(String.join("|", titles)),
QueryResult.SCHEMA
);
}
}
// License: GPL. For details, see LICENSE file.
package org.wikipedia.api.wikipedia_action.json;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import org.openstreetmap.josm.tools.Logging;
import org.wikipedia.api.SerializationSchema;
public final class QueryResult {
public static final SerializationSchema<QueryResult> SCHEMA = new SerializationSchema<>(
QueryResult.class,
mapper -> {
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new SimpleModule().addDeserializer(
QueryResult.Query.Redirects.class,
new Query.Redirects.Deserializer()
));
mapper.registerModule(new SimpleModule().addDeserializer(
QueryResult.Query.Pages.class,
new Query.Pages.Deserializer(mapper)
));
}
);
private final Query query;
@JsonCreator
public QueryResult(@JsonProperty("query") final Query query) {
this.query = query;
}
public Query getQuery() {
return query;
}
public static final class Query {
private final Redirects redirects;
private final Pages pages;
@JsonCreator
public Query(@JsonProperty("redirects") final Redirects redirects, @JsonProperty("pages") final Pages pages) {
this.redirects = redirects == null ? new Redirects() : redirects;
this.pages = pages == null ? new Pages() : pages;
}
public Collection<Pages.Page> getPages() {
return Collections.unmodifiableCollection(pages.pages);
}
public Redirects getRedirects() {
return redirects;
}
public static final class Redirects {
private final Map<String, String> redirectMap = new HashMap<>();
private Redirects() { }
public String resolveRedirect(final String from) {
if (!redirectMap.containsKey(from)) {
return from;
} else if (redirectMap.containsKey(from) && !redirectMap.containsKey(redirectMap.get(from))) {
return redirectMap.get(from);
} else {
final Deque<String> redirectChain = new ArrayDeque<>();
redirectChain.add(from);
while (redirectMap.containsKey(redirectChain.getLast())) {
final String newVal = redirectMap.get(redirectChain.getLast());
if (redirectChain.contains(newVal)) {
Logging.warn("Circular redirect in Wikipedia detected: " + String.join(" → ", redirectChain));
return from;
} else {
redirectChain.add(newVal);
}
}
// Shortcut for future requests: Redirect all elements in the redirect chain to the last element.
while (redirectChain.size() >= 2) {
redirectMap.put(redirectChain.pollFirst(), redirectChain.getLast());
}
return redirectChain.getLast();
}
}
static class Deserializer extends StdDeserializer<Redirects> {
Deserializer() {
super((Class<?>) null);
}
@Override
public Redirects deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode node = p.getCodec().readTree(p);
final Redirects result = new Redirects();
node.elements().forEachRemaining(e -> {
final JsonNode from = e.get("from");
final JsonNode to = e.get("to");
if (from.isTextual() && to.isTextual()) {
result.redirectMap.put(from.textValue(), to.textValue());
}
});
return result;
}
}
}
public static class Pages {
private final Collection<Page> pages = new ArrayList<>();
public static class Page {
private final String title;
@JsonCreator
public Page(@JsonProperty("title") final String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
static class Deserializer extends StdDeserializer<Pages> {
private final ObjectMapper mapper;
Deserializer(final ObjectMapper mapper) {
super((Class<?>) null);
this.mapper = mapper;
}
@Override
public Pages deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode node = p.getCodec().readTree(p);
final Collection<JsonProcessingException> exception = new ArrayList<>();
final Pages pages = new Pages();
node.fields().forEachRemaining(entry -> {
try {
pages.pages.add(mapper.treeToValue(entry.getValue(), Page.class));
} catch (JsonProcessingException e) {
exception.add(e);
}
});
if (exception.size() >= 1) {
throw exception.iterator().next();
}
return pages;
}
}
}
}
}
// License: GPL. For details, see LICENSE file.
package org.wikipedia.data;
import org.wikipedia.api.wikidata_action.json.SitematrixResult;
public interface IWikipediaSite {
/**
* @return the site for a certain language, as returned by {@code action=sitematrix} with the Wikidata Action API
*/
public SitematrixResult.Sitematrix.Site getSite();
/**
* @return the language code of the wikipedia, always non-null
*/
public String getLanguageCode();
}
......@@ -3,7 +3,6 @@ package org.wikipedia.data;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import org.openstreetmap.josm.tools.I18n;
import org.wikipedia.api.ApiQuery;
import org.wikipedia.api.ApiQueryClient;
......@@ -13,8 +12,9 @@ import org.wikipedia.api.wikidata_action.json.SitematrixResult;
/**
* A Wikipedia site for a certain language. For each instance of this class you can assume that there is a Wikipedia in this language.
*/
public class WikipediaSite {
public class WikipediaSite implements IWikipediaSite {
private final SitematrixResult.Sitematrix.Site site;
private final SitematrixResult.Sitematrix.Language language;
/**
* Constructs a Wikipedia site for a given language (iff such a Wikipedia exists).
......@@ -25,23 +25,26 @@ public class WikipediaSite {
public WikipediaSite(final String langCode) throws IOException, IllegalArgumentException {
Objects.requireNonNull(langCode);
final SitematrixResult sitematrix = ApiQueryClient.query(WikidataActionApiQuery.sitematrix());
final Optional<SitematrixResult.Sitematrix.Language> language = sitematrix.getSitematrix().getLanguages().stream()
language = sitematrix.getSitematrix().getLanguages().stream()
.filter(it -> langCode.equals(it.getCode()))
.findFirst();
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(I18n.tr("''{0}'' is an illegal language code!", langCode)));
this.site = language
.orElseThrow(() -> new IllegalArgumentException(I18n.tr("{0} is an illegal language code!", langCode)))
.getSites().stream()
.filter(it -> "wiki".equals(it.getCode()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(
I18n.tr("For language {0} there is no Wikipedia site!", language.get().getCode())
I18n.tr("There is no Wikipedia site for language ''{0}''!", language.getCode())
));
}
/**
* @return the site for a certain language, as returned by {@code action=sitematrix} with the Wikidata Action API
*/
@Override
public SitematrixResult.Sitematrix.Site getSite() {
return site;
}
@Override
public String getLanguageCode() {
return language.getCode();
}
}
// License: GPL. For details, see LICENSE file.
package org.wikipedia.tools;
import java.io.IOException;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.tools.Pair;
import org.wikipedia.data.IWikipediaSite;
import org.wikipedia.data.WikipediaSite;
public final class OsmPrimitiveUtil {
private static final Pattern WIKIPEDIA_PATTERN = Pattern.compile("(.+):(.+)");
public static final String TAG_NAME_WIKIPEDIA = "wikipedia";
private OsmPrimitiveUtil() {
// Private constructor to avoid instantiation
}
/**
* Returns the language and article title iff the given primitive has a wikipedia=* tag of
* the form {@code (.+):(.+)} and the part before the colon is an existent Wikipedia language.
* @param primitive the primitive for which the Wikipedia site and title of the Wikipedia article will be returned
* @return A pair of the Wikipedia site as the first component, the article title as second component.
* Or an empty optional if there is either no wikipedia=* tag, or if the tag value does not match {@code (.+):(.+)},
* or if the Wikipedia language does not exist or is closed
*/
public static Optional<Pair<IWikipediaSite, String>> getWikipediaValue(final OsmPrimitive primitive) {
final String tagValue = primitive.get(TAG_NAME_WIKIPEDIA);
if (tagValue != null) {
final Matcher matcher = WIKIPEDIA_PATTERN.matcher(tagValue);
if (matcher.matches()) {
try {
final WikipediaSite site = new WikipediaSite(matcher.group(1));
if (!site.getSite().isClosed()) {
return Optional.of(Pair.create(site, matcher.group(2)));
}
} catch (IOException | IllegalArgumentException e) {
return Optional.empty();
}
}
}
return Optional.empty();
}
}
......@@ -14,6 +14,8 @@ class AllValidationTests {
static final ValidationTest<WikidataItemExists> WIKIDATA_ITEM_DOES_NOT_EXIST = new ValidationTest<>(Severity.ERROR, 30_002);
static final ValidationTest<WikidataItemExists> WIKIDATA_ITEM_IS_REDIRECT = new ValidationTest<>(Severity.WARNING, 30_003);
static final ValidationTest<WikipediaAgainstWikidata> WIKIDATA_ITEM_NOT_MATCHING_WIKIPEDIA = new ValidationTest<>(Severity.WARNING, 30_004);
static final ValidationTest<WikipediaRedirect> WIKIPEDIA_ARTICLE_REDIRECTS = new ValidationTest<>(Severity.WARNING, 30_005);
static final ValidationTest<WikipediaRedirect> WIKIPEDIA_TAG_INVALID = new ValidationTest<>(Severity.ERROR, 30_006);
// i18n: Prefix for the validator messages. Note the space at the end!
static final String VALIDATOR_MESSAGE_MARKER = I18n.tr("[Wiki] ");
......
......@@ -11,6 +11,7 @@ import org.openstreetmap.josm.data.validation.Test;
import org.openstreetmap.josm.gui.Notification;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.tools.I18n;
import org.openstreetmap.josm.tools.Logging;
import org.wikipedia.tools.ListUtil;
public abstract class BatchProcessedTagTest<T extends BatchProcessedTagTest.TestCompanion> extends Test.TagTest {
......@@ -60,11 +61,15 @@ public abstract class BatchProcessedTagTest<T extends BatchProcessedTagTest.Test
@Override
public final void endTest() {
check(primitivesForBatches);
if (finalNotification != null) {
finalNotification.show();
try {
check(primitivesForBatches);
if (finalNotification != null) {
finalNotification.show();
}
super.endTest();
} catch (Exception e) {
Logging.error(e);
}
super.endTest();
}
static abstract class TestCompanion {
......
// License: GPL. For details, see LICENSE file.
package org.wikipedia.validator;
import static org.wikipedia.validator.AllValidationTests.SEE_OTHER_CATEGORY_VALIDATOR_ERRORS;
import static org.wikipedia.validator.AllValidationTests.VALIDATOR_MESSAGE_MARKER;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.gui.Notification;
import org.openstreetmap.josm.tools.I18n;
import org.openstreetmap.josm.tools.Pair;
import org.wikipedia.WikipediaPlugin;
import org.wikipedia.api.ApiQueryClient;
import org.wikipedia.api.wikipedia_action.WikipediaActionApiQuery;
import org.wikipedia.api.wikipedia_action.json.QueryResult;
import org.wikipedia.data.IWikipediaSite;
import org.wikipedia.tools.ListUtil;
import org.wikipedia.tools.OsmPrimitiveUtil;
public class WikipediaRedirect extends BatchProcessedTagTest<WikipediaRedirect.TestCompanion> {
private static final Notification NETWORK_FAILED_NOTIFICATION = new Notification(
I18n.tr("Could not check for all wikipedia=* tags if they redirect to another lemma.") +
"\n" + SEE_OTHER_CATEGORY_VALIDATOR_ERRORS
).setIcon(WikipediaPlugin.LOGO);
public WikipediaRedirect() {
super(
I18n.tr("Check wikipedia=* is not a redirect"),
I18n.tr("Make sure that the wikipedia=* article is not redirecting to another lemma")
);
}
@Override
protected TestCompanion prepareTestCompanion(OsmPrimitive primitive) {
final String plainWikipediaValue = primitive.get(OsmPrimitiveUtil.TAG_NAME_WIKIPEDIA);
final Optional<Pair<IWikipediaSite, String>> companion = OsmPrimitiveUtil.getWikipediaValue(primitive);
if (plainWikipediaValue != null && !companion.isPresent()) {
errors.add(
AllValidationTests.WIKIPEDIA_TAG_INVALID.getBuilder(this)
.message(
VALIDATOR_MESSAGE_MARKER + I18n.tr("Wikipedia tag has invalid format!"),
I18n.marktr("The value ''{0}'' is not allowed for the wikipedia=* tag"),
plainWikipediaValue
)
.primitives(primitive)
.build()
);
}
return companion
.map(it -> new TestCompanion(primitive, it.a, it.b))
.orElse(null);
}
@Override
protected void check(List<TestCompanion> allPrimitives) {
allPrimitives.stream()
.collect(Collectors.groupingBy(it -> it.site.getLanguageCode()))
.forEach((langCode, primitiveList) -> {
ListUtil.processInBatches(
new ArrayList<>(primitiveList.stream()
.collect(Collectors.groupingBy(
it -> it.title,
Collectors.mapping(BatchProcessedTagTest.TestCompanion::getPrimitive, Collectors.toList())
))
.entrySet()
),
50,
batch -> {
primitiveList.stream().findAny().ifPresent(any -> {
this.checkBatch(any.site, batch);
});
},
this::updateBatchProgress
);
});
}
/**
* Check one batch containing only article titles for one Wikipedia
* @param site the Wikimedia site for which the titles should be checked
* @param batch a list of map entries, which map the title of an article to the list of primitives
* whose wikipedia=* tag points to that lemma.
*/
private void checkBatch(final IWikipediaSite site, final List<Map.Entry<String, List<OsmPrimitive>>> batch) {
try {
final QueryResult queryResult = ApiQueryClient.query(
WikipediaActionApiQuery.query(site, batch.stream().map(Map.Entry::getKey).collect(Collectors.toList()))
);
for (Map.Entry<String, List<OsmPrimitive>> entry : batch) {
final String redirectedTitle = queryResult.getQuery().getRedirects().resolveRedirect(entry.getKey());
if (redirectedTitle != null && !redirectedTitle.equals(entry.getKey())) {
errors.add(
AllValidationTests.WIKIPEDIA_ARTICLE_REDIRECTS.getBuilder(this)
.primitives(entry.getValue())
.message(
VALIDATOR_MESSAGE_MARKER + I18n.tr("Wikipedia article is a redirect"),
I18n.marktr("Wikipedia article ''{0}'' redirects to ''{1}''"),
entry.getKey(),
redirectedTitle
)
.fix(() -> {
// TODO: Allow the user to view either Wikipedia article
final int optionPaneResult = JOptionPane.showConfirmDialog(
null,
I18n.tr("Should the wikipedia tag be replaced with the redirect target? Make sure the meaning of the tag remains the same!\n\nBefore: wikipedia={0}:{1}\nAfter: wikipedia={0}:{2}", site.getLanguageCode(), entry.getKey(), redirectedTitle),
I18n.tr("Change wikipedia tag?"),
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE
);
if (optionPaneResult == JOptionPane.YES_OPTION) {
return new ChangePropertyCommand(
entry.getValue(),
OsmPrimitiveUtil.TAG_NAME_WIKIPEDIA,
site.getLanguageCode() + ':' + redirectedTitle
);
}
return null;
})
.build()
);
}
}
} catch (IOException e) {
errors.add(
AllValidationTests.API_REQUEST_FAILED.getBuilder(this)
.primitives(batch.stream().flatMap(it -> it.getValue().stream()).collect(Collectors.toList()))
.message(VALIDATOR_MESSAGE_MARKER + e.getMessage())
.build()
);
finalNotification = NETWORK_FAILED_NOTIFICATION;
}
}
static class TestCompanion extends BatchProcessedTagTest.TestCompanion {
final IWikipediaSite site;
final String title;
TestCompanion(OsmPrimitive primitive, final IWikipediaSite site, final String title) {
super(primitive);
this.site = Objects.requireNonNull(site);
this.title = Objects.requireNonNull(title);
}
}
}
{"batchcomplete":"","query":{"redirects":[{"from":"USA","to":"United States of America"},{"from":"United States of America","to":"United States"},{"from":"US","to":"United States"}],"pages":{"31880":{"pageid":31880,"ns":0,"title":"Universe"},"3434750":{"pageid":3434750,"ns":0,"title":"United States"}}}}
......@@ -18,8 +18,6 @@ import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
......@@ -31,6 +29,7 @@ import org.junit.Test;
import org.openstreetmap.josm.testutils.JOSMTestRules;
import org.wikipedia.api.ApiQueryClient;
import org.wikipedia.api.wikidata_action.json.CheckEntityExistsResult;
import org.wikipedia.testutils.ResourceFileLoader;
public class WikidataActionApiQueryTest {
......@@ -92,7 +91,7 @@ public class WikidataActionApiQueryTest {
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(getApiResponseBytesFromResource("wbgetentities/dewiki:Berlin.json"))
.withBody(ResourceFileLoader.getResourceBytes(WikidataActionApiQueryTest.class, "response/wbgetentities/dewiki:Berlin.json"))
)
);
......@@ -121,7 +120,7 @@ public class WikidataActionApiQueryTest {
aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody(getApiResponseBytesFromResource("wbgetentities/enwiki:2entities2missing.json"))
.withBody(ResourceFileLoader.getResourceBytes(WikidataActionApiQueryTest.class, "response/wbgetentities/enwiki:2entities2missing.json"))
)
);
......@@ -152,10 +151,6 @@ public class WikidataActionApiQueryTest {
verify(postRequestedFor(urlEqualTo("/")).withRequestBody(new EqualToPattern("format=json&utf8=1&formatversion=1&action=wbgetentities&props=sitelinks&sites=enwiki&sitefilter=enwiki&titles=United+States%7Cmissing-article%7CGreat+Britain%7CAnother+missing+article")));
}
public static byte[] getApiResponseBytesFromResource(final String path) throws URISyntaxException, IOException {
return Files.readAllBytes(Paths.get(WikidataActionApiQueryTest.class.getResource("response/" + path).toURI()));
}
/**
* Sets {@link WikidataActionApiQuery#defaultUrl} to the supplied URL
* @param url the new URL
......
// License: GPL. For details, see LICENSE file.
package org.wikipedia.api.wikipedia_action;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;