Commit 4f321e04 authored by Florian Schäfer's avatar Florian Schäfer

Introduce ApiQuery, will replace ApiUrl as a more generic way of representing API queries

It encapsulates the URL, parameters like POST data and the `SerializationSchema` associated with the query.
parent 6dfdb928
package org.wikipedia.api;
import java.net.URL;
import org.wikipedia.api.wikidata_action.json.SerializationSchema;
public abstract class ApiQuery<T> {
private final SerializationSchema<T> schema;
private final URL url;
protected ApiQuery(final URL url, final SerializationSchema<T> schema) {
this.schema = schema;
this.url = url;
}
public SerializationSchema<T> getSchema() {
return schema;
}
public URL getUrl() {
return url;
}
public String getCacheKey() {
return url.toString();
}
}
......@@ -4,9 +4,8 @@ package org.wikipedia.api.wikidata_action;
import java.awt.GraphicsEnvironment;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.openstreetmap.josm.gui.bugreport.BugReportDialog;
......@@ -19,31 +18,50 @@ import org.wikipedia.api.InvalidApiQueryException;
import org.wikipedia.api.wikidata_action.json.SerializationSchema;
public final class ApiQueryClient {
private static final ObjectMapper JSON_OBJECT_MAPPER = new ObjectMapper();
static {
JSON_OBJECT_MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
private ApiQueryClient() {
// Private constructor to avoid instantiation
}
private static String getUserAgent() {
return String.format("JOSM-wikipedia (%s).", WikipediaPlugin.getVersionInfo()) +
"Report issues at https://josm.openstreetmap.de/newticket?component=Plugin%20wikipedia&priority=major&keywords=api%20wikidata%20ActionAPI";
}
public static <T> T query(final WikidataActionApiQuery<T> query) throws IOException {
return query(
HttpClient.create(query.getUrl(), "POST")
.setAccept("application/json")
.setHeader("Content-Type", "text/plain; charset=utf-8")
.setHeader("User-Agent", getUserAgent())
.setRequestBody(query.getQuery().getBytes(StandardCharsets.UTF_8)),
query.getUrl(),
query.getSchema().getSchemaClass(),
query.getSchema().getMapper()
);
}
/**
* Queries the given URL and converts the received JSON to the given class using the Jackson library
* @param url the {@link URL} to query
* @param klass the class object of the desired type
* @param schema the {@link SerializationSchema} determining how to map from JSON to the desired type
* @param <T> the type to which the JSON is deserialized
* @return the deserialized object
* @throws IOException if any error occurs while executing the query, with a translated message that can be shown to the user.
*/
public static <T> T query(final URL url, final SerializationSchema<T> schema) throws IOException {
return query(
HttpClient.create(url).setAccept("application/json").setHeader("User-Agent", getUserAgent()),
url,
schema.getSchemaClass(),
schema.getMapper()
);
}
private static <T> T query(final HttpClient client, final URL url, final Class<T> resultClass, final ObjectMapper mapper) throws IOException {
final HttpClient.Response response;
try {
response = HttpClient.create(url)
.setAccept("application/json")
.setHeader("User-Agent", String.format("JOSM-wikipedia (%s). Report issues at https://josm.openstreetmap.de/newticket?component=Plugin%%20wikipedia&priority=major&keywords=api%%20wikidata%%20ActionAPI", WikipediaPlugin.getVersionInfo()))
.connect();
response = client.connect();
} catch (IOException e) {
// i18n: {0} is the name of the exception, {1} is the message of the exception. Typical values would be: {0}="UnknownHostException" {1}="www.wikidata.org"
throw new IOException(I18n.tr("Could not connect to the Wikidata Action API, probably a network issue or the website is currently offline ({0}: {1})", e.getClass().getSimpleName(), e.getLocalizedMessage()), e);
......@@ -64,10 +82,12 @@ public final class ApiQueryClient {
throw new IOException(I18n.tr("The Wikidata Action API reported that the query was invalid! Please report as bug to the Wikipedia plugin!"));
}
try {
return schema.getMapper().readValue(response.getContent(), schema.getSchemaClass());
return mapper.readValue(response.getContent(), resultClass);
} catch (JsonMappingException | JsonParseException e) {
Logging.warn(e);
throw new IOException(I18n.tr("The JSON response from the Wikidata Action API can't be read!"), e);
} catch (IOException e) {
Logging.warn(e);
throw new IOException(I18n.tr("When reading the JSON response from the Wikidata Action API, an error occured! ({0}: {1})", e.getClass().getSimpleName(), e.getLocalizedMessage()), e);
}
}
......
package org.wikipedia.api.wikidata_action;
import java.net.URL;
import org.wikipedia.api.ApiQuery;
import org.wikipedia.api.ApiUrl;
import org.wikipedia.api.wikidata_action.json.SerializationSchema;
import org.wikipedia.api.wikidata_action.json.SitematrixResult;
public class WikidataActionApiQuery<T> extends ApiQuery<T> {
private static final URL URL = ApiUrl.url("https://www.wikidata.org/w/api.php");
private static final String FORMAT_PARAMS = "format=json&utf8=1&formatversion=1";
private final String query;
private WikidataActionApiQuery(final String query, final SerializationSchema<T> schema) {
super(URL, schema);
this.query = query;
}
public String getQuery() {
return query;
}
public static WikidataActionApiQuery<SitematrixResult> sitematrix() {
return new WikidataActionApiQuery<>(
FORMAT_PARAMS + "&action=sitematrix",
SerializationSchema.SITEMATRIX
);
}
@Override
public String getCacheKey() {
return getUrl().toString() + '?' + getQuery();
}
}
// License: GPL. For details, see LICENSE file.
package org.wikipedia.api.wikidata_action.json;
import java.util.Objects;
import java.util.function.Consumer;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
......@@ -22,14 +22,24 @@ public class SerializationSchema<T> {
));
}
);
public static final SerializationSchema<SitematrixResult> SITEMATRIX = new SerializationSchema<>(
SitematrixResult.class,
mapper -> {
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new SimpleModule().addDeserializer(
SitematrixResult.Sitematrix.class,
new SitematrixResult.Sitematrix.Deserializer(mapper)
));
}
);
private final ObjectMapper mapper;
private final ObjectMapper mapper = new ObjectMapper();
private final Class<T> schemaClass;
private SerializationSchema(final Class<T> schemaClass, final Consumer<ObjectMapper> mapperConfig) {
Objects.requireNonNull(schemaClass);
this.schemaClass = schemaClass;
mapper = new ObjectMapper();
mapperConfig.accept(mapper);
mapperConfig.accept(mapper); // configure the object mapper
}
public ObjectMapper getMapper() {
......
package org.wikipedia.api.wikidata_action.json;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.wikipedia.tools.RegexUtil;
public final class SitematrixResult {
private final Sitematrix sitematrix;
@JsonCreator
public SitematrixResult(@JsonProperty("sitematrix") final Sitematrix sitematrix) {
this.sitematrix = sitematrix;
}
public Sitematrix getSitematrix() {
return sitematrix;
}
public static final class Sitematrix {
private final Collection<Language> languages = new ArrayList<>();
private final Collection<Site> specialSites = new ArrayList<>();
private Sitematrix() {
// Instantiates empty object
}
public Collection<Language> getLanguages() {
return Collections.unmodifiableCollection(languages);
}
public Collection<Site> getSpecialSites() {
return Collections.unmodifiableCollection(specialSites);
}
public static class Deserializer extends StdDeserializer<Sitematrix> {
private final ObjectMapper mapper;
public Deserializer(final ObjectMapper mapper) {
super((Class<?>) null);
this.mapper = mapper;
}
@Override
public Sitematrix deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final Sitematrix result = new Sitematrix();
final JsonNode node = p.getCodec().readTree(p);
final Iterator<Map.Entry<String, JsonNode>> fields = node.fields();
while (fields.hasNext()) {
final Map.Entry<String, JsonNode> field = fields.next();
if (RegexUtil.INTEGER_PATTERN.matcher(field.getKey()).matches()) {
result.languages.add(mapper.treeToValue(field.getValue(), Language.class));
}
if ("specials".equals(field.getKey())) {
final Iterator<JsonNode> elements = field.getValue().elements();
while (elements.hasNext()) {
result.specialSites.add(mapper.treeToValue(elements.next(), Site.class));
}
}
}
return result;
}
}
public static final class Language {
private final String code;
private final String name;
private final Collection<Site> sites = new ArrayList<>();
@JsonCreator
public Language(
@JsonProperty("code") final String code,
@JsonProperty("name") final String name,
@JsonProperty("site") final Collection<Site> sites
) {
this.code = code;
this.name = name;
if (sites != null) {
this.sites.addAll(sites);
}
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
public Collection<Site> getSites() {
return Collections.unmodifiableCollection(sites);
}
}
public static final class Site {
private final boolean closed;
private final String code;
private final String dbName;
private final String url;
@JsonCreator
public Site(
@JsonProperty("url") final String url,
@JsonProperty("dbname") final String dbName,
@JsonProperty("code") final String code,
@JsonProperty("closed") final String closed
) {
this.closed = closed != null;
this.code = code;
this.dbName = dbName;
this.url = url;
}
public String getCode() {
return code;
}
public String getDbName() {
return dbName;
}
public String getUrl() {
return url;
}
public boolean isClosed() {
return closed;
}
}
}
}
......@@ -8,6 +8,8 @@ public class RegexUtil {
private static final Pattern SITE_ID_PATTERN = Pattern.compile("^[a-z][a-z][a-z]?wiki$");
public static final Pattern WIKIPEDIA_TAG_VALUE_PATTERN = Pattern.compile("([a-z][a-z][a-z]?):(.+)");
public static final Pattern INTEGER_PATTERN = Pattern.compile("^[0-9]+$");
private RegexUtil() {
// Private constructor to avoid instantiation
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment