Skip to content

Commit

Permalink
Provide wrappers for Jakarta and Java EE
Browse files Browse the repository at this point in the history
  • Loading branch information
dheid committed Nov 13, 2023
1 parent 0841d05 commit 5b2447a
Show file tree
Hide file tree
Showing 27 changed files with 1,027 additions and 82 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,8 @@ In a Servlet environment, it might be easier to use the `ServletMatomoRequest` c
import jakarta.servlet.http.HttpServletRequest;
import org.matomo.java.tracking.MatomoRequest;
import org.matomo.java.tracking.MatomoTracker;
import org.matomo.java.tracking.parameters.DeviceResolution;
import org.matomo.java.tracking.ServletMatomoRequest;
import org.matomo.java.tracking.servlet.JakartaHttpServletWrapper;
import org.matomo.java.tracking.servlet.ServletMatomoRequest;
public class ServletMatomoRequestExample {
Expand All @@ -353,10 +353,10 @@ public class ServletMatomoRequestExample {
public ServletMatomoRequestExample(MatomoTracker tracker) {
this.tracker = tracker;
}
public void someControllerMethod(HttpServletRequest req) {
MatomoRequest matomoRequest = ServletMatomoRequest
.fromServletRequest(req)
.fromServletRequest(JakartaHttpServletWrapper.fromHttpServletRequest(req))
.actionName("Some Controller Action")
// ...
.build();
Expand All @@ -369,7 +369,8 @@ public class ServletMatomoRequestExample {

The `ServletMatomoRequest` automatically sets the action URL, applies browser request headers, corresponding Matomo
cookies and the visitor IP address. It sets the visitor ID, Matomo session ID, custom variables and heatmap
if Matomo cookies are present.
if Matomo cookies are present. Since there was a renaming from Java EE (javax) to Jakarta EE (jakarta), we provide a
wrapper class `JakartaHttpServletWrapper` for Jakarta and `JavaxHttpServletWrapper` for javax.

### Tracking Configuration

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.matomo.java.tracking.servlet;

import lombok.Value;

/**
* Wrapper for the cookie name and value.
*/
@Value
public class CookieWrapper {

String name;

String value;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package org.matomo.java.tracking.servlet;

import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;

/**
* Wraps a HttpServletRequest to be compatible with both the Jakarta and the Java EE API.
*/
@Builder
@Value
public class HttpServletRequestWrapper {

StringBuffer requestURL;

String remoteAddr;

Map<String, String> headers;

CookieWrapper[] cookies;

/**
* Returns an enumeration of all the header names this request contains. If the request has no
* headers, this method returns an empty enumeration.
*
* @return an enumeration of all the header names sent with this request
*/
public Enumeration<String> getHeaderNames() {
return headers == null ? Collections.emptyEnumeration() :
Collections.enumeration(headers.keySet());
}

/**
* Returns the value of the specified request header as a String. If the request did not include a
* header of the specified name, this method returns null. If there are multiple headers with the
* same name, this method returns the last header in the request. The header name is case
* insensitive. You can use this method with any request header.
*
* @param name a String specifying the header name (case insensitive) - must not be {@code null}.
* @return a String containing the value of the requested header, or null if the request does not
* have a header of that name
*/
@Nullable
public String getHeader(@NonNull String name) {
return headers == null ? null : headers.get(name.toLowerCase(Locale.ROOT));
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package org.matomo.java.tracking;
package org.matomo.java.tracking.servlet;

import static java.util.Arrays.asList;

import edu.umd.cs.findbugs.annotations.Nullable;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
Expand All @@ -14,16 +12,17 @@
import java.util.Map;
import java.util.Set;
import lombok.NonNull;
import org.matomo.java.tracking.MatomoRequest;
import org.matomo.java.tracking.parameters.CustomVariables;
import org.matomo.java.tracking.parameters.VisitorId;


/**
* Adds the headers from a {@link HttpServletRequest} to a
* Adds the headers from a {@link HttpServletRequestWrapper} to a
* {@link MatomoRequest.MatomoRequestBuilder}.
*
* <p>Use #fromServletRequest(HttpServletRequest) to create a new builder with the headers from the
* request or #addServletRequestHeaders(MatomoRequest.MatomoRequestBuilder, HttpServletRequest) to
* <p>Use #fromServletRequest(HttpServletRequestWrapper) to create a new builder with the headers from the
* request or #addServletRequestHeaders(MatomoRequest.MatomoRequestBuilder, HttpServletRequestWrapper) to
* add the headers to an existing builder.
*/
public final class ServletMatomoRequest {
Expand All @@ -47,22 +46,22 @@ private ServletMatomoRequest() {
/**
* Creates a new builder with the headers from the request.
*
* <p>Use #addServletRequestHeaders(MatomoRequest.MatomoRequestBuilder, HttpServletRequest) to
* <p>Use #addServletRequestHeaders(MatomoRequest.MatomoRequestBuilder, HttpServletRequestWrapper) to
* add the headers to an existing builder.
*
* @param request the request to get the headers from (must not be null)
*
* @return a new builder with the headers from the request (never null)
*/
@edu.umd.cs.findbugs.annotations.NonNull
public static MatomoRequest.MatomoRequestBuilder fromServletRequest(@NonNull HttpServletRequest request) {
public static MatomoRequest.MatomoRequestBuilder fromServletRequest(@NonNull HttpServletRequestWrapper request) {
return addServletRequestHeaders(MatomoRequest.request(), request);
}

/**
* Adds the headers from the request to an existing builder.
*
* <p>Use #fromServletRequest(HttpServletRequest) to create a new builder with the headers from
* <p>Use #fromServletRequest(HttpServletRequestWrapper) to create a new builder with the headers from
* the request.
*
* @param builder the builder to add the headers to (must not be null)
Expand All @@ -72,7 +71,7 @@ public static MatomoRequest.MatomoRequestBuilder fromServletRequest(@NonNull Htt
*/
@edu.umd.cs.findbugs.annotations.NonNull
public static MatomoRequest.MatomoRequestBuilder addServletRequestHeaders(
@NonNull MatomoRequest.MatomoRequestBuilder builder, @NonNull HttpServletRequest request
@NonNull MatomoRequest.MatomoRequestBuilder builder, @NonNull HttpServletRequestWrapper request
) {
return builder
.actionUrl(request.getRequestURL() == null ? null : request.getRequestURL().toString())
Expand All @@ -83,7 +82,7 @@ public static MatomoRequest.MatomoRequestBuilder addServletRequestHeaders(

@edu.umd.cs.findbugs.annotations.NonNull
private static Map<String, String> collectHeaders(
@edu.umd.cs.findbugs.annotations.NonNull HttpServletRequest request
@edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request
) {
Map<String, String> headers = new HashMap<>(10);
Enumeration<String> headerNames = request.getHeaderNames();
Expand All @@ -100,7 +99,7 @@ private static Map<String, String> collectHeaders(

@Nullable
private static String determineVisitorIp(
@edu.umd.cs.findbugs.annotations.NonNull HttpServletRequest request
@edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request
) {
String realIpHeader = request.getHeader("X-Real-Ip");
if (isNotEmpty(realIpHeader)) {
Expand All @@ -119,12 +118,12 @@ private static String determineVisitorIp(
@edu.umd.cs.findbugs.annotations.NonNull
private static Map<String, String> processCookies(
@edu.umd.cs.findbugs.annotations.NonNull MatomoRequest.MatomoRequestBuilder builder,
@edu.umd.cs.findbugs.annotations.NonNull HttpServletRequest request
@edu.umd.cs.findbugs.annotations.NonNull HttpServletRequestWrapper request
) {
Map<String, String> cookies = new LinkedHashMap<>(3);
if (request.getCookies() != null) {
builder.supportsCookies(Boolean.TRUE);
for (Cookie cookie : request.getCookies()) {
for (CookieWrapper cookie : request.getCookies()) {
if (isNotEmpty(cookie.getValue())) {
processCookie(builder, cookies, cookie.getName(), cookie.getValue());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
package org.matomo.java.tracking;
package org.matomo.java.tracking.servlet;

import static java.util.Collections.singleton;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.matomo.java.tracking.MatomoRequest;

class ServletMatomoRequestTest {

@Test
void addsServletRequestHeaders() {

MockHttpServletRequest request = new MockHttpServletRequest();
request.setHeaders(singletonMap("headerName", "headerValue"));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.headers(singletonMap("headername", "headerValue"))
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

MatomoRequest matomoRequest = builder.build();
assertThat(matomoRequest.getHeaders()).hasSize(1).containsEntry("headerName", "headerValue");
assertThat(matomoRequest.getHeaders()).hasSize(1).containsEntry("headername", "headerValue");
}

@Test
void skipsEmptyHeaderNames() {

MockHttpServletRequest request = new MockHttpServletRequest();
request.setHeaders(singletonMap("", "headerValue"));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.headers(singletonMap("", "headerValue"))
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

Expand All @@ -40,8 +43,10 @@ void skipsEmptyHeaderNames() {
@Test
void skipsBlankHeaderNames() {

MockHttpServletRequest request = new MockHttpServletRequest();
request.setHeaders(singletonMap(" ", "headerValue"));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.headers(singletonMap(" ", "headerValue"))
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

Expand All @@ -53,8 +58,10 @@ void skipsBlankHeaderNames() {
@ParameterizedTest
@ValueSource(strings = {"connection", "content-length", "expect", "host", "upgrade"})
void doesNotAddRestrictedHeaders(String restrictedHeader) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setHeaders(singletonMap(restrictedHeader, "headerValue"));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.headers(singletonMap(restrictedHeader, "headerValue"))
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

Expand All @@ -73,16 +80,20 @@ void failsIfServletRequestIsNull() {
void failsIfBuilderIsNull() {
assertThatThrownBy(() -> ServletMatomoRequest.addServletRequestHeaders(
null,
new MockHttpServletRequest()
HttpServletRequestWrapper.builder().build()
))
.isInstanceOf(NullPointerException.class)
.hasMessage("builder is marked non-null but is null");
}

@Test
void extractsVisitorIdFromCookie() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(singleton(new Cookie("_pk_id.1.1fff", "be40d677d6c7270b.1699801331.")));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.cookies(new CookieWrapper[] {
new CookieWrapper("_pk_id.1.1fff", "be40d677d6c7270b.1699801331.")
})
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

Expand All @@ -98,8 +109,10 @@ void extractsVisitorIdFromCookie() {
strings = {"_pk_ses.1.1fff", "_pk_ref.1.1fff", "_pk_hsr.1.1fff"}
)
void extractsMatomoCookies(String cookieName) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(singleton(new Cookie(cookieName, "anything")));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.cookies(new CookieWrapper[] {new CookieWrapper(cookieName, "anything")})
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

Expand All @@ -109,8 +122,15 @@ void extractsMatomoCookies(String cookieName) {

@Test
void extractsSessionIdFromMatomoSessIdCookie() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(singleton(new Cookie("MATOMO_SESSID", "2cbf8b5ba00fbf9ba70853308cd0944a")));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.cookies(new CookieWrapper[] {
new CookieWrapper(
"MATOMO_SESSID",
"2cbf8b5ba00fbf9ba70853308cd0944a"
)
})
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

Expand All @@ -120,11 +140,15 @@ void extractsSessionIdFromMatomoSessIdCookie() {

@Test
void parsesVisitCustomVariablesFromCookie() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(singleton(new Cookie(
"_pk_cvar.1.1fff",
"{\"1\":[\"VAR 1 set, var 2 not set\",\"yes\"],\"3\":[\"var 3 set\",\"yes!!!!\"]}"
)));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.cookies(new CookieWrapper[] {
new CookieWrapper(
"_pk_cvar.1.1fff",
"{\"1\":[\"VAR 1 set, var 2 not set\",\"yes\"],\"3\":[\"var 3 set\",\"yes!!!!\"]}"
)
})
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

Expand All @@ -139,8 +163,23 @@ void parsesVisitCustomVariablesFromCookie() {

@Test
void determinerVisitorIpFromXForwardedForHeader() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setHeaders(singletonMap("X-Forwarded-For", "44.55.66.77"));
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.headers(singletonMap("x-forwarded-for", "44.55.66.77"))
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

MatomoRequest matomoRequest = builder.build();
assertThat(matomoRequest.getVisitorIp()).isEqualTo("44.55.66.77");
}

@Test
void determinerVisitorIpFromXRealIpHeader() {
HttpServletRequestWrapper request = HttpServletRequestWrapper
.builder()
.headers(singletonMap("x-real-ip", "44.55.66.77"))
.build();

MatomoRequest.MatomoRequestBuilder builder = ServletMatomoRequest.fromServletRequest(request);

Expand Down
Loading

0 comments on commit 5b2447a

Please sign in to comment.