Commit 136205e0 authored by Florian Schäfer's avatar Florian Schäfer

No longer use `CapturingInputStream` and `InvalidApiQueryException`

The former is replaced by first reading the String from the stream, only then start decoding the JSON.
The latter is now just a simple IOException, that exception was no longer up to date, because for POST requests the query is no longer in the URL, so that Exception would not contain the important information (i.e. the query).
parent e2cd12c6
// License: GPL. For details, see LICENSE file.
package org.wikipedia.api;
import java.net.URL;
public class InvalidApiQueryException extends Exception {
public InvalidApiQueryException(final URL url) {
super("The API query to the following URL is invalid: " + url);
}
}
......@@ -3,24 +3,22 @@ package org.wikipedia.api.wikidata_action;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.awt.GraphicsEnvironment;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.jcs.engine.behavior.ICacheElement;
import org.openstreetmap.josm.gui.bugreport.BugReportDialog;
import org.openstreetmap.josm.tools.HttpClient;
import org.openstreetmap.josm.tools.I18n;
import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.bugreport.BugReport;
import org.openstreetmap.josm.tools.bugreport.ReportedException;
import org.wikipedia.Caches;
import org.wikipedia.api.ApiQuery;
import org.wikipedia.api.InvalidApiQueryException;
import org.wikipedia.api.wikidata_action.json.SerializationSchema;
import org.wikipedia.tools.CapturingInputStream;
public final class ApiQueryClient {
......@@ -29,7 +27,7 @@ public final class ApiQueryClient {
}
/**
* Execute the given query and converts the received JSON to the given Generic class {@code T}.
* Execute the given query and convert the received JSON to the given Generic class {@code T}.
* @param query the query that will be executed
* @param <T> the type to which the JSON is deserialized
* @return the resulting object received from the API (or from the cache, if the query allows caching, see {@link ApiQuery#getCacheExpiryTime()})
......@@ -37,27 +35,40 @@ public final class ApiQueryClient {
* with localized message is returned
*/
public static <T> T query(final ApiQuery<T> query) throws IOException {
final InputStream stream;
if (query.getCacheExpiryTime() >= 1) {
// If query should be cached, get the cache element
final ICacheElement<String, String> cachedElement = Caches.API_RESPONSES.getCacheElement(query.getCacheKey());
final String cachedValue = cachedElement == null ? null : cachedElement.getVal();
if (cachedValue == null || System.currentTimeMillis() - cachedElement.getElementAttributes().getCreateTime() > query.getCacheExpiryTime()) {
// If the cache element is not found or has expired, try to update the value in the cache
try {
final CapturingInputStream captureStream = new CapturingInputStream(getInputStreamForQuery(query));
final T newValue = query.getSchema().getMapper().readValue(captureStream, query.getSchema().getSchemaClass());
Caches.API_RESPONSES.put(query.getCacheKey(), new String(captureStream.getCapturedBytes(), StandardCharsets.UTF_8));
final String remoteResponse = new String(IOUtils.toByteArray(getInputStreamForQuery(query)), StandardCharsets.UTF_8);
Caches.API_RESPONSES.put(query.getCacheKey(), remoteResponse);
Logging.info("Successfully updated API cache for " + query.getCacheKey());
return newValue;
return query.getSchema().getMapper().readValue(
new ByteArrayInputStream(remoteResponse.getBytes(StandardCharsets.UTF_8)),
query.getSchema().getSchemaClass()
);
} catch (IOException e) {
if (cachedValue == null) {
throw new IOException(I18n.tr("Failed to read from the API and there's no response available in the cache to use instead!"), e);
throw wrapReadDecodeJsonExceptions(e);
}
// If there's an expired cache entry, continue using it
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());
return decodeJson(query.getSchema(), new ByteArrayInputStream(cachedElement.getVal().getBytes(StandardCharsets.UTF_8)));
stream = new ByteArrayInputStream(cachedValue.getBytes(StandardCharsets.UTF_8));
} else {
stream = getInputStreamForQuery(query);
}
try {
return query.getSchema().getMapper().readValue(stream, query.getSchema().getSchemaClass());
} catch (IOException e) {
throw wrapReadDecodeJsonExceptions(e);
}
return decodeJson(query.getSchema(), getInputStreamForQuery(query));
}
private static InputStream getInputStreamForQuery(final ApiQuery query) throws IOException {
......@@ -85,14 +96,14 @@ public final class ApiQueryClient {
final IOException wrapperEx = new IOException(I18n.tr(
// I18n: {0} is the query, normally as URL. {1} is the error message returned from the API
"The Wikidata Action API reported an invalid query for {0} ({1}). This is a programming error, please report to the Wikipedia plugin.",
query,
query.getCacheKey(),
errorHeader
));
Logging.error(wrapperEx.getMessage());
if (!GraphicsEnvironment.isHeadless()) {
BugReport report = new BugReport(BugReport.intercept(new InvalidApiQueryException(query.getUrl())));
BugReportDialog dialog = new BugReportDialog(report);
final ReportedException re = BugReport.intercept(wrapperEx).put("component", "Plugin wikipedia").put("keywords", "API");
final BugReportDialog dialog = new BugReportDialog(new BugReport(re));
dialog.setVisible(true);
}
throw wrapperEx;
......@@ -100,22 +111,19 @@ public final class ApiQueryClient {
return response.getContent();
}
private static <T> T decodeJson(final SerializationSchema<T> schema, final InputStream stream) throws IOException {
try {
return schema.getMapper().readValue(stream, schema.getSchemaClass());
} catch (IOException e) {
final IOException wrapper;
if (e instanceof JsonParseException || e instanceof JsonMappingException) {
wrapper = new IOException(I18n.tr("The cached JSON response from the Wikidata Action API can't be decoded!"), e);
} else {
wrapper = 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);
}
Logging.log(Level.WARNING, wrapper.getMessage(), e);
throw wrapper;
private static IOException wrapReadDecodeJsonExceptions(final IOException exception) {
final IOException wrapper;
if (exception instanceof JsonParseException || exception instanceof JsonMappingException) {
wrapper = new IOException(I18n.tr("The JSON response from the Wikidata Action API can't be decoded!"), exception);
} else {
wrapper = new IOException(I18n.tr(
// i18n: {0} is the name of the Exception, {1} is the message that exception provides
"When reading the JSON response from the Wikidata Action API, an error occured! ({0}: {1})",
exception.getClass().getSimpleName(),
exception.getLocalizedMessage()
), exception);
}
Logging.log(Level.WARNING, wrapper.getMessage(), exception);
return wrapper;
}
}
......@@ -79,7 +79,7 @@ public class WikidataActionApiQuery<T> extends ApiQuery<T> {
.setAccept("application/json")
.setHeader("Content-Type", "text/plain; charset=utf-8")
.setHeader("User-Agent", getUserAgent(TICKET_KEYWORDS))
.setReasonForRequest(String.join(" ", getQuery().split("&")))
.setReasonForRequest(getQuery().replace('&', ' '))
.setRequestBody(getQuery().getBytes(StandardCharsets.UTF_8));
}
}
// License: GPL. For details, see LICENSE file.
package org.wikipedia.tools;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* A {@link FilterInputStream} that redirects all calls to the contained {@link InputStream}
* and whenever a {@code read} method is called, the read bytes are recorded.
* You can receive all bytes that were read so far from the stream by calling the method {@link #getCapturedBytes()}.
*/
public class CapturingInputStream extends FilterInputStream {
private final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
public CapturingInputStream(final InputStream in) {
super(in);
}
/**
* @return the bytes that the {@link InputStream} has captured so far.
*/
public byte[] getCapturedBytes() {
return byteStream.toByteArray();
}
@Override
public int read() throws IOException {
final int result = super.read();
if (result >= 0) {
byteStream.write(result);
}
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
final int result = super.read(b, off, len);
byteStream.write(b, off, result);
return result;
}
}
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