| | 1 | // License: GPL. For details, see LICENSE file. |
| | 2 | package org.openstreetmap.josm.testutils; |
| | 3 | |
| | 4 | import java.io.File; |
| | 5 | import java.io.IOException; |
| | 6 | import java.nio.file.Path; |
| | 7 | import java.util.HashMap; |
| | 8 | import java.util.List; |
| | 9 | import java.util.Map; |
| | 10 | import java.util.Objects; |
| | 11 | import java.util.jar.JarFile; |
| | 12 | import java.util.stream.Collectors; |
| | 13 | |
| | 14 | import org.openstreetmap.josm.TestUtils; |
| | 15 | import org.openstreetmap.josm.tools.Logging; |
| | 16 | |
| | 17 | import org.junit.runner.Description; |
| | 18 | import org.junit.runners.model.Statement; |
| | 19 | |
| | 20 | import com.google.common.collect.ImmutableList; |
| | 21 | |
| | 22 | import com.github.tomakehurst.wiremock.client.MappingBuilder; |
| | 23 | import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; |
| | 24 | import com.github.tomakehurst.wiremock.client.WireMock; |
| | 25 | import com.github.tomakehurst.wiremock.core.Options; |
| | 26 | import com.github.tomakehurst.wiremock.junit.WireMockRule; |
| | 27 | import com.github.tomakehurst.wiremock.WireMockServer; |
| | 28 | |
| | 29 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; |
| | 30 | |
| | 31 | public class PluginServer { |
| | 32 | public static class RemotePlugin { |
| | 33 | private final File srcJar; |
| | 34 | private final Map<String, String> attrOverrides; |
| | 35 | private final String pluginName; |
| | 36 | private final String pluginURL; |
| | 37 | |
| | 38 | public RemotePlugin( |
| | 39 | final File srcJar |
| | 40 | ) { |
| | 41 | this(srcJar, null, null, null); |
| | 42 | } |
| | 43 | |
| | 44 | public RemotePlugin( |
| | 45 | final File srcJar, |
| | 46 | final Map<String, String> attrOverrides |
| | 47 | ) { |
| | 48 | this(srcJar, attrOverrides, null, null); |
| | 49 | } |
| | 50 | |
| | 51 | public RemotePlugin( |
| | 52 | final File srcJar, |
| | 53 | final Map<String, String> attrOverrides, |
| | 54 | final String pluginName |
| | 55 | ) { |
| | 56 | this(srcJar, attrOverrides, pluginName, null); |
| | 57 | } |
| | 58 | |
| | 59 | public RemotePlugin( |
| | 60 | final File srcJar, |
| | 61 | final Map<String, String> attrOverrides, |
| | 62 | final String pluginName, |
| | 63 | final String pluginURL |
| | 64 | ) { |
| | 65 | this.srcJar = srcJar; |
| | 66 | this.attrOverrides = attrOverrides; |
| | 67 | this.pluginName = pluginName; |
| | 68 | this.pluginURL = pluginURL; |
| | 69 | } |
| | 70 | |
| | 71 | @Override |
| | 72 | public int hashCode() { |
| | 73 | return Objects.hash(this.srcJar, this.attrOverrides, this.pluginName, this.pluginURL, this.getClass()); |
| | 74 | } |
| | 75 | |
| | 76 | public String getRemotePluginsListManifestSection() { |
| | 77 | final Map<String, String> attrs = new HashMap<String, String>(); |
| | 78 | JarFile jarFile = null; |
| | 79 | |
| | 80 | if (srcJar != null) { |
| | 81 | try { |
| | 82 | jarFile = new JarFile(srcJar, false); |
| | 83 | jarFile.getManifest().getMainAttributes().entrySet().stream().forEach( |
| | 84 | entry -> attrs.put(entry.getKey().toString(), entry.getValue().toString()) |
| | 85 | ); |
| | 86 | } catch (IOException e) { |
| | 87 | Logging.warn( |
| | 88 | "Failed to open {0} as a jar file. Using empty initial manifest. Error was: {1}", |
| | 89 | srcJar, |
| | 90 | e |
| | 91 | ); |
| | 92 | } finally { |
| | 93 | if (jarFile != null) { |
| | 94 | try { |
| | 95 | jarFile.close(); |
| | 96 | } catch (IOException e) { |
| | 97 | Logging.warn( |
| | 98 | "Somehow failed to close jar file {0}. Error was: {1}", |
| | 99 | srcJar, |
| | 100 | e |
| | 101 | ); |
| | 102 | } |
| | 103 | } |
| | 104 | } |
| | 105 | } |
| | 106 | |
| | 107 | if (this.attrOverrides != null) { |
| | 108 | attrs.putAll(this.attrOverrides); |
| | 109 | } |
| | 110 | |
| | 111 | return attrs.entrySet().stream().filter(entry -> entry.getValue() != null).map( |
| | 112 | entry -> String.format("\t%s: %s\n", entry.getKey(), entry.getValue()) |
| | 113 | ).collect(Collectors.joining()); |
| | 114 | } |
| | 115 | |
| | 116 | private String getJarPathBeneathFilesDir() { |
| | 117 | if (this.srcJar != null) { |
| | 118 | final Path jarPath = this.srcJar.toPath().toAbsolutePath().normalize(); |
| | 119 | final Path filesRootPath = new File(TestUtils.getTestDataRoot()).toPath().toAbsolutePath().resolve("__files").normalize(); |
| | 120 | |
| | 121 | if (jarPath.startsWith(filesRootPath)) { |
| | 122 | return filesRootPath.relativize(jarPath).toString(); |
| | 123 | } |
| | 124 | } |
| | 125 | return null; |
| | 126 | } |
| | 127 | |
| | 128 | protected String getPluginURLPath() { |
| | 129 | final String jarPathBeneathFilesDir = this.getJarPathBeneathFilesDir(); |
| | 130 | |
| | 131 | if (jarPathBeneathFilesDir != null) { |
| | 132 | return "/" + jarPathBeneathFilesDir; |
| | 133 | } |
| | 134 | |
| | 135 | return String.format("/%h/%s.jar", this.hashCode(), pluginName != null ? pluginName : Integer.toHexString(this.hashCode())); |
| | 136 | } |
| | 137 | |
| | 138 | public String getPluginURL(Integer port) { |
| | 139 | if (this.pluginURL != null) { |
| | 140 | return this.pluginURL; |
| | 141 | } else if (port != null && this.getJarPathBeneathFilesDir() != null) { |
| | 142 | return String.format("http://localhost:%s%s", port, this.getPluginURLPath()); |
| | 143 | } |
| | 144 | return "http://example.com" + this.getPluginURLPath(); |
| | 145 | } |
| | 146 | |
| | 147 | public String getName() { |
| | 148 | if (this.pluginName != null) { |
| | 149 | return this.pluginName; |
| | 150 | } else if (this.srcJar != null) { |
| | 151 | return this.srcJar.getName().split("\\.", 2)[0]; |
| | 152 | } |
| | 153 | return Integer.toHexString(this.hashCode()); |
| | 154 | } |
| | 155 | |
| | 156 | public String getRemotePluginsListSection(Integer port) { |
| | 157 | return String.format( |
| | 158 | "%s.jar;%s\n%s", |
| | 159 | this.getName(), |
| | 160 | this.getPluginURL(port), |
| | 161 | this.getRemotePluginsListManifestSection() |
| | 162 | ); |
| | 163 | } |
| | 164 | |
| | 165 | public MappingBuilder getMappingBuilder() { |
| | 166 | final String jarPathBeneathFilesDir = this.getJarPathBeneathFilesDir(); |
| | 167 | |
| | 168 | if (jarPathBeneathFilesDir != null) { |
| | 169 | return WireMock.get(WireMock.urlMatching(this.getPluginURLPath())); |
| | 170 | } |
| | 171 | return null; |
| | 172 | } |
| | 173 | |
| | 174 | public ResponseDefinitionBuilder getResponseDefinitionBuilder() { |
| | 175 | final String jarPathBeneathFilesDir = this.getJarPathBeneathFilesDir(); |
| | 176 | |
| | 177 | if (jarPathBeneathFilesDir != null) { |
| | 178 | return WireMock.aResponse().withStatus(200).withHeader("Content-Type", "application/java-archive").withBodyFile( |
| | 179 | jarPathBeneathFilesDir |
| | 180 | ); |
| | 181 | } |
| | 182 | return null; |
| | 183 | } |
| | 184 | } |
| | 185 | |
| | 186 | protected final List<RemotePlugin> pluginList; |
| | 187 | |
| | 188 | public PluginServer(RemotePlugin... remotePlugins) { |
| | 189 | this.pluginList = ImmutableList.copyOf(remotePlugins); |
| | 190 | } |
| | 191 | |
| | 192 | public void applyToWireMockServer(WireMockServer wireMockServer) { |
| | 193 | // first add the plugins list |
| | 194 | wireMockServer.stubFor( |
| | 195 | WireMock.get(WireMock.urlEqualTo("/plugins")).willReturn( |
| | 196 | WireMock.aResponse().withStatus(200).withHeader("Content-Type", "text/plain").withBody( |
| | 197 | this.pluginList.stream().map( |
| | 198 | remotePlugin -> remotePlugin.getRemotePluginsListSection(wireMockServer.port()) |
| | 199 | ).collect(Collectors.joining()) |
| | 200 | ) |
| | 201 | ) |
| | 202 | ); |
| | 203 | |
| | 204 | // now add each file that we're able to serve |
| | 205 | for (final RemotePlugin remotePlugin : this.pluginList) { |
| | 206 | final MappingBuilder mappingBuilder = remotePlugin.getMappingBuilder(); |
| | 207 | final ResponseDefinitionBuilder responseDefinitionBuilder = remotePlugin.getResponseDefinitionBuilder(); |
| | 208 | |
| | 209 | if (mappingBuilder != null && responseDefinitionBuilder != null) { |
| | 210 | wireMockServer.stubFor( |
| | 211 | remotePlugin.getMappingBuilder().willReturn(remotePlugin.getResponseDefinitionBuilder()) |
| | 212 | ); |
| | 213 | } |
| | 214 | } |
| | 215 | } |
| | 216 | |
| | 217 | public PluginServerRule asWireMockRule() { |
| | 218 | return this.asWireMockRule( |
| | 219 | options().dynamicPort().usingFilesUnderDirectory(TestUtils.getTestDataRoot()), |
| | 220 | true |
| | 221 | ); |
| | 222 | } |
| | 223 | |
| | 224 | public PluginServerRule asWireMockRule(Options ruleOptions, boolean failOnUnmatchedRequests) { |
| | 225 | return new PluginServerRule(ruleOptions, failOnUnmatchedRequests); |
| | 226 | } |
| | 227 | |
| | 228 | public class PluginServerRule extends WireMockRule { |
| | 229 | public PluginServerRule(Options ruleOptions, boolean failOnUnmatchedRequests) { |
| | 230 | super(ruleOptions, failOnUnmatchedRequests); |
| | 231 | } |
| | 232 | |
| | 233 | public PluginServer getPluginServer() { |
| | 234 | return PluginServer.this; |
| | 235 | } |
| | 236 | |
| | 237 | @Override |
| | 238 | public Statement apply(Statement base, Description description) { |
| | 239 | return super.apply(new Statement() { |
| | 240 | @Override |
| | 241 | @SuppressWarnings("unchecked") |
| | 242 | public void evaluate() throws Throwable { |
| | 243 | PluginServer.this.applyToWireMockServer(PluginServerRule.this); |
| | 244 | base.evaluate(); |
| | 245 | } |
| | 246 | }, description); |
| | 247 | } |
| | 248 | } |
| | 249 | } |