diff --git a/README.md b/README.md index b81ad182..24cc15fa 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ Most dependencies are defined on a per-subproject level, only the versions for t ./gradlew test -Pjackson.version=2.9.0 ``` -The [integration tests](.github/workflows/ci.yaml) include running the build with our supported baseline dependency as well as the latest micro release of Jackson, Apache HttpClient and Spring. Please update the section in the README when bumping dependency baselines, and add this to the release notes. +The [integration tests](.github/workflows/ci.yml) include running the build with our supported baseline dependency as well as the latest micro release of Jackson, Apache HttpClient and Spring. Please update the section in the README when bumping dependency baselines, and add this to the release notes. ### End-to-End tests @@ -353,7 +353,7 @@ open build/reports/dependency-check-report.html ### Releasing -Fahrschein uses Github Workflows to [build](.github/workflows/ci.yaml) and [publish releases](.github/workflows/maven-publish.yaml). This happens automatically whenever a new release is created in Github. After creating a release, please bump the `project.version` property in [gradle.properties](./gradle.properties). +Fahrschein uses Github Workflows to [build](.github/workflows/ci.yml) and [publish releases](.github/workflows/maven-publish.yml). This happens automatically whenever a new release is created in Github. After creating a release, please bump the `project.version` property in [gradle.properties](./gradle.properties). If needed, you can preview the signed release artifacts in your local maven repository. diff --git a/fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/Headers.java b/fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/Headers.java index 173df5b7..82f8ec1a 100644 --- a/fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/Headers.java +++ b/fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/Headers.java @@ -11,6 +11,7 @@ public interface Headers { String CONTENT_TYPE = "Content-Type"; String CONTENT_ENCODING = "Content-Encoding"; String ACCEPT_ENCODING = "Accept-Encoding"; + String USER_AGENT = "User-Agent"; List get(String headerName); diff --git a/fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/UserAgent.java b/fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/UserAgent.java new file mode 100644 index 00000000..3cd74a59 --- /dev/null +++ b/fahrschein-http-api/src/main/java/org/zalando/fahrschein/http/api/UserAgent.java @@ -0,0 +1,57 @@ +package org.zalando.fahrschein.http.api; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public final class UserAgent { + private static final String PROPERTIES_FILE = "fahrschein.properties"; + private static final Logger logger = LoggerFactory.getLogger(UserAgent.class); + + private static final Properties fahrscheinProperties = new Properties(); + + static { + try (final InputStream stream = + UserAgent.class.getResourceAsStream("/" + PROPERTIES_FILE)) { + if (stream != null) { + fahrscheinProperties.load(stream); + } else { + logger.warn("Properties file not found: {}", PROPERTIES_FILE); + } + } catch (IOException e) { + logger.warn("Cannot read file: " + PROPERTIES_FILE, e); + } + } + + private static final String AGENT_STR_TEMPLATE = "Fahrschein/%s (%s; Java%d)"; + + private final String userAgent; + + public UserAgent(Class implementation) { + this.userAgent = String.format(AGENT_STR_TEMPLATE, fahrscheinVersion(), implementation.getSimpleName().replace("RequestFactory", ""), javaVersion()); + } + + public String userAgent() { + return userAgent; + } + + static String fahrscheinVersion() { + return String.valueOf(fahrscheinProperties.get("fahrschein-version")); + } + + static int javaVersion() { + String version = System.getProperty("java.version"); + if (version.startsWith("1.")) { + version = version.substring(2, 3); + } else { + int dot = version.indexOf("."); + if (dot != -1) { + version = version.substring(0, dot); + } + } + return Integer.parseInt(version); + } +} diff --git a/fahrschein-http-api/src/test/java/org/zalando/fahrschein/http/api/UserAgentTest.java b/fahrschein-http-api/src/test/java/org/zalando/fahrschein/http/api/UserAgentTest.java new file mode 100644 index 00000000..e1eaa906 --- /dev/null +++ b/fahrschein-http-api/src/test/java/org/zalando/fahrschein/http/api/UserAgentTest.java @@ -0,0 +1,30 @@ +package org.zalando.fahrschein.http.api; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class UserAgentTest { + + abstract class DummyRequestFactory implements RequestFactory {} + + @Test + public void javaVersion() { + String version = System.getProperty("java.version"); + int javaVersion = UserAgent.javaVersion(); + if (version.startsWith("1.8")) { + assertEquals(8, javaVersion, "Java 8"); + } else { + assertTrue(version.startsWith(String.valueOf(javaVersion))); + assertTrue(javaVersion > 8, "Java " + javaVersion); + } + } + + @Test + public void userAgentString() { + UserAgent ua = new UserAgent(DummyRequestFactory.class); + String userAgent = ua.userAgent(); + assertEquals("Fahrschein/0.1.0-SNAPSHOT (Dummy; Java" + UserAgent.javaVersion() + ")", userAgent); + } + +} diff --git a/fahrschein-http-api/src/test/resources/fahrschein.properties b/fahrschein-http-api/src/test/resources/fahrschein.properties new file mode 100644 index 00000000..854ce50d --- /dev/null +++ b/fahrschein-http-api/src/test/resources/fahrschein.properties @@ -0,0 +1,2 @@ +# for testing +fahrschein-version=0.1.0-SNAPSHOT \ No newline at end of file diff --git a/fahrschein-http-jdk11/src/main/java/org/zalando/fahrschein/http/jdk11/JavaNetBufferingRequest.java b/fahrschein-http-jdk11/src/main/java/org/zalando/fahrschein/http/jdk11/JavaNetBufferingRequest.java index c5f12164..dafd1ed0 100644 --- a/fahrschein-http-jdk11/src/main/java/org/zalando/fahrschein/http/jdk11/JavaNetBufferingRequest.java +++ b/fahrschein-http-jdk11/src/main/java/org/zalando/fahrschein/http/jdk11/JavaNetBufferingRequest.java @@ -68,7 +68,7 @@ public void add(String headerName, String value) { @Override public void put(String headerName, String value) { - request.header(headerName, value); + request.setHeader(headerName, value); } @Override diff --git a/fahrschein-http-test-support/src/testFixtures/java/org/zalando/fahrschein/http/test/AbstractRequestFactoryTest.java b/fahrschein-http-test-support/src/testFixtures/java/org/zalando/fahrschein/http/test/AbstractRequestFactoryTest.java index 707f50cf..0db81859 100644 --- a/fahrschein-http-test-support/src/testFixtures/java/org/zalando/fahrschein/http/test/AbstractRequestFactoryTest.java +++ b/fahrschein-http-test-support/src/testFixtures/java/org/zalando/fahrschein/http/test/AbstractRequestFactoryTest.java @@ -55,6 +55,26 @@ public static void startServer() throws IOException { @Captor public ArgumentCaptor exchangeCaptor; + @Test + public void testUserAgent() throws IOException { + // given + String expectedResponse = "{}"; + HttpHandler spy = Mockito.spy(new SimpleRequestResponseContentHandler(expectedResponse)); + server.createContext("/user-agent", spy); + + // when + final RequestFactory f = defaultRequestFactory(ContentEncoding.IDENTITY); + Request r = f.createRequest(serverAddress.resolve("/user-agent"), "GET"); + r.getHeaders().put("User-Agent", "Test"); + Response executed = r.execute(); + readStream(executed.getBody()); + + // then + Mockito.verify(spy).handle(exchangeCaptor.capture()); + HttpExchange capturedArgument = exchangeCaptor.getValue(); + assertThat("UserAgent header", capturedArgument.getRequestHeaders().get("user-agent"), equalTo(Arrays.asList("Test"))); + } + @Test public void testGzippedResponseBody() throws IOException { // given diff --git a/fahrschein/build.gradle b/fahrschein/build.gradle index 154c55bc..70b6d5b0 100644 --- a/fahrschein/build.gradle +++ b/fahrschein/build.gradle @@ -19,3 +19,37 @@ dependencies { } publishing.publications.maven.pom.description = 'A Java client for the Nakadi event bus' + +def generateResourcesTask = tasks.register("generate-resources", GenerateResourcesTask) { + resourcesDir.set(layout.buildDirectory.dir("generated-resources/main")) +} + +sourceSets { + main { + output.dir(generateResourcesTask) + } +} + +idea { + module { + // Marks the already(!) added srcDir as "generated" + generatedSourceDirs += file('build/generated-resources/main') + } +} + +abstract class GenerateResourcesTask extends DefaultTask { + + @Input + String getVersion() { + return project.version + } + + @OutputDirectory + abstract DirectoryProperty getResourcesDir() + + @TaskAction + def generateResources() { + def generated = resourcesDir.file("fahrschein.properties").get().asFile + generated.text = "fahrschein-version=" + getVersion() + } +} \ No newline at end of file diff --git a/fahrschein/src/main/java/org/zalando/fahrschein/NakadiClientBuilder.java b/fahrschein/src/main/java/org/zalando/fahrschein/NakadiClientBuilder.java index 50526049..56465f08 100644 --- a/fahrschein/src/main/java/org/zalando/fahrschein/NakadiClientBuilder.java +++ b/fahrschein/src/main/java/org/zalando/fahrschein/NakadiClientBuilder.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.zalando.fahrschein.http.api.RequestFactory; + import javax.annotation.Nullable; import java.net.URI; @@ -48,7 +49,7 @@ public NakadiClientBuilder withCursorManager(CursorManager cursorManager) { } static RequestFactory wrapClientHttpRequestFactory(RequestFactory delegate, @Nullable AuthorizationProvider authorizationProvider) { - RequestFactory requestFactory = new ProblemHandlingRequestFactory(delegate); + RequestFactory requestFactory = new ProblemHandlingRequestFactory(new UserAgentRequestFactory(delegate)); if (authorizationProvider != null) { requestFactory = new AuthorizedRequestFactory(requestFactory, authorizationProvider); } diff --git a/fahrschein/src/main/java/org/zalando/fahrschein/UserAgentRequestFactory.java b/fahrschein/src/main/java/org/zalando/fahrschein/UserAgentRequestFactory.java new file mode 100644 index 00000000..45f96360 --- /dev/null +++ b/fahrschein/src/main/java/org/zalando/fahrschein/UserAgentRequestFactory.java @@ -0,0 +1,26 @@ +package org.zalando.fahrschein; + +import org.zalando.fahrschein.http.api.Headers; +import org.zalando.fahrschein.http.api.Request; +import org.zalando.fahrschein.http.api.RequestFactory; +import org.zalando.fahrschein.http.api.UserAgent; + +import java.io.IOException; +import java.net.URI; + +public class UserAgentRequestFactory implements RequestFactory { + private final RequestFactory delegate; + private final String userAgent; + + UserAgentRequestFactory(final RequestFactory delegate) { + this.userAgent = new UserAgent(delegate.getClass()).userAgent(); + this.delegate = delegate; + } + + @Override + public Request createRequest(URI uri, String method) throws IOException { + final Request request = delegate.createRequest(uri, method); + request.getHeaders().put(Headers.USER_AGENT, userAgent); + return request; + } +} diff --git a/fahrschein/src/test/java/org/zalando/fahrschein/NakadiClientTest.java b/fahrschein/src/test/java/org/zalando/fahrschein/NakadiClientTest.java index 893c909c..81d2d3c4 100644 --- a/fahrschein/src/test/java/org/zalando/fahrschein/NakadiClientTest.java +++ b/fahrschein/src/test/java/org/zalando/fahrschein/NakadiClientTest.java @@ -1,5 +1,6 @@ package org.zalando.fahrschein; +import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.zalando.fahrschein.domain.Partition; @@ -56,6 +57,17 @@ public void setup() { this.client = nakadiClient; } + @Test + public void shouldAddUserAgent() throws IOException { + server.expectRequestTo("http://example.com/event-types/foobar/partitions", "GET") + .andExpectHeader("User-Agent", Matchers.startsWith("Fahrschein/")) + .andRespondWith(200, ContentType.APPLICATION_JSON, "[]") + .setup(); + + client.getPartitions("foobar"); + server.verify(); + } + @Test public void shouldGetPartitions() throws IOException { server.expectRequestTo("http://example.com/event-types/foobar/partitions", "GET") diff --git a/fahrschein/src/test/java/org/zalando/fahrschein/UserAgentRequestFactoryTest.java b/fahrschein/src/test/java/org/zalando/fahrschein/UserAgentRequestFactoryTest.java new file mode 100644 index 00000000..4937a954 --- /dev/null +++ b/fahrschein/src/test/java/org/zalando/fahrschein/UserAgentRequestFactoryTest.java @@ -0,0 +1,59 @@ +package org.zalando.fahrschein; + +import org.junit.jupiter.api.Test; +import org.zalando.fahrschein.http.api.Headers; +import org.zalando.fahrschein.http.api.HeadersImpl; +import org.zalando.fahrschein.http.api.Request; +import org.zalando.fahrschein.http.api.RequestFactory; +import org.zalando.fahrschein.http.api.Response; +import org.zalando.fahrschein.http.api.UserAgent; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UserAgentRequestFactoryTest { + static class DummyRequestFactory implements RequestFactory { + + @Override + public Request createRequest(URI uri, String method) throws IOException { + return new Request() { + private final Headers headers = new HeadersImpl(); + @Override + public String getMethod() { + return method; + } + + @Override + public URI getURI() { + return uri; + } + + @Override + public Headers getHeaders() { + return headers; + } + + @Override + public OutputStream getBody() throws IOException { + return null; + } + + @Override + public Response execute() throws IOException { + return null; + } + }; + } + } + + @Test + public void appendUserAgentToRequest() throws IOException { + UserAgent ua = new UserAgent(DummyRequestFactory.class); + UserAgentRequestFactory rf = new UserAgentRequestFactory(new DummyRequestFactory()); + Request r = rf.createRequest(URI.create("dummy://req"), "POST"); + assertEquals(ua.userAgent(), r.getHeaders().getFirst("User-Agent")); + } +}