MapWithAILayerInfo: Don't block JOSM startup

Signed-off-by: Taylor Smock <tsmock@fb.com>
pull/1/head
Taylor Smock 2021-06-30 16:03:17 -06:00
rodzic 64ff82db04
commit ed6b8bcd5d
7 zmienionych plików z 250 dodań i 89 usunięć

Wyświetl plik

@ -24,6 +24,7 @@ import org.openstreetmap.josm.gui.MapFrame;
import org.openstreetmap.josm.gui.download.DownloadDialog; import org.openstreetmap.josm.gui.download.DownloadDialog;
import org.openstreetmap.josm.gui.download.OSMDownloadSource; import org.openstreetmap.josm.gui.download.OSMDownloadSource;
import org.openstreetmap.josm.gui.preferences.PreferenceSetting; import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.io.remotecontrol.RequestProcessor; import org.openstreetmap.josm.io.remotecontrol.RequestProcessor;
import org.openstreetmap.josm.plugins.Plugin; import org.openstreetmap.josm.plugins.Plugin;
import org.openstreetmap.josm.plugins.PluginInformation; import org.openstreetmap.josm.plugins.PluginInformation;
@ -35,6 +36,7 @@ import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIObject;
import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIRemoteControl; import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIRemoteControl;
import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIUploadHook; import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIUploadHook;
import org.openstreetmap.josm.plugins.mapwithai.backend.MergeDuplicateWaysAction; import org.openstreetmap.josm.plugins.mapwithai.backend.MergeDuplicateWaysAction;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAILayerInfo;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.PreConflatedDataUtils; import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.PreConflatedDataUtils;
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.ConnectingNodeInformationTest; import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.ConnectingNodeInformationTest;
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.RoutingIslandsTest; import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.RoutingIslandsTest;
@ -118,6 +120,9 @@ public final class MapWithAIPlugin extends Plugin implements Destroyable {
MapPaintUtils.addMapWithAIPaintStyles(); MapPaintUtils.addMapWithAIPaintStyles();
destroyables = new ArrayList<>(); destroyables = new ArrayList<>();
// Run in EDT to avoid blocking (has to be run before MapWithAIDownloadOptions
// so its already initialized)
GuiHelper.runInEDT(MapWithAILayerInfo::getInstance);
MapWithAIDownloadOptions mapWithAIDownloadOptions = new MapWithAIDownloadOptions(); MapWithAIDownloadOptions mapWithAIDownloadOptions = new MapWithAIDownloadOptions();
mapWithAIDownloadOptions.addGui(DownloadDialog.getInstance()); mapWithAIDownloadOptions.addGui(DownloadDialog.getInstance());
destroyables.add(mapWithAIDownloadOptions); destroyables.add(mapWithAIDownloadOptions);

Wyświetl plik

@ -16,18 +16,18 @@ import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.swing.SwingUtilities; import javax.swing.SwingUtilities;
import org.openstreetmap.josm.actions.ExpertToggleAction; import org.openstreetmap.josm.actions.ExpertToggleAction;
import org.openstreetmap.josm.data.StructUtils; import org.openstreetmap.josm.data.StructUtils;
import org.openstreetmap.josm.data.imagery.ImageryInfo; import org.openstreetmap.josm.data.imagery.ImageryInfo;
import org.openstreetmap.josm.data.preferences.BooleanProperty; import org.openstreetmap.josm.data.preferences.BooleanProperty;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.PleaseWaitRunnable; import org.openstreetmap.josm.gui.PleaseWaitRunnable;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.io.CachedFile; import org.openstreetmap.josm.io.CachedFile;
import org.openstreetmap.josm.io.NetworkManager; import org.openstreetmap.josm.io.NetworkManager;
import org.openstreetmap.josm.io.imagery.ImageryReader; import org.openstreetmap.josm.io.imagery.ImageryReader;
@ -48,6 +48,8 @@ public class MapWithAILayerInfo {
*/ */
public static final BooleanProperty SHOW_PREVIEW = new BooleanProperty("mapwithai.sources.preview", false); public static final BooleanProperty SHOW_PREVIEW = new BooleanProperty("mapwithai.sources.preview", false);
/** Finish listeners */
private ListenerList<FinishListener> finishListenerListenerList = ListenerList.create();
/** List of all usable layers */ /** List of all usable layers */
private final List<MapWithAIInfo> layers = Collections.synchronizedList(new ArrayList<>()); private final List<MapWithAIInfo> layers = Collections.synchronizedList(new ArrayList<>());
/** List of layer ids of all usable layers */ /** List of layer ids of all usable layers */
@ -70,23 +72,29 @@ public class MapWithAILayerInfo {
private static MapWithAILayerInfo instance; private static MapWithAILayerInfo instance;
public static MapWithAILayerInfo getInstance() { public static MapWithAILayerInfo getInstance() {
AtomicBoolean finished; final AtomicBoolean finished = new AtomicBoolean();
synchronized (DEFAULT_LAYER_SITES) { synchronized (DEFAULT_LAYER_SITES) {
if (instance == null) { if (instance == null) {
finished = new AtomicBoolean(); instance = new MapWithAILayerInfo(() -> {
instance = new MapWithAILayerInfo(() -> finished.set(true)); synchronized (finished) {
finished.set(true);
finished.notifyAll();
}
});
} else { } else {
finished = null; finished.set(true);
} }
} }
// Avoid a deadlock in the EDT. // Avoid a deadlock in the EDT.
if (finished != null && !SwingUtilities.isEventDispatchThread()) { if (!finished.get() && !SwingUtilities.isEventDispatchThread()) {
while (!finished.get()) { synchronized (finished) {
try { while (!finished.get()) {
Thread.sleep(100); try {
} catch (InterruptedException e) { finished.wait();
Logging.error(e); } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Logging.error(e);
Thread.currentThread().interrupt();
}
} }
} }
} }
@ -169,7 +177,12 @@ public class MapWithAILayerInfo {
layers.removeIf(i -> i.getUrl().contains("localhost:8111")); layers.removeIf(i -> i.getUrl().contains("localhost:8111"));
Collections.sort(layers); Collections.sort(layers);
} }
loadDefaults(false, MainApplication.worker, fastFail, listener); // Ensure that the cache is initialized prior to running in the fork join pool
// on webstart
if (System.getSecurityManager() != null) {
ESRISourceReader.SOURCE_CACHE.getClass();
}
loadDefaults(false, ForkJoinPool.commonPool(), fastFail, listener);
} }
/** /**
@ -188,18 +201,53 @@ public class MapWithAILayerInfo {
* @since 12634 * @since 12634
*/ */
public void loadDefaults(boolean clearCache, ExecutorService worker, boolean fastFail, FinishListener listener) { public void loadDefaults(boolean clearCache, ExecutorService worker, boolean fastFail, FinishListener listener) {
final DefaultEntryLoader loader = new DefaultEntryLoader(clearCache, fastFail, listener); final DefaultEntryLoader loader = new DefaultEntryLoader(clearCache, fastFail);
if (this.finishListenerListenerList == null) {
this.finishListenerListenerList = ListenerList.create();
}
if (listener != null) {
this.finishListenerListenerList.addListener(listener);
}
if (worker == null) { if (worker == null) {
loader.realRun(); PleaseWaitRunnable pleaseWaitRunnable = new PleaseWaitRunnable(tr("Update default entries")) {
@Override
protected void cancel() {
loader.canceled = true;
}
@Override
protected void realRun() {
loader.run();
}
@Override
protected void finish() {
loader.finish();
}
};
pleaseWaitRunnable.run();
} else { } else {
worker.execute(loader); worker.execute(loader);
} }
} }
/**
* Add a listener for when the data finishes updating
*
* @param finishListener The listener
*/
public void addFinishListener(final FinishListener finishListener) {
if (this.finishListenerListenerList == null) {
finishListener.onFinish();
} else {
this.finishListenerListenerList.addListener(finishListener);
}
}
/** /**
* Loader/updater of the available imagery entries * Loader/updater of the available imagery entries
*/ */
class DefaultEntryLoader extends PleaseWaitRunnable { class DefaultEntryLoader implements Runnable {
private final boolean clearCache; private final boolean clearCache;
private final boolean fastFail; private final boolean fastFail;
@ -207,30 +255,28 @@ public class MapWithAILayerInfo {
private MapWithAISourceReader reader; private MapWithAISourceReader reader;
private boolean canceled; private boolean canceled;
private boolean loadError; private boolean loadError;
private final FinishListener listener;
DefaultEntryLoader(boolean clearCache, boolean fastFail, FinishListener listener) { DefaultEntryLoader(boolean clearCache, boolean fastFail) {
super(tr("Update default entries"));
this.clearCache = clearCache; this.clearCache = clearCache;
this.fastFail = fastFail; this.fastFail = fastFail;
this.listener = listener;
} }
@Override
protected void cancel() { protected void cancel() {
canceled = true; canceled = true;
Utils.close(reader); Utils.close(reader);
} }
@Override public void run() {
protected void realRun() { if (this.clearCache) {
ESRISourceReader.SOURCE_CACHE.clear();
}
for (String source : getImageryLayersSites()) { for (String source : getImageryLayersSites()) {
if (canceled) { if (canceled) {
return; return;
} }
loadSource(source); loadSource(source);
} }
GuiHelper.runInEDT(this::finish); this.finish();
} }
protected void loadSource(String source) { protected void loadSource(String source) {
@ -242,6 +288,9 @@ public class MapWithAILayerInfo {
reader = new MapWithAISourceReader(source); reader = new MapWithAISourceReader(source);
reader.setFastFail(fastFail); reader.setFastFail(fastFail);
Collection<MapWithAIInfo> result = reader.parse(); Collection<MapWithAIInfo> result = reader.parse();
// This is called here to "pre-cache" the layer information, to avoid blocking
// the EDT
this.updateEsriLayers(result);
newLayers.addAll(result); newLayers.addAll(result);
} catch (IOException ex) { } catch (IOException ex) {
loadError = true; loadError = true;
@ -249,18 +298,26 @@ public class MapWithAILayerInfo {
} }
} }
@Override /**
protected void finish() { * Update the esri layer information
defaultLayers.clear(); *
allDefaultLayers.clear(); * @param layers The layers to update
defaultLayers.addAll(newLayers); */
for (MapWithAIInfo layer : newLayers) { private void updateEsriLayers(@Nonnull final Collection<MapWithAIInfo> layers) {
for (MapWithAIInfo layer : layers) {
if (MapWithAIType.ESRI == layer.getSourceType()) { if (MapWithAIType.ESRI == layer.getSourceType()) {
allDefaultLayers.addAll(parseEsri(layer)); allDefaultLayers.addAll(parseEsri(layer));
} else { } else {
allDefaultLayers.add(layer); allDefaultLayers.add(layer);
} }
} }
}
protected void finish() {
defaultLayers.clear();
allDefaultLayers.clear();
defaultLayers.addAll(newLayers);
this.updateEsriLayers(newLayers);
defaultLayerIds.clear(); defaultLayerIds.clear();
Collections.sort(defaultLayers, new MapWithAIInfo.MapWithAIInfoCategoryComparator()); Collections.sort(defaultLayers, new MapWithAIInfo.MapWithAIInfoCategoryComparator());
@ -271,8 +328,10 @@ public class MapWithAILayerInfo {
if (!loadError && !defaultLayerIds.isEmpty()) { if (!loadError && !defaultLayerIds.isEmpty()) {
dropOldEntries(); dropOldEntries();
} }
if (listener != null) { final ListenerList<FinishListener> listenerList = MapWithAILayerInfo.this.finishListenerListenerList;
listener.onFinish(); MapWithAILayerInfo.this.finishListenerListenerList = null;
if (listenerList != null) {
listenerList.fireEvent(FinishListener::onFinish);
} }
} }
@ -283,8 +342,8 @@ public class MapWithAILayerInfo {
* @return The Feature Servers for the ESRI layer * @return The Feature Servers for the ESRI layer
*/ */
private Collection<MapWithAIInfo> parseEsri(MapWithAIInfo layer) { private Collection<MapWithAIInfo> parseEsri(MapWithAIInfo layer) {
try (ESRISourceReader esriReader = new ESRISourceReader(layer)) { try {
return esriReader.parse(); return new ESRISourceReader(layer).parse();
} catch (IOException e) { } catch (IOException e) {
Logging.error(e); Logging.error(e);
} }

Wyświetl plik

@ -14,6 +14,7 @@ import java.util.stream.Stream;
import javax.swing.table.DefaultTableModel; import javax.swing.table.DefaultTableModel;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAICategory; import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAICategory;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo; import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAILayerInfo; import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAILayerInfo;
@ -49,6 +50,7 @@ class MapWithAIDefaultLayerTableModel extends DefaultTableModel {
columnDataRetrieval.add(i -> i.getAttributionText(0, null, null)); columnDataRetrieval.add(i -> i.getAttributionText(0, null, null));
columnDataRetrieval.add(info -> Optional.ofNullable(info.getTermsOfUseURL()).orElse("")); columnDataRetrieval.add(info -> Optional.ofNullable(info.getTermsOfUseURL()).orElse(""));
columnDataRetrieval.add(i -> MapWithAILayerInfo.getInstance().getLayers().contains(i)); columnDataRetrieval.add(i -> MapWithAILayerInfo.getInstance().getLayers().contains(i));
MapWithAILayerInfo.getInstance().addFinishListener(() -> GuiHelper.runInEDT(this::fireTableDataChanged));
} }
/** /**
@ -58,12 +60,15 @@ class MapWithAIDefaultLayerTableModel extends DefaultTableModel {
* @return The imagery info at the given row number * @return The imagery info at the given row number
*/ */
public static MapWithAIInfo getRow(int row) { public static MapWithAIInfo getRow(int row) {
if (row == 0 && MapWithAILayerInfo.getInstance().getAllDefaultLayers().isEmpty()) {
return new MapWithAIInfo(tr("Loading"), "");
}
return MapWithAILayerInfo.getInstance().getAllDefaultLayers().get(row); return MapWithAILayerInfo.getInstance().getAllDefaultLayers().get(row);
} }
@Override @Override
public int getRowCount() { public int getRowCount() {
return MapWithAILayerInfo.getInstance().getAllDefaultLayers().size(); return Math.max(MapWithAILayerInfo.getInstance().getAllDefaultLayers().size(), 1);
} }
@Override @Override
@ -76,7 +81,7 @@ class MapWithAIDefaultLayerTableModel extends DefaultTableModel {
@Override @Override
public Object getValueAt(int row, int column) { public Object getValueAt(int row, int column) {
MapWithAIInfo info = MapWithAILayerInfo.getInstance().getAllDefaultLayers().get(row); MapWithAIInfo info = getRow(row);
if (column < columnDataRetrieval.size()) { if (column < columnDataRetrieval.size()) {
return columnDataRetrieval.get(column).apply(info); return columnDataRetrieval.get(column).apply(info);
} }

Wyświetl plik

@ -190,8 +190,8 @@ public class MapWithAIProvidersPanel extends JPanel {
if (MapWithAILayerTableModel.contains(info)) { if (MapWithAILayerTableModel.contains(info)) {
Color t = IMAGERY_BACKGROUND_COLOR.get(); Color t = IMAGERY_BACKGROUND_COLOR.get();
GuiHelper.setBackgroundReadable(label, isSelected ? t.darker() : t); GuiHelper.setBackgroundReadable(label, isSelected ? t.darker() : t);
} else if (this.area != null && (this.area.intersects(info.getBounds()) } else if (this.area != null && info.getBounds() != null
|| (info.getBounds() != null && info.getBounds().intersects(this.area)))) { && (this.area.intersects(info.getBounds()) || (info.getBounds().intersects(this.area)))) {
Color t = MAPWITHAI_AREA_BACKGROUND_COLOR.get(); Color t = MAPWITHAI_AREA_BACKGROUND_COLOR.get();
GuiHelper.setBackgroundReadable(label, isSelected ? t.darker() : t); GuiHelper.setBackgroundReadable(label, isSelected ? t.darker() : t);
} else { } else {
@ -657,7 +657,8 @@ public class MapWithAIProvidersPanel extends JPanel {
info.setSourceType(MapWithAIType.THIRD_PARTY); info.setSourceType(MapWithAIType.THIRD_PARTY);
} }
if (MapWithAIType.ESRI == info.getSourceType()) { if (MapWithAIType.ESRI == info.getSourceType()) {
try (ESRISourceReader reader = new ESRISourceReader(info)) { final ESRISourceReader reader = new ESRISourceReader(info);
try {
for (MapWithAIInfo i : reader.parse()) { for (MapWithAIInfo i : reader.parse()) {
activeModel.addRow(i); activeModel.addRow(i);
} }
@ -845,11 +846,11 @@ public class MapWithAIProvidersPanel extends JPanel {
@Override @Override
public void actionPerformed(ActionEvent evt) { public void actionPerformed(ActionEvent evt) {
MapWithAILayerInfo.getInstance().loadDefaults(true, MainApplication.worker, false, () -> { MapWithAILayerInfo.getInstance().loadDefaults(true, MainApplication.worker, false, () -> {
defaultModel.fireTableDataChanged(); GuiHelper.runInEDT(defaultModel::fireTableDataChanged);
defaultTable.getSelectionModel().clearSelection(); GuiHelper.runInEDT(defaultTable.getSelectionModel()::clearSelection);
defaultTableListener.clearMap(); GuiHelper.runInEDT(defaultTableListener::clearMap);
/* loading new file may change active layers */ /* loading new file may change active layers */
activeModel.fireTableDataChanged(); GuiHelper.runInEDT(activeModel::fireTableDataChanged);
}); });
} }
} }

Wyświetl plik

@ -1,18 +1,23 @@
// License: GPL. For details, see LICENSE file. // License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapwithai.io.mapwithai; package org.openstreetmap.josm.plugins.mapwithai.io.mapwithai;
import java.io.BufferedReader; import java.io.ByteArrayInputStream;
import java.io.Closeable; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.json.Json; import javax.json.Json;
import javax.json.JsonArray; import javax.json.JsonArray;
import javax.json.JsonNumber; import javax.json.JsonNumber;
@ -21,23 +26,30 @@ import javax.json.JsonReader;
import javax.json.JsonString; import javax.json.JsonString;
import javax.json.JsonStructure; import javax.json.JsonStructure;
import javax.json.JsonValue; import javax.json.JsonValue;
import javax.json.stream.JsonParsingException;
import org.openstreetmap.josm.data.cache.JCSCacheManager;
import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds; import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryBounds;
import org.openstreetmap.josm.io.CachedFile; import org.openstreetmap.josm.io.CachedFile;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAICategory; import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAICategory;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo; import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIType; import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIType;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.tools.HttpClient; import org.openstreetmap.josm.tools.HttpClient;
import org.openstreetmap.josm.tools.Logging; import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.Utils;
import org.apache.commons.jcs3.access.CacheAccess;
import org.apache.commons.jcs3.engine.behavior.IElementAttributes;
/** /**
* Take a {@link MapWithAIInfo.MapWithAIType#ESRI} layer and convert it to a * Take a {@link MapWithAIInfo.MapWithAIType#ESRI} layer and convert it to a
* list of "true" layers. * list of "true" layers.
*/ */
public class ESRISourceReader implements Closeable { public class ESRISourceReader {
/** The cache storing ESRI source information (json) */
public static final CacheAccess<String, String> SOURCE_CACHE = JCSCacheManager.getCache("mapwithai:esrisources", 5,
50_000, new File(Config.getDirs().getCacheDirectory(true), "mapwithai").getPath());
private final MapWithAIInfo source; private final MapWithAIInfo source;
private final List<CachedFile> cachedFiles = new ArrayList<>();
private boolean fastFail; private boolean fastFail;
private final List<MapWithAICategory> ignoreConflationCategories; private final List<MapWithAICategory> ignoreConflationCategories;
private static final String JSON_QUERY_PARAM = "?f=json"; private static final String JSON_QUERY_PARAM = "?f=json";
@ -84,11 +96,13 @@ public class ESRISourceReader implements Closeable {
String next = "1"; String next = "1";
String searchUrl = startReplace.matcher(search).replaceAll(next); String searchUrl = startReplace.matcher(search).replaceAll(next);
while (!next.equals("-1")) { while (!next.equals("-1")) {
try (CachedFile layers = new CachedFile(url + "content/groups/" + group + searchUrl); final String finalUrl = url + "content/groups/" + group + searchUrl;
BufferedReader i = layers.getContentReader(); final String jsonString = getJsonString(finalUrl, this.fastFail);
JsonReader reader = Json.createReader(i)) { if (jsonString == null) {
cachedFiles.add(layers); continue;
layers.setFastFail(fastFail); }
try (JsonReader reader = Json
.createReader(new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8)))) {
JsonStructure parser = reader.read(); JsonStructure parser = reader.read();
if (parser.getValueType() == JsonValue.ValueType.OBJECT) { if (parser.getValueType() == JsonValue.ValueType.OBJECT) {
JsonObject obj = parser.asJsonObject(); JsonObject obj = parser.asJsonObject();
@ -102,7 +116,7 @@ public class ESRISourceReader implements Closeable {
information.add(parse(feature)); information.add(parse(feature));
} }
} }
} catch (ClassCastException | IOException e) { } catch (ClassCastException e) {
Logging.error(e); Logging.error(e);
next = "-1"; next = "-1";
} }
@ -115,6 +129,59 @@ public class ESRISourceReader implements Closeable {
return information; return information;
} }
/**
* Get the json string for a URL
*
* @param url The URL to get
* @param fastFail Fail fast (1 second)
* @return The json string, or {@code null}.
*/
@Nullable
private static String getJsonString(@Nonnull final String url, final boolean fastFail) {
String jsonString = SOURCE_CACHE.get(url);
// TODO FIXME remove sometime after January 2022 (give it a chance to cleanup
// directories)
CachedFile.cleanup(url);
if (jsonString == null) {
synchronized (SOURCE_CACHE) {
jsonString = SOURCE_CACHE.get(url);
if (jsonString == null) {
HttpClient client = null;
try {
client = HttpClient.create(new URL(url));
if (fastFail) {
client.setReadTimeout(1000);
}
final HttpClient.Response response = client.connect();
jsonString = response.fetchContent();
if (jsonString != null && response.getResponseCode() < 400
&& response.getResponseCode() >= 200) {
final IElementAttributes elementAttributes = SOURCE_CACHE.getDefaultElementAttributes();
// getExpiration returns milliseconds
final long expirationTime = response.getExpiration();
if (expirationTime > 0) {
elementAttributes.setMaxLife(response.getExpiration());
} else {
elementAttributes.setMaxLife(TimeUnit.SECONDS.toMillis(
Config.getPref().getLong("mirror.maxtime", TimeUnit.DAYS.toSeconds(7))));
}
SOURCE_CACHE.put(url, jsonString, elementAttributes);
} else {
jsonString = null;
}
} catch (final IOException e) {
Logging.error(e);
} finally {
if (client != null) {
client.disconnect();
}
}
}
}
}
return jsonString;
}
private MapWithAIInfo parse(JsonObject feature) { private MapWithAIInfo parse(JsonObject feature) {
// Use the initial esri server information to keep conflation info // Use the initial esri server information to keep conflation info
MapWithAIInfo newInfo = new MapWithAIInfo(source); MapWithAIInfo newInfo = new MapWithAIInfo(source);
@ -168,42 +235,60 @@ public class ESRISourceReader implements Closeable {
return (newInfo); return (newInfo);
} }
private static String featureService(MapWithAIInfo mapwithaiInfo, String url) { /**
String toGet = url.endsWith(JSON_QUERY_PARAM) ? url : url.concat(JSON_QUERY_PARAM); * Get feature service information
try (CachedFile featureServer = new CachedFile(toGet); *
BufferedReader br = featureServer.getContentReader(); * @param mapwithaiInfo The info to update
JsonReader reader = Json.createReader(br)) { * @param url The url to get
* @return The base url for the feature service
*/
@Nullable
private String featureService(@Nonnull MapWithAIInfo mapwithaiInfo, @Nonnull String url) {
final String toGet = url.endsWith(JSON_QUERY_PARAM) ? url : url.concat(JSON_QUERY_PARAM);
final String jsonString = getJsonString(toGet, this.fastFail);
if (jsonString == null) {
return null;
}
try (JsonReader reader = Json
.createReader(new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8)))) {
JsonObject info = reader.readObject(); JsonObject info = reader.readObject();
JsonArray layers = info.getJsonArray("layers"); JsonArray layers = info.getJsonArray("layers");
// This fixes #20551 // This fixes #20551
if (layers == null || layers.stream().noneMatch(jsonvalue -> jsonvalue != null)) { if (layers == null || layers.stream().noneMatch(Objects::nonNull)) {
return null; return null;
} }
// TODO use all the layers? // TODO use all the layers?
JsonObject layer = layers.stream().filter(jsonValue -> jsonValue != null).findFirst() JsonObject layer = layers.stream().filter(Objects::nonNull).findFirst().orElse(JsonValue.EMPTY_JSON_OBJECT)
.orElse(JsonObject.EMPTY_JSON_OBJECT).asJsonObject(); .asJsonObject();
if (layer.containsKey("id")) { if (layer.containsKey("id")) {
String partialUrl = (url.endsWith("/") ? url : url + "/") + layer.getInt("id"); String partialUrl = (url.endsWith("/") ? url : url + "/") + layer.getInt("id");
mapwithaiInfo.setReplacementTags(getReplacementTags(partialUrl)); mapwithaiInfo.setReplacementTags(getReplacementTags(partialUrl));
return partialUrl; return partialUrl;
} }
return null; } catch (JsonParsingException e) {
} catch (IOException e) {
Logging.error(e); Logging.error(e);
return null;
} }
return null;
} }
private static Map<String, String> getReplacementTags(String layerUrl) { /**
* Get the replacement tags for a feature service
*
* @param layerUrl The service url
* @return A map of replacement tags
*/
@Nonnull
private Map<String, String> getReplacementTags(@Nonnull String layerUrl) {
String toGet = layerUrl.endsWith(JSON_QUERY_PARAM) ? layerUrl : layerUrl.concat(JSON_QUERY_PARAM); String toGet = layerUrl.endsWith(JSON_QUERY_PARAM) ? layerUrl : layerUrl.concat(JSON_QUERY_PARAM);
try (CachedFile featureServer = new CachedFile(toGet); final String jsonString = getJsonString(toGet, this.fastFail);
BufferedReader br = featureServer.getContentReader(); if (jsonString != null) {
JsonReader reader = Json.createReader(br)) { try (JsonReader reader = Json
return reader.readObject().getJsonArray("fields").getValuesAs(JsonObject.class).stream() .createReader(new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8)))) {
.collect(Collectors.toMap(o -> o.getString("name"), ESRISourceReader::getReplacementTag)); return reader.readObject().getJsonArray("fields").getValuesAs(JsonObject.class).stream()
} catch (IOException e) { .collect(Collectors.toMap(o -> o.getString("name"), ESRISourceReader::getReplacementTag));
Logging.error(e); }
} }
return Collections.emptyMap(); return Collections.emptyMap();
} }
@ -225,10 +310,4 @@ public class ESRISourceReader implements Closeable {
public void setFastFail(boolean fastFail) { public void setFastFail(boolean fastFail) {
this.fastFail = fastFail; this.fastFail = fastFail;
} }
@Override
public void close() throws IOException {
cachedFiles.forEach(Utils::close);
}
} }

Wyświetl plik

@ -9,6 +9,8 @@ import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo; import org.openstreetmap.josm.plugins.mapwithai.data.mapwithai.MapWithAIInfo;
@ -19,6 +21,16 @@ class ESRISourceReaderTest {
@RegisterExtension @RegisterExtension
MapWithAITestRules rule = (MapWithAITestRules) new MapWithAITestRules().wiremock().projection(); MapWithAITestRules rule = (MapWithAITestRules) new MapWithAITestRules().wiremock().projection();
@BeforeEach
void setUp() {
ESRISourceReader.SOURCE_CACHE.clear();
}
@AfterEach
void tearDown() {
this.setUp();
}
/** /**
* Test that ESRI servers are properly added * Test that ESRI servers are properly added
* *
@ -33,15 +45,14 @@ class ESRISourceReaderTest {
String tUrl = rule.getWireMock().baseUrl() + "/sharing/rest"; String tUrl = rule.getWireMock().baseUrl() + "/sharing/rest";
for (String url : Arrays.asList(tUrl, tUrl + "/")) { for (String url : Arrays.asList(tUrl, tUrl + "/")) {
info.setUrl(url); info.setUrl(url);
try (ESRISourceReader reader = new ESRISourceReader(info)) { final ESRISourceReader reader = new ESRISourceReader(info);
Collection<MapWithAIInfo> layers = reader.parse(); Collection<MapWithAIInfo> layers = reader.parse();
assertFalse(layers.isEmpty(), "There should be a MapWithAI layer"); assertFalse(layers.isEmpty(), "There should be a MapWithAI layer");
assertTrue(layers.stream().noneMatch(i -> info.getUrl().equals(i.getUrl())), assertTrue(layers.stream().noneMatch(i -> info.getUrl().equals(i.getUrl())),
"The ESRI server should be expanded to feature servers"); "The ESRI server should be expanded to feature servers");
assertTrue(layers.stream().allMatch(i -> MapWithAIType.ESRI_FEATURE_SERVER.equals(i.getSourceType())), assertTrue(layers.stream().allMatch(i -> MapWithAIType.ESRI_FEATURE_SERVER.equals(i.getSourceType())),
"There should only be ESRI feature servers"); "There should only be ESRI feature servers");
assertEquals(24, layers.size()); assertEquals(24, layers.size());
}
} }
} }
} }

Wyświetl plik

@ -20,6 +20,7 @@ import org.awaitility.Awaitility;
import org.awaitility.Durations; import org.awaitility.Durations;
import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation;
import org.junit.runners.model.InitializationError; import org.junit.runners.model.InitializationError;
import org.openstreetmap.josm.gui.progress.NullProgressMonitor; import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.io.OsmApi; import org.openstreetmap.josm.io.OsmApi;