source: josm/trunk/test/unit/org/openstreetmap/josm/testutils/TileSourceRule.java

Last change on this file was 19519, checked in by stoecker, 7 weeks ago

unify eol-style

  • Property svn:eol-style set to native
File size: 13.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.testutils;
3
4import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
5import static org.openstreetmap.josm.TestUtils.getPrivateStaticField;
6
7import java.awt.Color;
8import java.awt.Graphics2D;
9import java.awt.image.BufferedImage;
10import java.io.ByteArrayOutputStream;
11import java.io.IOException;
12import java.util.Arrays;
13import java.util.Collections;
14import java.util.HashMap;
15import java.util.List;
16import java.util.Objects;
17
18import javax.imageio.ImageIO;
19
20import org.junit.runner.Description;
21import org.junit.runners.model.Statement;
22import org.openstreetmap.josm.data.imagery.ImageryInfo;
23import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
24import org.openstreetmap.josm.gui.bbox.SlippyMapBBoxChooser;
25import org.openstreetmap.josm.tools.Logging;
26
27import com.github.tomakehurst.wiremock.client.MappingBuilder;
28import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
29import com.github.tomakehurst.wiremock.client.WireMock;
30import com.github.tomakehurst.wiremock.junit.WireMockRule;
31
32/**
33 * A JUnit rule, based on {@link WireMockRule} to provide a test with a simple mock tile server serving multiple tile
34 * sources.
35 */
36public class TileSourceRule extends WireMockRule {
37 private static class ByteArrayWrapper {
38 public final byte[] byteArray;
39
40 ByteArrayWrapper(byte[] ba) {
41 this.byteArray = ba;
42 }
43 }
44
45 /**
46 * allocation is expensive and many tests may be wanting to set up the same tile sources one after the other, hence
47 * this cache
48 */
49 public static final HashMap<ConstSource, ByteArrayWrapper> constPayloadCache = new HashMap<>();
50
51 /**
52 * Class defining a tile source for TileSourceRule to mock. Due to the way WireMock is designed, it is far more
53 * straightforward to serve a single image in all tile positions
54 */
55 public abstract static class ConstSource {
56 /**
57 * method for actually generating the payload body bytes, uncached
58 * @return the payload body bytes
59 */
60 public abstract byte[] generatePayloadBytes();
61
62 /**
63 * @return a {@link MappingBuilder} representing the request matching properties of this tile source, suitable
64 * for passing to {@link WireMockRule#stubFor}.
65 */
66 public abstract MappingBuilder getMappingBuilder();
67
68 /**
69 * @return text label/name for this source if displayed in JOSM menus
70 */
71 public abstract String getLabel();
72
73 /**
74 * @param port the port this WireMock server is running on
75 * @return {@link ImageryInfo} describing this tile source, as might be submitted to {@link ImageryLayerInfo#add}
76 */
77 public abstract ImageryInfo getImageryInfo(int port);
78
79 /**
80 * @return byte array of the payload body for this source, possibly retrieved from a global cache
81 */
82 public byte[] getPayloadBytes() {
83 ByteArrayWrapper payloadWrapper = constPayloadCache.get(this);
84 if (payloadWrapper == null) {
85 payloadWrapper = new ByteArrayWrapper(this.generatePayloadBytes());
86 constPayloadCache.put(this, payloadWrapper);
87 }
88 return payloadWrapper.byteArray;
89 }
90
91 /**
92 * @return a {@link ResponseDefinitionBuilder} embodying the payload of this tile source suitable for
93 * application to a {@link MappingBuilder}.
94 */
95 public ResponseDefinitionBuilder getResponseDefinitionBuilder() {
96 return WireMock.aResponse().withStatus(200).withHeader("Content-Type", "image/png").withBody(
97 this.getPayloadBytes()
98 );
99 }
100 }
101
102 /**
103 * A plain color tile source
104 */
105 public static class ColorSource extends ConstSource {
106 protected final Color color;
107 protected final String label;
108 protected final int tileSize;
109
110 /**
111 * @param color Color for these tiles
112 * @param label text label/name for this source if displayed in JOSM menus
113 * @param tileSize Pixel dimension of tiles (usually 256)
114 */
115 public ColorSource(Color color, String label, int tileSize) {
116 this.color = color;
117 this.label = label;
118 this.tileSize = tileSize;
119 }
120
121 @Override
122 public int hashCode() {
123 return Objects.hash(this.color, this.label, this.tileSize, this.getClass());
124 }
125
126 @Override
127 public byte[] generatePayloadBytes() {
128 BufferedImage image = new BufferedImage(this.tileSize, this.tileSize, BufferedImage.TYPE_INT_RGB);
129 Graphics2D g = image.createGraphics();
130 g.setBackground(this.color);
131 g.clearRect(0, 0, image.getWidth(), image.getHeight());
132
133 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
134 try {
135 ImageIO.write(image, "png", outputStream);
136 } catch (IOException e) {
137 Logging.trace(e);
138 }
139 return outputStream.toByteArray();
140 }
141
142 @Override
143 public MappingBuilder getMappingBuilder() {
144 return WireMock.get(WireMock.urlMatching(String.format("/%h/(\\d+)/(\\d+)/(\\d+)\\.png", this.hashCode())));
145 }
146
147 @Override
148 public ImageryInfo getImageryInfo(int port) {
149 return new ImageryInfo(
150 this.label,
151 String.format("tms[20]:http://localhost:%d/%h/{z}/{x}/{y}.png", port, this.hashCode()),
152 "tms",
153 (String) null,
154 (String) null,
155 this.label
156 );
157 }
158
159 @Override
160 public String getLabel() {
161 return this.label;
162 }
163 }
164
165 protected final List<ConstSource> sourcesList;
166 protected final boolean clearLayerList;
167 protected final boolean clearSlippyMapSources;
168 protected final boolean registerInLayerList;
169
170 /**
171 * Construct a TileSourceRule for use with a JUnit test.
172 *
173 * This variant will not make any attempt to register the sources' existence with any JOSM subsystems, so is safe
174 * for direct application to a JUnit test.
175 *
176 * @param sources tile sources to serve from this mock server
177 */
178 public TileSourceRule(ConstSource... sources) {
179 this(false, false, false, sources);
180 }
181
182 /**
183 * Construct a TileSourceRule for use with a JUnit test.
184 *
185 * The three boolean parameters control whether to perform various steps registering the tile sources with parts
186 * of JOSM's internals as part of the setup process. It is advised to only enable any of these if it can be ensured
187 * that this rule will have its setup routine executed *after* the relevant parts of JOSM have been set up, e.g.
188 * when handled by {@link org.openstreetmap.josm.testutils.JOSMTestRules#fakeImagery}.
189 *
190 * @param clearLayerList whether to clear ImageryLayerInfo's layer list of any pre-existing entries
191 * @param clearSlippyMapSources whether to clear SlippyMapBBoxChooser's stubborn fallback Mapnik TileSource
192 * @param registerInLayerList whether to add sources to ImageryLayerInfo's layer list
193 * @param sources tile sources to serve from this mock server
194 */
195 @SuppressWarnings("this-escape")
196 public TileSourceRule(
197 boolean clearLayerList,
198 boolean clearSlippyMapSources,
199 boolean registerInLayerList,
200 ConstSource... sources
201 ) {
202 super(options().dynamicPort());
203 this.clearLayerList = clearLayerList;
204 this.clearSlippyMapSources = clearSlippyMapSources;
205 this.registerInLayerList = registerInLayerList;
206
207 // set up a stub target for the early request hack
208 this.stubFor(WireMock.get(
209 WireMock.urlMatching("/_poke")
210 ).willReturn(
211 WireMock.aResponse().withStatus(200).withBody("ow.")
212 ));
213
214 this.sourcesList = Collections.unmodifiableList(Arrays.asList(sources));
215 for (ConstSource source : this.sourcesList) {
216 this.stubFor(source.getMappingBuilder().willReturn(source.getResponseDefinitionBuilder()));
217 }
218 }
219
220 /**
221 * Get the tile sources served by this TileSourceRule.
222 *
223 * @return an unmodifiable list of the tile sources served by this TileSourceRule
224 */
225 public List<ConstSource> getSourcesList() {
226 return Collections.unmodifiableList(this.sourcesList);
227 }
228
229 /**
230 * A junit-rule {@code apply} method exposed separately to allow a chaining rule to put this much earlier in
231 * the test's initialization routine. The idea being to allow WireMock's web server to be starting up while other
232 * necessary initialization is taking place.
233 * See {@link org.junit.rules.TestRule#apply} for arguments.
234 * @param base The {@link Statement} to be modified
235 * @param description A {@link Description} of the test implemented in {@code base}
236 * @return a new statement, which may be the same as {@code base},
237 * a wrapper around {@code base}, or a completely new Statement.
238 */
239 public Statement applyRunServer(Statement base, Description description) {
240 return super.apply(new Statement() {
241 @Override
242 public void evaluate() throws Throwable {
243 try {
244 // a hack to circumvent a WireMock bug concerning delayed server startup. sending an early request
245 // to the mock server seems to prompt it to start earlier (though this request itself is not
246 // expected to succeed). see https://github.com/tomakehurst/wiremock/issues/97
247 new java.net.URL(TileSourceRule.this.url("/_poke")).getContent();
248 } catch (IOException e) {
249 Logging.trace(e);
250 }
251 base.evaluate();
252 }
253 }, description);
254 }
255
256 /**
257 * A junit-rule {@code apply} method exposed separately, containing initialization steps which can only be performed
258 * once more of josm's environment has been set up.
259 * See {@link org.junit.rules.TestRule#apply} for arguments.
260 * @param base The {@link Statement} to be modified
261 * @param description A {@link Description} of the test implemented in {@code base}
262 * @return a new statement, which may be the same as {@code base},
263 * a wrapper around {@code base}, or a completely new Statement.
264 */
265 public Statement applyRegisterLayers(Statement base, Description description) {
266 if (this.registerInLayerList || this.clearLayerList) {
267 return new Statement() {
268 @Override
269 @SuppressWarnings("unchecked")
270 public void evaluate() throws Throwable {
271 List<SlippyMapBBoxChooser.TileSourceProvider> slippyMapProviders = null;
272 SlippyMapBBoxChooser.TileSourceProvider slippyMapDefaultProvider = null;
273 List<ImageryInfo> originalImageryInfoList = null;
274 if (TileSourceRule.this.clearSlippyMapSources) {
275 try {
276 slippyMapProviders = (List<SlippyMapBBoxChooser.TileSourceProvider>) getPrivateStaticField(
277 SlippyMapBBoxChooser.class,
278 "providers"
279 );
280 // pop this off the beginning of the list, keep for later
281 slippyMapDefaultProvider = slippyMapProviders.remove(0);
282 } catch (ReflectiveOperationException e) {
283 Logging.warn("Failed to remove default SlippyMapBBoxChooser TileSourceProvider");
284 }
285 }
286
287 if (TileSourceRule.this.clearLayerList) {
288 originalImageryInfoList = ImageryLayerInfo.instance.getLayers();
289 ImageryLayerInfo.instance.clear();
290 }
291 if (TileSourceRule.this.registerInLayerList) {
292 for (ConstSource source : TileSourceRule.this.sourcesList) {
293 ImageryLayerInfo.addLayer(source.getImageryInfo(TileSourceRule.this.port()));
294 }
295 }
296
297 try {
298 base.evaluate();
299 } finally {
300 // clean up to original state
301 if (slippyMapDefaultProvider != null && slippyMapProviders != null) {
302 slippyMapProviders.add(0, slippyMapDefaultProvider);
303 }
304 if (originalImageryInfoList != null) {
305 ImageryLayerInfo.instance.clear();
306 ImageryLayerInfo.addLayers(originalImageryInfoList);
307 }
308 }
309 }
310 };
311 } else {
312 return base;
313 }
314 }
315
316 /**
317 * A standard implementation of apply which simply calls both sub- {@code apply} methods, {@link #applyRunServer}
318 * and {@link #applyRegisterLayers}. Called when used as a standard junit rule.
319 */
320 @Override
321 public Statement apply(Statement base, Description description) {
322 return applyRunServer(applyRegisterLayers(base, description), description);
323 }
324}
Note: See TracBrowser for help on using the repository browser.