diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 502d4acc..7abf6b97 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,11 +9,19 @@ on: jobs: build: runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 - with: - java-version: '8' - distribution: 'temurin' - cache: maven - - run: mvn -B verify jacoco:report + - uses: actions/checkout@v4 + - uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'temurin' + cache: maven + - run: mvn -B verify + - uses: madrapps/jacoco-report@v1.6.1 + with: + paths: ${{ github.workspace }}/target/site/jacoco/jacoco.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 80 + min-coverage-changed-files: 80 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4991ec03..45a7fb5d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -23,13 +23,13 @@ jobs: language: [ 'java' ] steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + - name: Checkout repository + uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9902e44b..e1bd90b8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,32 +5,32 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 - with: - java-version: '8' - distribution: 'temurin' - cache: maven - server-id: ossrh - server-username: OSSRH_USERNAME - server-password: OSSRH_PASSWORD - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg-passphrase: GPG_PASSPHRASE - - run: | - git config user.email "mail@daniel-heid.de" - git config user.name "Daniel Heid" - - id: version - run: | - VERSION=$( mvn -B help:evaluate -Dexpression=project.version -q -DforceStdout ) - echo "::set-output name=version::${VERSION#-SNAPSHOT}" - - run: mvn -B release:prepare release:perform - env: - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - - uses: release-drafter/release-drafter@v5 - with: - version: ${{ steps.version.outputs.version }} - publish: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v4 + - uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'temurin' + cache: maven + server-id: ossrh + server-username: OSSRH_USERNAME + server-password: OSSRH_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + - run: | + git config user.email "matomo-java-tracker@daniel-heid.de" + git config user.name "Matomo Java Tracker" + - id: version + run: | + VERSION=$( mvn -B help:evaluate -Dexpression=project.version -q -DforceStdout ) + echo "::set-output name=version::${VERSION#-SNAPSHOT}" + - run: mvn -B release:prepare release:perform + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + - uses: release-drafter/release-drafter@v5 + with: + version: ${{ steps.version.outputs.version }} + publish: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 298acc40..494c2390 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an +standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d90529cd..1c208038 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,4 +89,5 @@ This Code of Conduct is adapted from the [Contributor Covenant][homepage], versi available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org + [version]: http://contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index 0782d6b8..232366e1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Official Java implementation of the [Matomo Tracking HTTP API](https://developer ## Javadoc -The Javadoc for this project is hosted as a Github page for this repo. The latest Javadoc can be +The Javadoc for this project is hosted as a GitHub page for this repo. The latest Javadoc can be found [here](https://matomo-org.github.io/matomo-java-tracker/javadoc/HEAD/index.html). Javadoc for the latest and all releases can be found [here](https://matomo-org.github.io/matomo-java-tracker/javadoc/index.html). @@ -61,13 +61,13 @@ public class YourImplementation { Per default every request has the following default parameters: -| Parameter Name | Default Value | -|------------------|--------------------------------| -| required | true | -| visitorId | random 16 character hex string | -| randomValue | random 20 character hex string | -| apiVersion | 1 | -| responseAsImage | false | +| Parameter Name | Default Value | +|-----------------|--------------------------------| +| required | true | +| visitorId | random 16 character hex string | +| randomValue | random 20 character hex string | +| apiVersion | 1 | +| responseAsImage | false | Overwrite these properties as desired. @@ -86,7 +86,8 @@ public class YourImplementation { MatomoRequest request = MatomoRequest.builder() .siteId(42) - .actionUrl("http://example.org/landing.html?pk_campaign=Email-Nov2011&pk_kwd=LearnMore") // include the query parameters to the url + .actionUrl( + "http://example.org/landing.html?pk_campaign=Email-Nov2011&pk_kwd=LearnMore") // include the query parameters to the url .actionName("LearnMore") .build(); } @@ -153,7 +154,8 @@ public class YourImplementation { public void yourMethod() { - MatomoRequest request = MatomoRequest.builder().siteId(42).actionUrl("https://www.mydomain.com/some/page").actionName("Signup").build(); + MatomoRequest request = + MatomoRequest.builder().siteId(42).actionUrl("https://www.mydomain.com/some/page").actionName("Signup").build(); MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php"); try { @@ -183,7 +185,6 @@ package example; import org.apache.http.HttpResponse; import org.matomo.java.tracking.MatomoRequest; -import org.matomo.java.tracking.MatomoRequestBuilder; import org.matomo.java.tracking.MatomoTracker; import java.io.IOException; @@ -232,7 +233,6 @@ package example; import org.apache.http.HttpResponse; import org.matomo.java.tracking.MatomoLocale; import org.matomo.java.tracking.MatomoRequest; -import org.matomo.java.tracking.MatomoRequestBuilder; import org.matomo.java.tracking.MatomoTracker; import java.io.IOException; @@ -250,11 +250,15 @@ public class YourImplementation { Collection requests = new ArrayList<>(); MatomoRequestBuilder builder = MatomoRequest.builder().siteId(42); requests.add(builder.actionUrl("https://www.mydomain.com/some/page").actionName("Some Page").build()); - requests.add(builder.actionUrl("https://www.mydomain.com/another/page").actionName("Another Page").visitorCountry(new MatomoLocale(Locale.GERMANY)).build()); + requests.add(builder.actionUrl("https://www.mydomain.com/another/page").actionName("Another Page") + .visitorCountry(new MatomoLocale(Locale.GERMANY)).build()); MatomoTracker tracker = new MatomoTracker("https://your-matomo-domain.tld/matomo.php"); try { - Future response = tracker.sendBulkRequestAsync(requests, "33dc3f2536d3025974cccb4b4d2d98f4"); // second parameter is authentication token need for country override + Future response = tracker.sendBulkRequestAsync( + requests, + "33dc3f2536d3025974cccb4b4d2d98f4" + ); // second parameter is authentication token need for country override // usually not needed: HttpResponse httpResponse = response.get(); int statusCode = httpResponse.getStatusLine().getStatusCode(); @@ -274,6 +278,80 @@ public class YourImplementation { ``` +## Migration from Version 2 to 3 + +We improved this library by adding the dimension parameter and removing outdated parameters in Matomo version 5, +removing some dependencies (that even contained vulnerabilities) and increasing maintainability. Sadly this includes the +following breaking changes: + +### Removals + +* The parameter `actionTime` (`gt_ms`) is no longer supported by Matomo 5 and was removed. +* Many methods marked as deprecated in version 2 were removed. Please see the + former [Javadoc](https://matomo-org.github.io/matomo-java-tracker/javadoc/HEAD/index.html) of version 2 to get the + deprecated methods. +* We removed the vulnerable dependency to the Apache HTTP client. Callbacks are no longer of type `FutureCallback`, but + `Consumer` instead. +* The `send...` methods of `MatomoTracker` no longer return a value (usually Matomo always returns an HTTP 204 response + without a body). If the request fails, an exception will be thrown. +* Since there are several ways on how to set the auth token, `verifyAuthTokenSet` was removed. Just check yourself, + whether your auth token is null. However, the tracker checks, whether an auth token is either set by parameter, by + request or per configuration. +* Due to a major refactoring on how the queries are created, we no longer use a large map instead of concrete attributes + to collect the Matomo parameters. Therefore `getParameters()` of class `MatomoRequest` no longer exists. Please use + getters and setters instead. +* The methods `verifyEcommerceEnabled()` and `verifyEcommerceState()` were removed from `MatomoRequest`. The request + will be validated prior to sending and not during construction. +* `getRandomHexString` was removed. Use `RandomValue.random()` or `VisitorId.random()` instead. + +## Type Changes and Renaming + +* `requestDatetime`, `visitorPreviousVisitTimestamp`, `visitorFirstVisitTimestamp`, `ecommerceLastOrderTimestamp` are + now of type `Instant`. You can use `Instant.ofEpochSecond()` to create + them from epoch seconds. +* `requestDatetime` was renamed to `requestTimestamp` due to setter collision and downwards compatibility +* `goalRevenue` is the same parameter as `ecommerceRevenue` and was removed to prevent duplication. + Use `ecommerceRevenue` instead. +* `setEventValue` requires a double parameter now +* `setEcommerceLastOrderTimestamp` requires an `Instant` parameter now +* `headerAcceptLanguage` is now of type `AcceptLanguage`. You can build it easily + using `AcceptLanguage.fromHeader("de")` +* `visitorCountry` is now of type `Country`. You can build it easily using `AcceptLanguage.fromCode("fr")` +* `deviceResolution` is now of type `DeviceResolution`. You can build it easily + using `DeviceResolution.builder.width(...).height(...).build()`. To easy the migration, we added a constructor + method `DeviceResolution.fromString()` that accepts inputs of kind _width_x_height_, e.g. `100x200` +* `pageViewId` is now of type `UniqueId`. You can build it easily using `UniqueId.random()` +* `randomValue` is now of type `RandomValue`. You can build it easily using `RandomValue.random()`. However, if you + really + want to insert a custom string here, use `RandomValue.fromString()` construction method. +* URL was removed due to performance and complicated exception handling and problems with parsing of complex + URLs. `actionUrl`, `referrerUrl`, `outlinkUrl`, `contentTarget` and `downloadUrl` are now strings. +* `getCustomTrackingParameter()` of `MatomoRequest` returns an unmodifiable list now. +* Instead of `IllegalStateException` the tracker throws `MatomoException` +* In former versions the goal id had always to be zero or null. You can now define higher numbers than zero. +* For more type changes see the sections below. + +### Visitor ID + +* `visitorId` and `visitorCustomId` are now of type `VisitorId`. You can build them easily + using `VisitorId.fromHash(...)`. +* You can use `VisitorId.fromHex()` to create a `VisitorId` from a string that contains only hexadecimal characters. +* VisitorId.fromHex() now supports less than 16 hexadecimal characters. If the string is shorter than 16 characters, + the remaining characters will be filled with zeros. + +### Custom Variables + +* According to Matomo, custom variables should no longer be used. Please use dimensions instead. Dimension support has + been introduced. +* `CustomVariable` is now in package `org.matomo.java.tracking.parameters`. +* `customTrackingParameters` in `MatomoRequestBuilder` requires a `Map>` instead + of `Map` now +* `pageCustomVariables` and `visitCustomVariables` are now of type `CustomVariables` instead of collections. Create them + with `CustomVariables.builder().variable(customVariable)` +* `setPageCustomVariable` and `getPageCustomVariable` now longer accept a string as an index. Please use integers + instead. +* Custom variables will now be sent URL encoded + ## Building You need a GPG signing key on your machine. Please follow these @@ -285,8 +363,8 @@ This project can be tested and built by calling mvn install ``` -The built jars and javadoc can be found in `target`. By using the install Maven goal, the snapshot -version can be used using your local Maven repository for testing purposes, e.g. +The built jars and javadoc can be found in `target`. By using the Maven goal `install, the snapshot +version can be used in your local Maven repository for testing purposes, e.g. ```xml diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 00000000..c71071b6 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,358 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 992a9dce..b4818b1d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,10 +1,10 @@ - 4.0.0 org.piwik.java.tracking matomo-java-tracker - 2.2.0-SNAPSHOT + 3.0.0-rc1-SNAPSHOT jar Matomo Java Tracker @@ -58,6 +58,10 @@ UTF-8 + UTF-8 + UTF-8 + 1.8 + 1.8 2.0.9 @@ -68,36 +72,46 @@ org.apache.maven.plugins maven-compiler-plugin 3.11.0 - - 1.8 - 1.8 - - org.pitest - pitest-maven - 1.15.2 - - - org.matomo.java.tracking* - - - org.matomo.java.tracking* - - + org.apache.maven.plugins + maven-project-info-reports-plugin + 3.4.5 - org.jacoco - jacoco-maven-plugin - 0.8.11 - - - prepare-agent - - prepare-agent - - - + org.apache.maven.plugins + maven-clean-plugin + 3.3.2 + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.1 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.1 + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + + org.apache.maven.plugins + maven-site-plugin + 3.12.1 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.1 org.apache.maven.plugins @@ -121,7 +135,7 @@ ossrh https://oss.sonatype.org/ - false + true @@ -157,6 +171,10 @@ org.apache.maven.plugins maven-javadoc-plugin 3.6.0 + + true + all,-missing,-reference + attach-javadocs @@ -166,24 +184,143 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + 3.4.1 + + + enforce-maven + + enforce + + + + + 3.2.5 + + + + + + + + org.pitest + pitest-maven + 1.15.2 + + + org.matomo.java.tracking* + + + org.matomo.java.tracking* + + + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + prepare-agent + + prepare-agent + + + + report + + report + + + + check + + check + + + + + CLASS + + + LINE + COVEREDRATIO + 0.9 + + + BRANCH + COVEREDRATIO + 0.5 + + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.1 + + warning + checkstyle.xml + true + + + + + validate + + + check + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.7.3.6 + + + org.owasp + dependency-check-maven + 8.4.2 + + true + + + + + release + + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + + + + - org.apache.httpcomponents - httpasyncclient - 4.1.5 - - - com.google.guava - guava - 32.1.3-jre - - - com.fasterxml.jackson.core - jackson-databind - 2.15.3 + org.jetbrains + annotations + 24.0.1 + provided org.slf4j @@ -191,15 +328,21 @@ ${slf4j.version} - org.mockito - mockito-core - 4.11.0 + org.projectlombok + lombok + 1.18.30 + provided + + + org.junit.jupiter + junit-jupiter + 5.10.0 test - junit - junit - 4.13.2 + org.assertj + assertj-core + 3.24.2 test @@ -209,10 +352,11 @@ test - org.projectlombok - lombok - 1.18.30 - provided + com.github.tomakehurst + wiremock + 2.27.2 + test + diff --git a/src/main/java/lombok.config b/src/main/java/lombok.config new file mode 100644 index 00000000..7a21e880 --- /dev/null +++ b/src/main/java/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true diff --git a/src/main/java/org/matomo/java/tracking/AuthToken.java b/src/main/java/org/matomo/java/tracking/AuthToken.java new file mode 100644 index 00000000..1c2f1ecb --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/AuthToken.java @@ -0,0 +1,43 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import org.jetbrains.annotations.Nullable; + +final class AuthToken { + + private AuthToken() { + // utility + } + + @Nullable + static String determineAuthToken( + @Nullable String overrideAuthToken, + @Nullable Iterable requests, + @Nullable TrackerConfiguration trackerConfiguration + ) { + if (isNotBlank(overrideAuthToken)) { + return overrideAuthToken; + } + if (requests != null) { + for (MatomoRequest request : requests) { + if (request != null && isNotBlank(request.getAuthToken())) { + return request.getAuthToken(); + } + } + } + if (trackerConfiguration != null && isNotBlank(trackerConfiguration.getDefaultAuthToken())) { + return trackerConfiguration.getDefaultAuthToken(); + } + return null; + } + + private static boolean isNotBlank(@Nullable String str) { + return str != null && !str.isEmpty() && !str.trim().isEmpty(); + } +} diff --git a/src/main/java/org/matomo/java/tracking/CustomVariable.java b/src/main/java/org/matomo/java/tracking/CustomVariable.java index 2d52e4d4..ad148b46 100644 --- a/src/main/java/org/matomo/java/tracking/CustomVariable.java +++ b/src/main/java/org/matomo/java/tracking/CustomVariable.java @@ -4,32 +4,27 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.matomo.java.tracking; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.NonNull; -import lombok.ToString; -import lombok.experimental.FieldDefaults; /** * A user defined custom variable. * * @author brettcsorba + * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. */ -@Getter -@FieldDefaults(level = AccessLevel.PRIVATE) -@AllArgsConstructor -@ToString -@EqualsAndHashCode -public class CustomVariable { - - @NonNull - String key; - - @NonNull - String value; +@Deprecated +public class CustomVariable extends org.matomo.java.tracking.parameters.CustomVariable { + /** + * Instantiates a new custom variable. + * + * @param key the key of the custom variable (required) + * @param value the value of the custom variable (required) + */ + public CustomVariable(@NonNull String key, @NonNull String value) { + super(key, value); + } } diff --git a/src/main/java/org/matomo/java/tracking/CustomVariables.java b/src/main/java/org/matomo/java/tracking/CustomVariables.java deleted file mode 100644 index 0bbe94df..00000000 --- a/src/main/java/org/matomo/java/tracking/CustomVariables.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.EqualsAndHashCode; -import lombok.NonNull; - -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; - -/** - * @author brettcsorba - */ -@EqualsAndHashCode -class CustomVariables { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private final Map variables = new HashMap<>(); - - void add(@NonNull CustomVariable variable) { - boolean found = false; - for (Map.Entry entry : variables.entrySet()) { - CustomVariable customVariable = entry.getValue(); - if (customVariable.getKey().equals(variable.getKey())) { - variables.put(entry.getKey(), variable); - found = true; - } - } - if (!found) { - int i = 1; - while (variables.putIfAbsent(i, variable) != null) { - i++; - } - } - } - - void add(@NonNull CustomVariable cv, int index) { - if (index <= 0) { - throw new IllegalArgumentException("Index must be greater than 0."); - } - variables.put(index, cv); - } - - @Nullable - CustomVariable get(int index) { - if (index <= 0) { - throw new IllegalArgumentException("Index must be greater than 0."); - } - return variables.get(index); - } - - @Nullable - String get(@NonNull String key) { - return variables.values().stream().filter(variable -> variable.getKey().equals(key)).findFirst().map(CustomVariable::getValue).orElse(null); - } - - void remove(int index) { - variables.remove(index); - } - - void remove(@NonNull String key) { - variables.entrySet().removeIf(entry -> entry.getValue().getKey().equals(key)); - } - - boolean isEmpty() { - return variables.isEmpty(); - } - - @Override - public String toString() { - ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); - for (Map.Entry entry : variables.entrySet()) { - CustomVariable variable = entry.getValue(); - objectNode.putArray(entry.getKey().toString()).add(variable.getKey()).add(variable.getValue()); - } - return objectNode.toString(); - } -} diff --git a/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java b/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java new file mode 100644 index 00000000..2bd4df9d --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java @@ -0,0 +1,24 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.ThreadFactory; + +class DaemonThreadFactory implements ThreadFactory { + + @Override + public Thread newThread(@Nullable Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + thread.setName("MatomoJavaTracker"); + return thread; + } + +} diff --git a/src/main/java/org/matomo/java/tracking/EcommerceItem.java b/src/main/java/org/matomo/java/tracking/EcommerceItem.java index f7b5bf23..e7df1ed0 100644 --- a/src/main/java/org/matomo/java/tracking/EcommerceItem.java +++ b/src/main/java/org/matomo/java/tracking/EcommerceItem.java @@ -4,28 +4,33 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.matomo.java.tracking; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; /** - * Represents an item in an ecommerce order. + * A user defined custom variable. * * @author brettcsorba + * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. */ -@Setter -@Getter -@AllArgsConstructor -@Builder -public class EcommerceItem { +@Deprecated +public class EcommerceItem extends org.matomo.java.tracking.parameters.EcommerceItem { - private String sku; - private String name; - private String category; - private Double price; - private Integer quantity; + /** + * Instantiates a new ecommerce item. + * + * @param sku the sku (Stock Keeping Unit) of the item + * @param name the name of the item + * @param category the category of the item + * @param price the price of the item + * @param quantity the quantity of the item + */ + public EcommerceItem( + String sku, String name, String category, + Double price, Integer quantity + ) { + super(sku, name, category, price, quantity); + } } diff --git a/src/main/java/org/matomo/java/tracking/EcommerceItems.java b/src/main/java/org/matomo/java/tracking/EcommerceItems.java deleted file mode 100644 index a27ae2f2..00000000 --- a/src/main/java/org/matomo/java/tracking/EcommerceItems.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import lombok.EqualsAndHashCode; -import lombok.NonNull; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.List; - -@EqualsAndHashCode -class EcommerceItems { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - private final List ecommerceItems = new ArrayList<>(); - - public void add(@NonNull EcommerceItem ecommerceItem) { - ecommerceItems.add(ecommerceItem); - } - - @Nonnull - public EcommerceItem get(int index) { - return ecommerceItems.get(index); - } - - @Override - public String toString() { - ArrayNode arrayNode = OBJECT_MAPPER.createArrayNode(); - for (EcommerceItem ecommerceItem : ecommerceItems) { - arrayNode.add(OBJECT_MAPPER.createArrayNode() - .add(ecommerceItem.getSku()) - .add(ecommerceItem.getName()) - .add(ecommerceItem.getCategory()) - .add(ecommerceItem.getPrice()) - .add(ecommerceItem.getQuantity()) - ); - } - return arrayNode.toString(); - } -} diff --git a/src/main/java/org/matomo/java/tracking/HttpClientFactory.java b/src/main/java/org/matomo/java/tracking/HttpClientFactory.java deleted file mode 100644 index 86dca94f..00000000 --- a/src/main/java/org/matomo/java/tracking/HttpClientFactory.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.matomo.java.tracking; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import org.apache.http.HttpHost; -import org.apache.http.client.HttpClient; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.conn.DefaultProxyRoutePlanner; -import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; -import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; - -import javax.annotation.Nullable; -import java.util.HashMap; -import java.util.Map; - -/** - * Internal factory for providing instances of HTTP clients. - * Especially {@link org.apache.http.nio.client.HttpAsyncClient} instances are intended to be global resources that share the same lifecycle as the application. - * For details see Apache documentation. - * - * @author norbertroamsys - */ -final class HttpClientFactory { - - private HttpClientFactory() { - // utility - } - - /** - * Internal key class for caching {@link CloseableHttpAsyncClient} instances. - */ - @EqualsAndHashCode - @AllArgsConstructor - private static final class KeyEntry { - - private final String proxyHost; - private final int proxyPort; - private final int timeout; - - } - - private static final Map ASYNC_INSTANCES = new HashMap<>(); - - /** - * Factory for getting a synchronous client by proxy and timeout configuration. - * The clients will be created on each call. - * - * @param proxyHost the proxy host - * @param proxyPort the proxy port - * @param timeout the timeout - * @return the created client - */ - public static HttpClient getInstanceFor(final String proxyHost, final int proxyPort, final int timeout) { - return HttpClientBuilder.create().setRoutePlanner(createRoutePlanner(proxyHost, proxyPort)).setDefaultRequestConfig(createRequestConfig(timeout)).build(); - } - - /** - * Factory for getting a asynchronous client by proxy and timeout configuration. - * The clients will be created and cached as a singleton instance. - * - * @param proxyHost the proxy host - * @param proxyPort the proxy port - * @param timeout the timeout - * @return the created client - */ - public static synchronized CloseableHttpAsyncClient getAsyncInstanceFor(final String proxyHost, final int proxyPort, final int timeout) { - return ASYNC_INSTANCES.computeIfAbsent(new KeyEntry(proxyHost, proxyPort, timeout), key -> - HttpAsyncClientBuilder.create().setRoutePlanner(createRoutePlanner(key.proxyHost, key.proxyPort)).setDefaultRequestConfig(createRequestConfig(key.timeout)).build()); - } - - @Nullable - private static DefaultProxyRoutePlanner createRoutePlanner(final String proxyHost, final int proxyPort) { - if (proxyHost != null && proxyPort != 0) { - final HttpHost proxy = new HttpHost(proxyHost, proxyPort); - return new DefaultProxyRoutePlanner(proxy); - } - return null; - } - - private static RequestConfig createRequestConfig(final int timeout) { - final RequestConfig.Builder config = RequestConfig.custom() - .setConnectTimeout(timeout) - .setConnectionRequestTimeout(timeout) - .setSocketTimeout(timeout); - return config.build(); - } - -} diff --git a/src/main/java/org/matomo/java/tracking/InvalidUrlException.java b/src/main/java/org/matomo/java/tracking/InvalidUrlException.java index 01d4fbcb..0b243ab0 100644 --- a/src/main/java/org/matomo/java/tracking/InvalidUrlException.java +++ b/src/main/java/org/matomo/java/tracking/InvalidUrlException.java @@ -1,8 +1,18 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + package org.matomo.java.tracking; +/** + * Thrown when an invalid URL is passed to the tracker. + */ public class InvalidUrlException extends RuntimeException { - public InvalidUrlException(Throwable cause) { + InvalidUrlException(Throwable cause) { super(cause); } } diff --git a/src/main/java/org/matomo/java/tracking/MatomoBoolean.java b/src/main/java/org/matomo/java/tracking/MatomoBoolean.java deleted file mode 100644 index faa3ee92..00000000 --- a/src/main/java/org/matomo/java/tracking/MatomoBoolean.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import lombok.Value; - -/** - * Object representing a locale required by some Matomo query parameters. - * - * @author brettcsorba - */ -@Value -public class MatomoBoolean { - boolean value; - - /** - * Returns the locale's lowercase country code. - * - * @return the locale's lowercase country code - */ - @Override - public String toString() { - return value ? "1" : "0"; - } -} diff --git a/src/main/java/org/matomo/java/tracking/MatomoDate.java b/src/main/java/org/matomo/java/tracking/MatomoDate.java index 25706998..9c02e400 100644 --- a/src/main/java/org/matomo/java/tracking/MatomoDate.java +++ b/src/main/java/org/matomo/java/tracking/MatomoDate.java @@ -4,22 +4,25 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.matomo.java.tracking; +import lombok.Getter; + import java.time.Instant; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.Locale; /** * A datetime object that will return the datetime in the format {@code yyyy-MM-dd hh:mm:ss}. * * @author brettcsorba + * @deprecated Please use {@link Instant} */ +@Deprecated +@Getter public class MatomoDate { - private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); private ZonedDateTime zonedDateTime; @@ -27,6 +30,7 @@ public class MatomoDate { * Allocates a Date object and initializes it so that it represents the time * at which it was allocated, measured to the nearest millisecond. */ + @Deprecated public MatomoDate() { zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC); } @@ -38,6 +42,7 @@ public MatomoDate() { * * @param epochMilli the milliseconds since January 1, 1970, 00:00:00 GMT. */ + @Deprecated public MatomoDate(long epochMilli) { zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneOffset.UTC); } @@ -52,18 +57,6 @@ public void setTimeZone(ZoneId zone) { zonedDateTime = zonedDateTime.withZoneSameInstant(zone); } - /** - * Converts this MatomoDate object to a String of the form:
- *
- * {@code yyyy-MM-dd hh:mm:ss}. - * - * @return a string representation of this MatomoDate - */ - @Override - public String toString() { - return DATE_TIME_FORMATTER.format(zonedDateTime); - } - /** * Converts this datetime to the number of milliseconds from the epoch * of 1970-01-01T00:00:00Z. diff --git a/src/main/java/org/matomo/java/tracking/MatomoException.java b/src/main/java/org/matomo/java/tracking/MatomoException.java index 39ea492e..1f94a08f 100644 --- a/src/main/java/org/matomo/java/tracking/MatomoException.java +++ b/src/main/java/org/matomo/java/tracking/MatomoException.java @@ -1,10 +1,25 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + package org.matomo.java.tracking; +/** + * Thrown when an error occurs while communicating with the Matomo server or when the request is invalid. + */ public class MatomoException extends RuntimeException { private static final long serialVersionUID = 4592083764365938934L; - public MatomoException(String message, Throwable cause) { + MatomoException(String message) { + super(message); + } + + MatomoException(String message, Throwable cause) { super(message, cause); } + } diff --git a/src/main/java/org/matomo/java/tracking/MatomoLocale.java b/src/main/java/org/matomo/java/tracking/MatomoLocale.java index a561a17a..f889e443 100644 --- a/src/main/java/org/matomo/java/tracking/MatomoLocale.java +++ b/src/main/java/org/matomo/java/tracking/MatomoLocale.java @@ -4,32 +4,37 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.matomo.java.tracking; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; +import static java.util.Objects.requireNonNull; import java.util.Locale; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; +import org.matomo.java.tracking.parameters.Country; /** * Object representing a locale required by some Matomo query parameters. * * @author brettcsorba + * @deprecated Use {@link Country} instead */ @Setter @Getter -@AllArgsConstructor -public class MatomoLocale { - private Locale locale; +@Deprecated +public class MatomoLocale extends Country { /** - * Returns the locale's lowercase country code. + * Constructs a new MatomoLocale. * - * @return the locale's lowercase country code + * @param locale The locale to get the country code from + * @deprecated Please use {@link Country} */ - @Override - public String toString() { - return locale.getCountry().toLowerCase(Locale.ENGLISH); + @Deprecated + public MatomoLocale(@NotNull Locale locale) { + super(requireNonNull(locale, "Locale must not be null")); } + } diff --git a/src/main/java/org/matomo/java/tracking/MatomoRequest.java b/src/main/java/org/matomo/java/tracking/MatomoRequest.java index f3681161..46570479 100644 --- a/src/main/java/org/matomo/java/tracking/MatomoRequest.java +++ b/src/main/java/org/matomo/java/tracking/MatomoRequest.java @@ -4,30 +4,39 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.matomo.java.tracking; -import com.google.common.collect.LinkedHashMultimap; -import com.google.common.collect.Multimap; -import com.google.common.io.BaseEncoding; -import lombok.NonNull; -import lombok.ToString; -import org.apache.http.client.utils.URIBuilder; +import static java.util.Objects.requireNonNull; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.net.MalformedURLException; -import java.net.URL; import java.nio.charset.Charset; -import java.security.SecureRandom; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; -import java.util.Date; -import java.util.HashSet; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.matomo.java.tracking.parameters.AcceptLanguage; +import org.matomo.java.tracking.parameters.Country; +import org.matomo.java.tracking.parameters.CustomVariable; +import org.matomo.java.tracking.parameters.CustomVariables; +import org.matomo.java.tracking.parameters.DeviceResolution; +import org.matomo.java.tracking.parameters.EcommerceItem; +import org.matomo.java.tracking.parameters.EcommerceItems; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.UniqueId; +import org.matomo.java.tracking.parameters.VisitorId; /** * A class that implements the @@ -35,1123 +44,671 @@ * * @author brettcsorba */ +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor @ToString public class MatomoRequest { - public static final int ID_LENGTH = 16; - public static final int AUTH_TOKEN_LENGTH = 32; - - private static final String ACTION_NAME = "action_name"; - private static final String ACTION_TIME = "gt_ms"; - private static final String ACTION_URL = "url"; - private static final String API_VERSION = "apiv"; - private static final String AUTH_TOKEN = "token_auth"; - private static final String CAMPAIGN_KEYWORD = "_rck"; - private static final String CAMPAIGN_NAME = "_rcn"; - private static final String CHARACTER_SET = "cs"; - private static final String CONTENT_INTERACTION = "c_i"; - private static final String CONTENT_NAME = "c_n"; - private static final String CONTENT_PIECE = "c_p"; - private static final String CONTENT_TARGET = "c_t"; - private static final String CURRENT_HOUR = "h"; - private static final String CURRENT_MINUTE = "m"; - private static final String CURRENT_SECOND = "s"; - - private static final String CUSTOM_ACTION = "ca"; - private static final String DEVICE_RESOLUTION = "res"; - private static final String DOWNLOAD_URL = "download"; - private static final String ECOMMERCE_DISCOUNT = "ec_dt"; - private static final String ECOMMERCE_ID = "ec_id"; - private static final String ECOMMERCE_ITEMS = "ec_items"; - private static final String ECOMMERCE_LAST_ORDER_TIMESTAMP = "_ects"; - private static final String ECOMMERCE_REVENUE = "revenue"; - private static final String ECOMMERCE_SHIPPING_COST = "ec_sh"; - private static final String ECOMMERCE_SUBTOTAL = "ec_st"; - private static final String ECOMMERCE_TAX = "ec_tx"; - private static final String EVENT_ACTION = "e_a"; - private static final String EVENT_CATEGORY = "e_c"; - private static final String EVENT_NAME = "e_n"; - private static final String EVENT_VALUE = "e_v"; - private static final String HEADER_ACCEPT_LANGUAGE = "lang"; - private static final String GOAL_ID = "idgoal"; - private static final String GOAL_REVENUE = "revenue"; - private static final String HEADER_USER_AGENT = "ua"; - private static final String NEW_VISIT = "new_visit"; - private static final String OUTLINK_URL = "link"; - private static final String PAGE_CUSTOM_VARIABLE = "cvar"; - private static final String PLUGIN_DIRECTOR = "dir"; - private static final String PLUGIN_FLASH = "fla"; - private static final String PLUGIN_GEARS = "gears"; - private static final String PLUGIN_JAVA = "java"; - private static final String PLUGIN_PDF = "pdf"; - private static final String PLUGIN_QUICKTIME = "qt"; - private static final String PLUGIN_REAL_PLAYER = "realp"; - private static final String PLUGIN_SILVERLIGHT = "ag"; - private static final String PLUGIN_WINDOWS_MEDIA = "wma"; - private static final String RANDOM_VALUE = "rand"; - private static final String REFERRER_URL = "urlref"; - private static final String REQUEST_DATETIME = "cdt"; - private static final String REQUIRED = "rec"; - private static final String RESPONSE_AS_IMAGE = "send_image"; - private static final String SEARCH_CATEGORY = "search_cat"; - private static final String SEARCH_QUERY = "search"; - private static final String SEARCH_RESULTS_COUNT = "search_count"; - private static final String SITE_ID = "idsite"; - private static final String TRACK_BOT_REQUESTS = "bots"; - private static final String VISIT_CUSTOM_VARIABLE = "_cvar"; - private static final String USER_ID = "uid"; - private static final String VISITOR_CITY = "city"; - private static final String VISITOR_COUNTRY = "country"; - private static final String VISITOR_CUSTOM_ID = "cid"; - private static final String VISITOR_FIRST_VISIT_TIMESTAMP = "_idts"; - private static final String VISITOR_ID = "_id"; - private static final String VISITOR_IP = "cip"; - private static final String VISITOR_LATITUDE = "lat"; - private static final String VISITOR_LONGITUDE = "long"; - private static final String VISITOR_PREVIOUS_VISIT_TIMESTAMP = "_viewts"; - private static final String VISITOR_REGION = "region"; - private static final String VISITOR_VISIT_COUNT = "_idvc"; - - private static final int RANDOM_VALUE_LENGTH = 20; - private static final long REQUEST_DATETIME_AUTH_LIMIT = 14400000L; - private static final Pattern VISITOR_ID_PATTERN = Pattern.compile("[0-9A-Fa-f]+"); - - private final Multimap parameters = LinkedHashMultimap.create(8, 1); - - private final Set customTrackingParameterNames = new HashSet<>(2); /** - * Create a new request from the id of the site being tracked and the full - * url for the current action. This constructor also sets: - *
-   * {@code
-   * Required = true
-   * Visior Id = random 16 character hex string
-   * Random Value = random 20 character hex string
-   * API version = 1
-   * Response as Image = false
-   * }
-   * 
- * Overwrite these values yourself as desired. - * - * @param siteId the id of the website we're tracking a visit/action for - * @param actionUrl the full URL for the current action + * The ID of the website we're tracking a visit/action for. Only needed, if no default site id is configured */ - public MatomoRequest(int siteId, String actionUrl) { - setParameter(SITE_ID, siteId); - setBooleanParameter(REQUIRED, true); - setParameter(ACTION_URL, actionUrl); - setParameter(VISITOR_ID, getRandomHexString(ID_LENGTH)); - setParameter(RANDOM_VALUE, getRandomHexString(RANDOM_VALUE_LENGTH)); - setParameter(API_VERSION, "1"); - setBooleanParameter(RESPONSE_AS_IMAGE, false); - } + @TrackingParameter(name = "rec") + @Default + private Boolean required = true; - public static MatomoRequestBuilder builder() { - return new MatomoRequestBuilder(); - } + /** + * The ID of the website we're tracking a visit/action for. Only needed, if no default site id is configured + */ + @TrackingParameter(name = "idsite") + private Integer siteId; /** - * Get the title of the action being tracked - * - * @return the title of the action being tracked + * The title of the action being tracked. For page tracks this is used as page title. If enabled in your installation + * you may use the category tree structure in this field. For example, "game / register new user" would then create a + * group "game" and add the item "register new user" in it. */ - @Nullable - public String getActionName() { - return castOrNull(ACTION_NAME); - } + @TrackingParameter(name = "action_name") + private String actionName; /** - * Set the title of the action being tracked. It is possible to - *
use slashes / - * to set one or several categories for this action. - * For example, Help / Feedback - * will create the Action Feedback in the category Help. - * - * @param actionName the title of the action to set. A null value will remove this parameter + * The full URL for the current action. */ - public void setActionName(String actionName) { - setParameter(ACTION_NAME, actionName); - } + @TrackingParameter(name = "url") + private String actionUrl; /** - * Get the amount of time it took the server to generate this action, in milliseconds. - * - * @return the amount of time + * Defines the API version to use (default: 1). */ - @Nullable - public Long getActionTime() { - return castOrNull(ACTION_TIME); - } + @TrackingParameter(name = "apiv") + @Default + private String apiVersion = "1"; /** - * Set the amount of time it took the server to generate this action, in milliseconds. - * This value is used to process the - * Page speed report - * Avg. generation time column in the Page URL and Page Title reports, - * as well as a site wide running average of the speed of your server. - * - * @param actionTime the amount of time to set. A null value will remove this parameter + * The unique visitor ID. See {@link VisitorId} */ - public void setActionTime(Long actionTime) { - setParameter(ACTION_TIME, actionTime); - } + @TrackingParameter(name = "_id") + @Default + private VisitorId visitorId = VisitorId.random(); /** - * Get the full URL for the current action. - * - * @return the full URL - * @deprecated Please use {@link #getActionUrlAsString} + * The full HTTP Referrer URL. This value is used to determine how someone got to your website (ie, through a website, + * search engine or campaign) */ - @Nullable - public URL getActionUrl() { - return castToUrlOrNull(ACTION_URL); - } + @TrackingParameter(name = "urlref") + private String referrerUrl; /** - * Get the full URL for the current action. - * - * @return the full URL + * Custom variables are custom name-value pairs that you can assign to your visitors (or page views). */ - @Deprecated - @Nullable - public String getActionUrlAsString() { - return castOrNull(ACTION_URL); - } + @TrackingParameter(name = "_cvar") + private CustomVariables visitCustomVariables; + /** + * The current count of visits for this visitor. To set this value correctly, it would be required to store the value + * for each visitor in your application (using sessions or persisting in a database). Then you would manually + * increment the counts by one on each new visit or "session", depending on how you choose to define a visit. + */ + @TrackingParameter(name = "_idvc") + private Integer visitorVisitCount; /** - * Set the full URL for the current action. - * - * @param actionUrl the full URL to set. A null value will remove this parameter - * @deprecated Please use {@link #setActionUrl(String)} + * The UNIX timestamp of this visitor's previous visit. This parameter is used to populate the report Visitors > + * Engagement > Visits by days since last visit. */ - @Deprecated - public void setActionUrl(@NonNull URL actionUrl) { - setActionUrl(actionUrl.toString()); - } + @TrackingParameter(name = "_viewts") + private Instant visitorPreviousVisitTimestamp; /** - * Set the full URL for the current action. - * - * @param actionUrl the full URL to set. A null value will remove this parameter + * The UNIX timestamp of this visitor's first visit. This could be set to the date where the user first started using + * your software/app, or when he/she created an account. */ - public void setActionUrl(String actionUrl) { - setParameter(ACTION_URL, actionUrl); - } + @TrackingParameter(name = "_idts") + private Instant visitorFirstVisitTimestamp; /** - * Set the full URL for the current action. - * - * @param actionUrl the full URL to set. A null value will remove this parameter - * @deprecated Please use {@link #setActionUrl(String)} + * The campaign name. This parameter will only be used for the first pageview of a visit. */ - @Deprecated - public void setActionUrlWithString(String actionUrl) { - setActionUrl(actionUrl); - } + @TrackingParameter(name = "_rcn") + private String campaignName; /** - * Get the api version - * - * @return the api version + * The campaign keyword (see + * Tracking Campaigns). Used to populate the Referrers > Campaigns report (clicking on a + * campaign loads all keywords for this campaign). This parameter will only be used for the first pageview of a visit. */ - @Nullable - public String getApiVersion() { - return castOrNull(API_VERSION); - } + @TrackingParameter(name = "_rck") + private String campaignKeyword; /** - * Set the api version to use (currently always set to 1) - * - * @param apiVersion the api version to set. A null value will remove this parameter + * The resolution of the device the visitor is using. */ - public void setApiVersion(String apiVersion) { - setParameter(API_VERSION, apiVersion); - } + @TrackingParameter(name = "res") + private DeviceResolution deviceResolution; /** - * Get the authorization key. - * - * @return the authorization key + * The current hour (local time). */ - @Nullable - public String getAuthToken() { - return castOrNull(AUTH_TOKEN); - } + @TrackingParameter(name = "h") + private Integer currentHour; /** - * Set the {@value #AUTH_TOKEN_LENGTH} character authorization key used to authenticate the API request. - * - * @param authToken the authorization key to set. A null value will remove this parameter + * The current minute (local time). */ - public void setAuthToken(String authToken) { - if (authToken != null && authToken.length() != AUTH_TOKEN_LENGTH) { - throw new IllegalArgumentException(authToken + " is not " + AUTH_TOKEN_LENGTH + " characters long."); - } - setParameter(AUTH_TOKEN, authToken); - } + @TrackingParameter(name = "m") + private Integer currentMinute; /** - * Verifies that AuthToken has been set for this request. Will throw an - * {@link IllegalStateException} if not. + * The current second (local time). */ - public void verifyAuthTokenSet() { - if (getAuthToken() == null) { - throw new IllegalStateException("AuthToken must be set before this value can be set."); - } - } + @TrackingParameter(name = "s") + private Integer currentSecond; /** - * Get the campaign keyword - * - * @return the campaign keyword + * Does the visitor use the Adobe Flash Plugin. */ - @Nullable - public String getCampaignKeyword() { - return castOrNull(CAMPAIGN_KEYWORD); - } + @TrackingParameter(name = "fla") + private Boolean pluginFlash; /** - * Set the Campaign Keyword (see - * Tracking Campaigns). - * Used to populate the Referrers > Campaigns report (clicking on a - * campaign loads all keywords for this campaign). Note: this parameter - * will only be used for the first pageview of a visit. - * - * @param campaignKeyword the campaign keyword to set. A null value will remove this parameter + * Does the visitor use the Java plugin. */ - public void setCampaignKeyword(String campaignKeyword) { - setParameter(CAMPAIGN_KEYWORD, campaignKeyword); - } + @TrackingParameter(name = "java") + private Boolean pluginJava; /** - * Get the campaign name - * - * @return the campaign name + * Does the visitor use Director plugin. */ - @Nullable - public String getCampaignName() { - return castOrNull(CAMPAIGN_NAME); - } + @TrackingParameter(name = "dir") + private Boolean pluginDirector; /** - * Set the Campaign Name (see - * Tracking Campaigns). - * Used to populate the Referrers > Campaigns report. Note: this parameter - * will only be used for the first pageview of a visit. - * - * @param campaignName the campaign name to set. A null value will remove this parameter + * Does the visitor use Quicktime plugin. */ - public void setCampaignName(String campaignName) { - setParameter(CAMPAIGN_NAME, campaignName); - } + @TrackingParameter(name = "qt") + private Boolean pluginQuicktime; /** - * Get the charset of the page being tracked - * - * @return the charset + * Does the visitor use Realplayer plugin. */ - @Nullable - public Charset getCharacterSet() { - return castOrNull(CHARACTER_SET); - } + @TrackingParameter(name = "realp") + private Boolean pluginRealPlayer; /** - * The charset of the page being tracked. Specify the charset if the data - * you send to Matomo is encoded in a different character set than the default - * utf-8. - * - * @param characterSet the charset to set. A null value will remove this parameter + * Does the visitor use a PDF plugin. */ - public void setCharacterSet(Charset characterSet) { - setParameter(CHARACTER_SET, characterSet); - } + @TrackingParameter(name = "pdf") + private Boolean pluginPDF; /** - * Get the name of the interaction with the content - * - * @return the name of the interaction + * Does the visitor use a Windows Media plugin. */ - @Nullable - public String getContentInteraction() { - return castOrNull(CONTENT_INTERACTION); - } + @TrackingParameter(name = "wma") + private Boolean pluginWindowsMedia; /** - * Set the name of the interaction with the content. For instance a 'click'. - * - * @param contentInteraction the name of the interaction to set. A null value will remove this parameter + * Does the visitor use a Gears plugin. */ - public void setContentInteraction(String contentInteraction) { - setParameter(CONTENT_INTERACTION, contentInteraction); - } + @TrackingParameter(name = "gears") + private Boolean pluginGears; /** - * Get the name of the content - * - * @return the name + * Does the visitor use a Silverlight plugin. */ - @Nullable - public String getContentName() { - return castOrNull(CONTENT_NAME); - } + @TrackingParameter(name = "ag") + private Boolean pluginSilverlight; /** - * Set the name of the content. For instance 'Ad Foo Bar'. - * - * @param contentName the name to set. A null value will remove this parameter + * Does the visitor's client is known to support cookies. */ - public void setContentName(String contentName) { - setParameter(CONTENT_NAME, contentName); - } + @TrackingParameter(name = "cookie") + private Boolean supportsCookies; /** - * Get the content piece. - * - * @return the content piece. + * An override value for the User-Agent HTTP header field. */ - @Nullable - public String getContentPiece() { - return castOrNull(CONTENT_PIECE); - } + @TrackingParameter(name = "ua") + private String headerUserAgent; /** - * Set the actual content piece. For instance the path to an image, video, audio, any text. - * - * @param contentPiece the content piece to set. A null value will remove this parameter + * An override value for the Accept-Language HTTP header field. This value is used to detect the visitor's country if + * GeoIP is not enabled. */ - public void setContentPiece(String contentPiece) { - setParameter(CONTENT_PIECE, contentPiece); - } + @TrackingParameter(name = "lang") + private AcceptLanguage headerAcceptLanguage; /** - * Get the content target - * - * @return the target + * Defines the User ID for this request. User ID is any non-empty unique string identifying the user (such as an email + * address or a username). When specified, the User ID will be "enforced". This means that if there is no recent + * visit with this User ID, a new one will be created. If a visit is found in the last 30 minutes with your specified + * User ID, then the new action will be recorded to this existing visit. */ - @Nullable - public URL getContentTarget() { - return castToUrlOrNull(CONTENT_TARGET); - } + @TrackingParameter(name = "uid") + private String userId; - @Nullable - private URL castToUrlOrNull(@NonNull String key) { - String url = castOrNull(key); - if (url == null) { - return null; - } - try { - return new URL(url); - } catch (MalformedURLException e) { - throw new InvalidUrlException(e); - } - } + /** + * defines the visitor ID for this request. + */ + @TrackingParameter(name = "cid") + private VisitorId visitorCustomId; /** - * Get the content target - * - * @return the target + * will force a new visit to be created for this action. */ - @Nullable - public String getContentTargetAsString() { - return castOrNull(CONTENT_TARGET); - } + @TrackingParameter(name = "new_visit") + private Boolean newVisit; /** - * Set the target of the content. For instance the URL of a landing page. - * - * @param contentTarget the target to set. A null value will remove this parameter - * @deprecated Please use {@link #setContentTarget(String)} + * Custom variables are custom name-value pairs that you can assign to your visitors (or page views). */ - @Deprecated - public void setContentTarget(@NonNull URL contentTarget) { - setContentTarget(contentTarget.toString()); - } + @TrackingParameter(name = "cvar") + private CustomVariables pageCustomVariables; /** - * Set the target of the content. For instance the URL of a landing page. - * - * @param contentTarget the target to set. A null value will remove this parameter + * An external URL the user has opened. Used for tracking outlink clicks. We recommend to also set the url parameter + * to this same value. */ - public void setContentTarget(String contentTarget) { - setParameter(CONTENT_TARGET, contentTarget); - } + @TrackingParameter(name = "link") + private String outlinkUrl; /** - * Set the target of the content. For instance the URL of a landing page. - * - * @param contentTarget the target to set. A null value will remove this parameter - * @deprecated Please use {@link #setContentTarget(String)} + * URL of a file the user has downloaded. Used for tracking downloads. We recommend to also set the url parameter to + * this same value. */ - @Deprecated - public void setContentTargetWithString(String contentTarget) { - setContentTarget(contentTarget); - } + @TrackingParameter(name = "download") + private String downloadUrl; /** - * Get the current hour. - * - * @return the current hour + * The Site Search keyword. When specified, the request will not be tracked as a normal pageview but will instead be + * tracked as a Site Search request */ - @Nullable - public Integer getCurrentHour() { - return castOrNull(CURRENT_HOUR); - } + @TrackingParameter(name = "search") + private String searchQuery; /** - * Set the current hour (local time). - * - * @param currentHour the hour to set. A null value will remove this parameter + * When search is specified, you can optionally specify a search category with this parameter. */ - public void setCurrentHour(Integer currentHour) { - setParameter(CURRENT_HOUR, currentHour); - } + @TrackingParameter(name = "search_cat") + private String searchCategory; /** - * Get the current minute. - * - * @return the current minute + * When search is specified, we also recommend setting the search_count to the number of search results displayed on + * the results page. When keywords are tracked with &search_count=0 they will appear in the "No Result Search Keyword" + * report. */ - @Nullable - public Integer getCurrentMinute() { - return castOrNull(CURRENT_MINUTE); - } + @TrackingParameter(name = "search_count") + private Long searchResultsCount; /** - * Set the current minute (local time). - * - * @param currentMinute the minute to set. A null value will remove this parameter + * Accepts a six character unique ID that identifies which actions were performed on a specific page view. When a page + * was viewed, all following tracking requests (such as events) during that page view should use the same pageview ID. + * Once another page was viewed a new unique ID should be generated. Use [0-9a-Z] as possible characters for the + * unique ID. */ - public void setCurrentMinute(Integer currentMinute) { - setParameter(CURRENT_MINUTE, currentMinute); - } + @TrackingParameter(name = "pv_id") + private UniqueId pageViewId; /** - * Get the current second - * - * @return the current second + * If specified, the tracking request will trigger a conversion for the goal of the website being tracked with this + * ID. */ - @Nullable - public Integer getCurrentSecond() { - return castOrNull(CURRENT_SECOND); - } + @TrackingParameter(name = "idgoal") + private Integer goalId; /** - * Set the current second (local time). - * - * @param currentSecond the second to set. A null value will remove this parameter + * The grand total for the ecommerce order (required when tracking an ecommerce order). */ - public void setCurrentSecond(Integer currentSecond) { - setParameter(CURRENT_SECOND, currentSecond); - } + @TrackingParameter(name = "revenue") + private Double ecommerceRevenue; /** - * Get the custom action - * - * @return the custom action + * The charset of the page being tracked. Specify the charset if the data you send to Matomo is encoded in a different + * character set than the default utf-8 */ - @Nullable - public Boolean getCustomAction() { - return getBooleanParameter(CUSTOM_ACTION); - } + @TrackingParameter(name = "cs") + private Charset characterSet; /** - * Set the custom action - * - * @param customAction the second to set. A null value will remove this parameter + * can be optionally sent along any tracking request that isn't a page view. For example, it can be sent together with + * an event tracking request. The advantage being that should you ever disable the event plugin, then the event + * tracking requests will be ignored vs if the parameter is not set, a page view would be tracked even though it isn't + * a page view. */ - public void setCustomAction(Boolean customAction) { - setBooleanParameter(CUSTOM_ACTION, customAction); - } + @TrackingParameter(name = "ca") + private Boolean customAction; /** - * Gets the list of objects currently stored at the specified custom tracking - * parameter. An empty list will be returned if there are no objects set at - * that key. - * - * @param key the key of the parameter whose list of objects to get. Cannot be null - * @return the list of objects currently stored at the specified key + * How long it took to connect to server. */ - public List getCustomTrackingParameter(@NonNull String key) { - return new ArrayList<>(parameters.get(key)); - } + @TrackingParameter(name = "pf_net") + private Long networkTime; /** - * Set a custom tracking parameter whose toString() value will be sent to - * the Matomo server. These parameters are stored separately from named Matomo - * parameters, meaning it is not possible to overwrite or clear named Matomo - * parameters with this method. A custom parameter that has the same name - * as a named Matomo parameter will be sent in addition to that named parameter. - * - * @param key the parameter's key. Cannot be null - * @param value the parameter's value. Removes the parameter if null + * How long it took the server to generate page. */ - public void setCustomTrackingParameter(@NonNull String key, @Nullable T value) { - customTrackingParameterNames.add(key); - setParameter(key, value); - } + @TrackingParameter(name = "pf_srv") + private Long serverTime; /** - * Add a custom tracking parameter to the specified key. This allows users - * to have multiple parameters with the same name and different values, - * commonly used during situations where list parameters are needed - * - * @param key the parameter's key. Cannot be null - * @param value the parameter's value. Cannot be null + * How long it takes the browser to download the response from the server. */ - public void addCustomTrackingParameter(@NonNull String key, @NonNull Object value) { - customTrackingParameterNames.add(key); - addParameter(key, value); - } + @TrackingParameter(name = "pf_tfr") + private Long transferTime; /** - * Removes all custom tracking parameters + * How long the browser spends loading the webpage after the response was fully received until the user can start + * interacting with it. */ - public void clearCustomTrackingParameter() { - for (String customTrackingParameterName : customTrackingParameterNames) { - setParameter(customTrackingParameterName, null); - } - } + @TrackingParameter(name = "pf_dm1") + private Long domProcessingTime; /** - * Get the resolution of the device - * - * @return the resolution + * How long it takes for the browser to load media and execute any Javascript code listening for the DOMContentLoaded + * event. */ - @Nullable - public String getDeviceResolution() { - return castOrNull(DEVICE_RESOLUTION); - } + @TrackingParameter(name = "pf_dm2") + private Long domCompletionTime; /** - * Set the resolution of the device the visitor is using, eg 1280x1024. - * - * @param deviceResolution the resolution to set. A null value will remove this parameter + * How long it takes the browser to execute Javascript code waiting for the window.load event. */ - public void setDeviceResolution(String deviceResolution) { - setParameter(DEVICE_RESOLUTION, deviceResolution); - } + @TrackingParameter(name = "pf_onl") + private Long onloadTime; /** - * Get the url of a file the user had downloaded - * - * @return the url + * eg. Videos, Music, Games... */ - @Nullable - public URL getDownloadUrl() { - return castToUrlOrNull(DOWNLOAD_URL); - } + @TrackingParameter(name = "e_c") + private String eventCategory; /** - * Get the url of a file the user had downloaded - * - * @return the url + * e.g. Play, Pause, Duration, Add Playlist, Downloaded, Clicked... */ - @Nullable - public String getDownloadUrlAsString() { - return castOrNull(DOWNLOAD_URL); - } + @TrackingParameter(name = "e_a") + private String eventAction; /** - * Set the url of a file the user has downloaded. Used for tracking downloads. - * We recommend to also set the url parameter to this same value. - * - * @param downloadUrl the url to set. A null value will remove this parameter - * @deprecated Please use {@link #setDownloadUrl(String)} + * e.g. a Movie name, or Song name, or File name... */ - @Deprecated - public void setDownloadUrl(@NonNull URL downloadUrl) { - setDownloadUrl(downloadUrl.toString()); - } + @TrackingParameter(name = "e_n") + private String eventName; /** - * Set the url of a file the user has downloaded. Used for tracking downloads. - * We recommend to also set the url parameter to this same value. - * - * @param downloadUrl the url to set. A null value will remove this parameter + * Some numeric value that represents the event value. */ - public void setDownloadUrl(String downloadUrl) { - setParameter(DOWNLOAD_URL, downloadUrl); - } + @TrackingParameter(name = "e_n") + private Double eventValue; /** - * Set the url of a file the user has downloaded. Used for tracking downloads. - * We recommend to also set the url parameter to this same value. - * - * @param downloadUrl the url to set. A null value will remove this parameter - * @deprecated Please use {@link #setDownloadUrl(String)} + * The name of the content. For instance 'Ad Foo Bar' */ - @Deprecated - public void setDownloadUrlWithString(String downloadUrl) { - setDownloadUrl(downloadUrl); - } + @TrackingParameter(name = "c_n") + private String contentName; /** - * Sets idgoal=0 in the request to track an ecommerce interaction: - * cart update or an ecommerce order. + * The actual content piece. For instance the path to an image, video, audio, any text */ - public void enableEcommerce() { - setGoalId(0); - } + @TrackingParameter(name = "c_p") + private String contentPiece; /** - * Verifies that Ecommerce has been enabled for the request. Will throw an - * {@link IllegalStateException} if not. + * The target of the content. For instance the URL of a landing page */ - public void verifyEcommerceEnabled() { - if (getGoalId() == null || getGoalId() != 0) { - throw new IllegalStateException("GoalId must be \"0\". Try calling enableEcommerce first before calling this method."); - } - } + @TrackingParameter(name = "c_t") + private String contentTarget; /** - * Verifies that Ecommerce has been enabled and that Ecommerce Id and - * Ecommerce Revenue have been set for the request. Will throw an - * {@link IllegalStateException} if not. + * The name of the interaction with the content. For instance a 'click' */ - public void verifyEcommerceState() { - verifyEcommerceEnabled(); - if (getEcommerceId() == null) { - throw new IllegalStateException("EcommerceId must be set before this value can be set."); - } - if (getEcommerceRevenue() == null) { - throw new IllegalStateException("EcommerceRevenue must be set before this value can be set."); - } - } + @TrackingParameter(name = "c_i") + private String contentInteraction; /** - * Get the discount offered. - * - * @return the discount + * he unique string identifier for the ecommerce order (required when tracking an ecommerce order). you must set + * &idgoal=0 in the request to track an ecommerce interaction: cart update or an ecommerce order. */ - @Nullable - public Double getEcommerceDiscount() { - return castOrNull(ECOMMERCE_DISCOUNT); - } + @TrackingParameter(name = "ec_id") + private String ecommerceId; /** - * Set the discount offered. Ecommerce must be enabled, and EcommerceId and - * EcommerceRevenue must first be set. - * - * @param discount the discount to set. A null value will remove this parameter + * Items in the Ecommerce order. */ - public void setEcommerceDiscount(Double discount) { - if (discount != null) { - verifyEcommerceState(); - } - setParameter(ECOMMERCE_DISCOUNT, discount); - } + @TrackingParameter(name = "ec_items") + private EcommerceItems ecommerceItems; /** - * Get the id of this order. - * - * @return the id + * The subtotal of the order; excludes shipping. */ - @Nullable - public String getEcommerceId() { - return castOrNull(ECOMMERCE_ID); - } + @TrackingParameter(name = "ec_st") + private Double ecommerceSubtotal; /** - * Set the unique string identifier for the ecommerce order (required when - * tracking an ecommerce order). Ecommerce must be enabled. - * - * @param id the id to set. A null value will remove this parameter + * Tax amount of the order. */ - public void setEcommerceId(String id) { - if (id != null) { - verifyEcommerceEnabled(); - } - setParameter(ECOMMERCE_ID, id); - } + @TrackingParameter(name = "ec_tx") + private Double ecommerceTax; /** - * Get the {@link EcommerceItem} at the specified index - * - * @param index the index of the {@link EcommerceItem} to return - * @return the {@link EcommerceItem} at the specified index + * Shipping cost of the order. */ - @Nullable - public EcommerceItem getEcommerceItem(int index) { - EcommerceItems ecommerceItems = castOrNull(ECOMMERCE_ITEMS); - if (ecommerceItems == null) { - return null; - } - return ecommerceItems.get(index); - } + @TrackingParameter(name = "ec_sh") + private Double ecommerceShippingCost; /** - * Add an {@link EcommerceItem} to this order. Ecommerce must be enabled, - * and EcommerceId and EcommerceRevenue must first be set. - * - * @param item the {@link EcommerceItem} to add. Cannot be null + * Discount offered. */ - public void addEcommerceItem(@NonNull EcommerceItem item) { - verifyEcommerceState(); - EcommerceItems ecommerceItems = castOrNull(ECOMMERCE_ITEMS); - if (ecommerceItems == null) { - ecommerceItems = new EcommerceItems(); - setParameter(ECOMMERCE_ITEMS, ecommerceItems); - } - ecommerceItems.add(item); - } + @TrackingParameter(name = "ec_dt") + private Double ecommerceDiscount; /** - * Clears all {@link EcommerceItem} from this order. + * The UNIX timestamp of this customer's last ecommerce order. This value is used to process the "Days since last + * order" report. */ - public void clearEcommerceItems() { - setParameter(ECOMMERCE_ITEMS, null); - } + @TrackingParameter(name = "_ects") + private Instant ecommerceLastOrderTimestamp; /** - * Get the timestamp of the customer's last ecommerce order - * - * @return the timestamp + * 32 character authorization key used to authenticate the API request. We recommend to create a user specifically for + * accessing the Tracking API, and give the user only write permission on the website(s). */ - @Nullable - public Long getEcommerceLastOrderTimestamp() { - return castOrNull(ECOMMERCE_LAST_ORDER_TIMESTAMP); - } + @TrackingParameter(name = "token_auth", regex = "[a-z0-9]{32}") + private String authToken; + /** - * Set the UNUX timestamp of this customer's last ecommerce order. This value - * is used to process the "Days since last order" report. Ecommerce must be - * enabled, and EcommerceId and EcommerceRevenue must first be set. - * - * @param timestamp the timestamp to set. A null value will remove this parameter + * Override value for the visitor IP (both IPv4 and IPv6 notations supported). */ - public void setEcommerceLastOrderTimestamp(Long timestamp) { - if (timestamp != null) { - verifyEcommerceState(); - } - setParameter(ECOMMERCE_LAST_ORDER_TIMESTAMP, timestamp); - } + @TrackingParameter(name = "cip") + private String visitorIp; /** - * Get the grand total of the ecommerce order. - * - * @return the grand total + * Override for the datetime of the request (normally the current time is used). This can be used to record visits and + * page views in the past. */ - @Nullable - public Double getEcommerceRevenue() { - return castOrNull(ECOMMERCE_REVENUE); - } + @TrackingParameter(name = "cdt") + private Instant requestTimestamp; /** - * Set the grand total of the ecommerce order (required when tracking an - * ecommerce order). Ecommerce must be enabled. - * - * @param revenue the grand total to set. A null value will remove this parameter + * An override value for the country. Must be a two-letter ISO 3166 Alpha-2 country code. */ - public void setEcommerceRevenue(Double revenue) { - if (revenue != null) { - verifyEcommerceEnabled(); - } - setParameter(ECOMMERCE_REVENUE, revenue); - } + @TrackingParameter(name = "country") + private Country visitorCountry; /** - * Get the shipping cost of the ecommerce order. - * - * @return the shipping cost + * An override value for the region. Should be set to a ISO 3166-2 region code, which are used by MaxMind's and + * DB-IP's GeoIP2 databases. See here for a list of them for every country. */ - @Nullable - public Double getEcommerceShippingCost() { - return castOrNull(ECOMMERCE_SHIPPING_COST); - } + @TrackingParameter(name = "region") + private String visitorRegion; /** - * Set the shipping cost of the ecommerce order. Ecommerce must be enabled, - * and EcommerceId and EcommerceRevenue must first be set. - * - * @param shippingCost the shipping cost to set. A null value will remove this parameter + * An override value for the city. The name of the city the visitor is located in, eg, Tokyo. */ - public void setEcommerceShippingCost(Double shippingCost) { - if (shippingCost != null) { - verifyEcommerceState(); - } - setParameter(ECOMMERCE_SHIPPING_COST, shippingCost); - } + @TrackingParameter(name = "city") + private String visitorCity; /** - * Get the subtotal of the ecommerce order; excludes shipping. - * - * @return the subtotal + * An override value for the visitor's latitude, eg 22.456. */ - @Nullable - public Double getEcommerceSubtotal() { - return castOrNull(ECOMMERCE_SUBTOTAL); - } + @TrackingParameter(name = "lat") + private Double visitorLatitude; /** - * Set the subtotal of the ecommerce order; excludes shipping. Ecommerce - * must be enabled and EcommerceId and EcommerceRevenue must first be set. - * - * @param subtotal the subtotal to set. A null value will remove this parameter + * An override value for the visitor's longitude, eg 22.456. */ - public void setEcommerceSubtotal(Double subtotal) { - if (subtotal != null) { - verifyEcommerceState(); - } - setParameter(ECOMMERCE_SUBTOTAL, subtotal); - } + @TrackingParameter(name = "long") + private Double visitorLongitude; /** - * Get the tax amount of the ecommerce order. - * - * @return the tax amount + * When set to false, the queued tracking handler won't be used and instead the tracking request will be executed + * directly. This can be useful when you need to debug a tracking problem or want to test that the tracking works in + * general. */ - @Nullable - public Double getEcommerceTax() { - return castOrNull(ECOMMERCE_TAX); - } + @TrackingParameter(name = "queuedtracking") + private Boolean queuedTracking; /** - * Set the tax amount of the ecommerce order. Ecommerce must be enabled, and - * EcommerceId and EcommerceRevenue must first be set. + * If set to 0 (send_image=0) Matomo will respond with an HTTP 204 response code instead of a GIF image. This improves + * performance and can fix errors if images are not allowed to be obtained directly (e.g. Chrome Apps). Available + * since Matomo 2.10.0 * - * @param tax the tax amount to set. A null value will remove this parameter + *

Default is {@code false} */ - public void setEcommerceTax(Double tax) { - if (tax != null) { - verifyEcommerceState(); - } - setParameter(ECOMMERCE_TAX, tax); - } - - /** - * Get the event action. - * - * @return the event action - */ - @Nullable - public String getEventAction() { - return castOrNull(EVENT_ACTION); - } - - /** - * Set the event action. Must not be empty. (eg. Play, Pause, Duration, - * Add Playlist, Downloaded, Clicked...). - * - * @param eventAction the event action to set. A null value will remove this parameter - */ - public void setEventAction(String eventAction) { - setNonEmptyStringParameter(EVENT_ACTION, eventAction); - } - - /** - * Get the event category. - * - * @return the event category - */ - @Nullable - public String getEventCategory() { - return castOrNull(EVENT_CATEGORY); - } - - /** - * Set the event category. Must not be empty. (eg. Videos, Music, Games...). - * - * @param eventCategory the event category to set. A null value will remove this parameter - */ - public void setEventCategory(String eventCategory) { - setNonEmptyStringParameter(EVENT_CATEGORY, eventCategory); - } + @TrackingParameter(name = "send_image") + @Default + private Boolean responseAsImage = false; /** - * Get the event name. - * - * @return the event name + * If set to true, the request will be a Heartbeat request which will not track any new activity (such as a new visit, + * new action or new goal). The heartbeat request will only update the visit's total time to provide accurate "Visit + * duration" metric when this parameter is set. It won't record any other data. This means by sending an additional + * tracking request when the user leaves your site or app with &ping=1, you fix the issue where the time spent of the + * last page visited is reported as 0 seconds. */ - @Nullable - public String getEventName() { - return castOrNull(EVENT_NAME); - } + @TrackingParameter(name = "ping") + private Boolean ping; /** - * Set the event name. (eg. a Movie name, or Song name, or File name...). - * - * @param eventName the event name to set. A null value will remove this parameter + * By default, Matomo does not track bots. If you use the Tracking HTTP API directly, you may be interested in + * tracking bot requests. */ - public void setEventName(String eventName) { - setParameter(EVENT_NAME, eventName); - } + @TrackingParameter(name = "bots") + private Boolean trackBotRequests; - /** - * Get the event value. - * - * @return the event value - */ - @Nullable - public Number getEventValue() { - return castOrNull(EVENT_VALUE); - } /** - * Set the event value. Must be a float or integer value (numeric), not a string. - * - * @param eventValue the event value to set. A null value will remove this parameter + * Meant to hold a random value that is generated before each request. Using it helps avoid the tracking request + * being cached by the browser or a proxy. */ - public void setEventValue(Number eventValue) { - setParameter(EVENT_VALUE, eventValue); - } + @TrackingParameter(name = "rand") + @Default + private RandomValue randomValue = RandomValue.random(); - /** - * Get the goal id - * - * @return the goal id - */ - @Nullable - public Integer getGoalId() { - return castOrNull(GOAL_ID); - } + private Iterable dimensions; - /** - * Set the goal id. If specified, the tracking request will trigger a - * conversion for the goal of the website being tracked with this id. - * - * @param goalId the goal id to set. A null value will remove this parameter - */ - public void setGoalId(Integer goalId) { - setParameter(GOAL_ID, goalId); - } + private Map> customTrackingParameters; /** - * Get the goal revenue. + * Create a new request from the id of the site being tracked and the full + * url for the current action. This constructor also sets: + *
+   * {@code
+   * Required = true
+   * Visior Id = random 16 character hex string
+   * Random Value = random 20 character hex string
+   * API version = 1
+   * Response as Image = false
+   * }
+   * 
+ * Overwrite these values yourself as desired. * - * @return the goal revenue + * @param siteId the id of the website we're tracking a visit/action for + * @param actionUrl the full URL for the current action + * @deprecated Please use {@link MatomoRequest#builder()} */ - @Nullable - public Double getGoalRevenue() { - return castOrNull(GOAL_REVENUE); + @Deprecated + public MatomoRequest(int siteId, String actionUrl) { + this.siteId = siteId; + this.actionUrl = actionUrl; + required = true; + visitorId = VisitorId.random(); + randomValue = RandomValue.random(); + apiVersion = "1"; + responseAsImage = false; } /** - * Set a monetary value that was generated as revenue by this goal conversion. - * Only used if idgoal is specified in the request. + * Gets the list of objects currently stored at the specified custom tracking + * parameter. An empty list will be returned if there are no objects set at + * that key. * - * @param goalRevenue the goal revenue to set. A null value will remove this parameter + * @param key the key of the parameter whose list of objects to get. Cannot be null + * @return the list of objects currently stored at the specified key */ - public void setGoalRevenue(Double goalRevenue) { - if (goalRevenue != null && getGoalId() == null) { - throw new IllegalStateException("GoalId must be set before GoalRevenue can be set."); + public List getCustomTrackingParameter(@NonNull String key) { + if (customTrackingParameters == null || customTrackingParameters.isEmpty()) { + return Collections.emptyList(); } - setParameter(GOAL_REVENUE, goalRevenue); - } - - /** - * Get the Accept-Language HTTP header - * - * @return the Accept-Language HTTP header - */ - @Nullable - public String getHeaderAcceptLanguage() { - return castOrNull(HEADER_ACCEPT_LANGUAGE); - } - - /** - * Set an override value for the Accept-Language HTTP header - * field. This value is used to detect the visitor's country if - * GeoIP is not enabled. - * - * @param acceptLangage the Accept-Language HTTP header to set. A null value will remove this parameter - */ - public void setHeaderAcceptLanguage(String acceptLangage) { - setParameter(HEADER_ACCEPT_LANGUAGE, acceptLangage); + Collection parameterValues = customTrackingParameters.get(key); + if (parameterValues == null || parameterValues.isEmpty()) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(new ArrayList<>(parameterValues)); } /** - * Get the User-Agent HTTP header + * Set a custom tracking parameter whose toString() value will be sent to + * the Matomo server. These parameters are stored separately from named Matomo + * parameters, meaning it is not possible to overwrite or clear named Matomo + * parameters with this method. A custom parameter that has the same name + * as a named Matomo parameter will be sent in addition to that named parameter. * - * @return the User-Agent HTTP header + * @param key the parameter's key. Cannot be null + * @param value the parameter's value. Removes the parameter if null */ - @Nullable - public String getHeaderUserAgent() { - return castOrNull(HEADER_USER_AGENT); - } + public void setCustomTrackingParameter(@NonNull String key, @Nullable Object value) { - /** - * Set an override value for the User-Agent HTTP header field. - * The user agent is used to detect the operating system and browser used. - * - * @param userAgent the User-Agent HTTP header tos et - */ - public void setHeaderUserAgent(String userAgent) { - setParameter(HEADER_USER_AGENT, userAgent); + if (value == null) { + if (customTrackingParameters != null) { + customTrackingParameters.remove(key); + } + } else { + if (customTrackingParameters == null) { + customTrackingParameters = new LinkedHashMap<>(); + } + Collection values = customTrackingParameters.computeIfAbsent(key, k -> new ArrayList<>()); + values.clear(); + values.add(value); + } } /** - * Get if this request will force a new visit. + * Add a custom tracking parameter to the specified key. This allows users + * to have multiple parameters with the same name and different values, + * commonly used during situations where list parameters are needed * - * @return true if this request will force a new visit + * @param key the parameter's key. Cannot be null + * @param value the parameter's value. Cannot be null */ - @Nullable - public Boolean getNewVisit() { - return getBooleanParameter(NEW_VISIT); + public void addCustomTrackingParameter(@NonNull String key, @NonNull Object value) { + if (customTrackingParameters == null) { + customTrackingParameters = new LinkedHashMap<>(); + } + customTrackingParameters.computeIfAbsent(key, k -> new ArrayList<>()).add(value); } /** - * If set to true, will force a new visit to be created for this action. - * - * @param newVisit if this request will force a new visit + * Removes all custom tracking parameters. */ - public void setNewVisit(Boolean newVisit) { - setBooleanParameter(NEW_VISIT, newVisit); + public void clearCustomTrackingParameter() { + customTrackingParameters.clear(); } /** - * Get the outlink url + * Sets idgoal=0 in the request to track an ecommerce interaction: + * cart update or an ecommerce order. * - * @return the outlink url + * @deprecated Please use {@link MatomoRequest#setGoalId(Integer)} instead */ - @Nullable - public URL getOutlinkUrl() { - return castToUrlOrNull(OUTLINK_URL); + @Deprecated + public void enableEcommerce() { + setGoalId(0); } /** - * Get the outlink url + * Get the {@link EcommerceItem} at the specified index. * - * @return the outlink url + * @param index the index of the {@link EcommerceItem} to return + * @return the {@link EcommerceItem} at the specified index */ @Nullable - public String getOutlinkUrlAsString() { - return castOrNull(OUTLINK_URL); - } - - /** - * Set an external URL the user has opened. Used for tracking outlink clicks. - * We recommend to also set the url parameter to this same value. - * - * @param outlinkUrl the outlink url to set. A null value will remove this parameter - * @deprecated Please use {@link #setOutlinkUrl(String)} - */ - @Deprecated - public void setOutlinkUrl(@NonNull URL outlinkUrl) { - setOutlinkUrl(outlinkUrl.toString()); + public EcommerceItem getEcommerceItem(int index) { + if (ecommerceItems == null || ecommerceItems.isEmpty()) { + return null; + } + return ecommerceItems.get(index); } - /** - * Set an external URL the user has opened. Used for tracking outlink clicks. - * We recommend to also set the url parameter to this same value. + * Add an {@link EcommerceItem} to this order. Ecommerce must be enabled, + * and EcommerceId and EcommerceRevenue must first be set. * - * @param outlinkUrl the outlink url to set. A null value will remove this parameter + * @param item the {@link EcommerceItem} to add. Cannot be null */ - public void setOutlinkUrl(String outlinkUrl) { - setParameter(OUTLINK_URL, outlinkUrl); + public void addEcommerceItem(@NonNull EcommerceItem item) { + if (ecommerceItems == null) { + ecommerceItems = new EcommerceItems(); + } + ecommerceItems.add(item); } /** - * Set an external URL the user has opened. Used for tracking outlink clicks. - * We recommend to also set the url parameter to this same value. - * - * @param outlinkUrl the outlink url to set. A null value will remove this parameter - * @deprecated Please use {@link #setOutlinkUrl(String)} + * Clears all {@link EcommerceItem} from this order. */ - @Deprecated - public void setOutlinkUrlWithString(String outlinkUrl) { - setOutlinkUrl(outlinkUrl); + public void clearEcommerceItems() { + ecommerceItems.clear(); } /** @@ -1164,7 +721,10 @@ public void setOutlinkUrlWithString(String outlinkUrl) { @Nullable @Deprecated public String getPageCustomVariable(String key) { - return getCustomVariable(PAGE_CUSTOM_VARIABLE, key); + if (pageCustomVariables == null) { + return null; + } + return pageCustomVariables.get(key); } /** @@ -1172,10 +732,20 @@ public String getPageCustomVariable(String key) { * * @param index the index of the variable to get. Must be greater than 0 * @return the variable at the specified key, null if nothing at this index + * @deprecated Use {@link MatomoRequest#getPageCustomVariables()} instead */ + @Deprecated @Nullable public CustomVariable getPageCustomVariable(int index) { - return getCustomVariable(PAGE_CUSTOM_VARIABLE, index); + return getCustomVariable(pageCustomVariables, index); + } + + @Nullable + private static CustomVariable getCustomVariable(CustomVariables customVariables, int index) { + if (customVariables == null) { + return null; + } + return customVariables.get(index); } /** @@ -1184,989 +754,161 @@ public CustomVariable getPageCustomVariable(int index) { * * @param key the key of the variable to set * @param value the value of the variable to set at the specified key. A null value will remove this custom variable - * @deprecated Use the {@link #setPageCustomVariable(CustomVariable, int)} method instead. + * @deprecated Use {@link MatomoRequest#getPageCustomVariables()} instead */ @Deprecated - public void setPageCustomVariable(String key, String value) { + public void setPageCustomVariable(@NotNull String key, @Nullable String value) { + requireNonNull(key, "Key must not be null"); if (value == null) { - removeCustomVariable(PAGE_CUSTOM_VARIABLE, key); + if (pageCustomVariables == null) { + return; + } + pageCustomVariables.remove(key); } else { - setCustomVariable(PAGE_CUSTOM_VARIABLE, new CustomVariable(key, value), null); + CustomVariable variable = new CustomVariable(key, value); + if (pageCustomVariables == null) { + pageCustomVariables = new CustomVariables(); + } + pageCustomVariables.add(variable); } } /** * Set a page custom variable at the specified index. * - * @param customVariable the CustomVariable to set. A null value will remove the CustomVariable at the specified index + * @param customVariable the CustomVariable to set. A null value will remove the CustomVariable at the specified + * index * @param index the index of he CustomVariable to set + * @deprecated Use {@link #getPageCustomVariables()} instead */ - public void setPageCustomVariable(CustomVariable customVariable, int index) { - setCustomVariable(PAGE_CUSTOM_VARIABLE, customVariable, index); - } - - /** - * Check if the visitor has the Director plugin. - * - * @return true if visitor has the Director plugin - */ - @Nullable - public Boolean getPluginDirector() { - return getBooleanParameter(PLUGIN_DIRECTOR); - } - - /** - * Set if the visitor has the Director plugin. - * - * @param director true if the visitor has the Director plugin - */ - public void setPluginDirector(Boolean director) { - setBooleanParameter(PLUGIN_DIRECTOR, director); - } - - /** - * Check if the visitor has the Flash plugin. - * - * @return true if the visitor has the Flash plugin - */ - @Nullable - public Boolean getPluginFlash() { - return getBooleanParameter(PLUGIN_FLASH); - } - - /** - * Set if the visitor has the Flash plugin. - * - * @param flash true if the visitor has the Flash plugin - */ - @Nullable - public void setPluginFlash(Boolean flash) { - setBooleanParameter(PLUGIN_FLASH, flash); - } - - /** - * Check if the visitor has the Gears plugin. - * - * @return true if the visitor has the Gears plugin - */ - @Nullable - public Boolean getPluginGears() { - return getBooleanParameter(PLUGIN_GEARS); - } - - /** - * Set if the visitor has the Gears plugin. - * - * @param gears true if the visitor has the Gears plugin - */ - public void setPluginGears(Boolean gears) { - setBooleanParameter(PLUGIN_GEARS, gears); - } - - /** - * Check if the visitor has the Java plugin. - * - * @return true if the visitor has the Java plugin - */ - @Nullable - public Boolean getPluginJava() { - return getBooleanParameter(PLUGIN_JAVA); + @Deprecated + public void setPageCustomVariable(@Nullable CustomVariable customVariable, int index) { + if (pageCustomVariables == null) { + if (customVariable == null) { + return; + } + pageCustomVariables = new CustomVariables(); + } + setCustomVariable(pageCustomVariables, customVariable, index); } - /** - * Set if the visitor has the Java plugin. - * - * @param java true if the visitor has the Java plugin - */ - public void setPluginJava(Boolean java) { - setBooleanParameter(PLUGIN_JAVA, java); + private static void setCustomVariable( + CustomVariables customVariables, @Nullable CustomVariable customVariable, + int index + ) { + if (customVariable == null) { + customVariables.remove(index); + } else { + customVariables.add(customVariable, index); + } } /** - * Check if the visitor has the PDF plugin. + * Get the datetime of the request. * - * @return true if the visitor has the PDF plugin + * @return the datetime of the request + * @deprecated Use {@link #getRequestTimestamp()} instead */ + @Deprecated @Nullable - public Boolean getPluginPDF() { - return getBooleanParameter(PLUGIN_PDF); - } - - /** - * Set if the visitor has the PDF plugin. - * - * @param pdf true if the visitor has the PDF plugin - */ - public void setPluginPDF(Boolean pdf) { - setBooleanParameter(PLUGIN_PDF, pdf); + public MatomoDate getRequestDatetime() { + return requestTimestamp == null ? null : new MatomoDate(requestTimestamp.toEpochMilli()); } /** - * Check if the visitor has the Quicktime plugin. + * Set the datetime of the request (normally the current time is used). + * This can be used to record visits and page views in the past. The datetime + * must be sent in UTC timezone. Note: if you record data in the past, you will + * need to force Matomo to re-process + * reports for the past dates. If you set the Request Datetime to a datetime + * older than four hours then Auth Token must be set. If you set + * Request Datetime with a datetime in the last four hours then you + * don't need to pass Auth Token. * - * @return true if the visitor has the Quicktime plugin + * @param matomoDate the datetime of the request to set. A null value will remove this parameter + * @deprecated Use {@link #setRequestTimestamp(Instant)} instead */ - @Nullable - public Boolean getPluginQuicktime() { - return getBooleanParameter(PLUGIN_QUICKTIME); + @Deprecated + public void setRequestDatetime(MatomoDate matomoDate) { + if (matomoDate == null) { + requestTimestamp = null; + } else { + setRequestTimestamp(matomoDate.getZonedDateTime().toInstant()); + } } - /** - * Set if the visitor has the Quicktime plugin. - * - * @param quicktime true if the visitor has the Quicktime plugin - */ - public void setPluginQuicktime(Boolean quicktime) { - setBooleanParameter(PLUGIN_QUICKTIME, quicktime); - } /** - * Check if the visitor has the RealPlayer plugin. + * Get the visit custom variable at the specified key. * - * @return true if the visitor has the RealPlayer plugin + * @param key the key of the variable to get + * @return the variable at the specified key, null if key is not present + * @deprecated Use the {@link #getVisitCustomVariable(int)} method instead. */ @Nullable - public Boolean getPluginRealPlayer() { - return getBooleanParameter(PLUGIN_REAL_PLAYER); - } - - /** - * Set if the visitor has the RealPlayer plugin. - * - * @param realPlayer true if the visitor has the RealPlayer plugin - */ - public void setPluginRealPlayer(Boolean realPlayer) { - setBooleanParameter(PLUGIN_REAL_PLAYER, realPlayer); + @Deprecated + public String getUserCustomVariable(String key) { + if (visitCustomVariables == null) { + return null; + } + return visitCustomVariables.get(key); } /** - * Check if the visitor has the Silverlight plugin. + * Get the visit custom variable at the specified index. * - * @return true if the visitor has the Silverlight plugin + * @param index the index of the variable to get + * @return the variable at the specified index, null if nothing at this index + * @deprecated Use {@link #getVisitCustomVariables()} instead */ @Nullable - public Boolean getPluginSilverlight() { - return getBooleanParameter(PLUGIN_SILVERLIGHT); + @Deprecated + public CustomVariable getVisitCustomVariable(int index) { + return getCustomVariable(visitCustomVariables, index); } /** - * Set if the visitor has the Silverlight plugin. + * Set a visit custom variable with the specified key and value at the first available index. + * All visit custom variables with this key will be overwritten or deleted * - * @param silverlight true if the visitor has the Silverlight plugin + * @param key the key of the variable to set + * @param value the value of the variable to set at the specified key. A null value will remove this parameter + * @deprecated Use {@link #setVisitCustomVariables(CustomVariables)} instead */ - public void setPluginSilverlight(Boolean silverlight) { - setBooleanParameter(PLUGIN_SILVERLIGHT, silverlight); + @Deprecated + public void setUserCustomVariable(@NotNull String key, @Nullable String value) { + requireNonNull(key, "Key must not be null"); + if (value == null) { + if (visitCustomVariables == null) { + return; + } + visitCustomVariables.remove(key); + } else { + CustomVariable variable = new CustomVariable(key, value); + if (visitCustomVariables == null) { + visitCustomVariables = new CustomVariables(); + } + visitCustomVariables.add(variable); + } } /** - * Check if the visitor has the Windows Media plugin. + * Set a user custom variable at the specified key. * - * @return true if the visitor has the Windows Media plugin + * @param customVariable the CustomVariable to set. A null value will remove the custom variable at the specified + * index + * @param index the index to set the customVariable at. + * @deprecated Use {@link #setVisitCustomVariables(CustomVariables)} instead */ - @Nullable - public Boolean getPluginWindowsMedia() { - return getBooleanParameter(PLUGIN_WINDOWS_MEDIA); - } - - /** - * Set if the visitor has the Windows Media plugin. - * - * @param windowsMedia true if the visitor has the Windows Media plugin - */ - public void setPluginWindowsMedia(Boolean windowsMedia) { - setBooleanParameter(PLUGIN_WINDOWS_MEDIA, windowsMedia); - } - - /** - * Get the random value for this request - * - * @return the random value - */ - @Nullable - public String getRandomValue() { - return castOrNull(RANDOM_VALUE); - } - - /** - * Set a random value that is generated before each request. Using it helps - * avoid the tracking request being cached by the browser or a proxy. - * - * @param randomValue the random value to set. A null value will remove this parameter - */ - public void setRandomValue(String randomValue) { - setParameter(RANDOM_VALUE, randomValue); - } - - /** - * Get the referrer url - * - * @return the referrer url - */ - @Nullable - public URL getReferrerUrl() { - return castToUrlOrNull(REFERRER_URL); - } - - /** - * Get the referrer url - * - * @return the referrer url - */ - @Nullable - public String getReferrerUrlAsString() { - return castOrNull(REFERRER_URL); - } - - /** - * Set the full HTTP Referrer URL. This value is used to determine how someone - * got to your website (ie, through a website, search engine or campaign). - * - * @param referrerUrl the referrer url to set. A null value will remove this parameter - * @deprecated Please use {@link #setReferrerUrl(String)} - */ - @Deprecated - public void setReferrerUrl(@NonNull URL referrerUrl) { - setReferrerUrl(referrerUrl.toString()); - } - - /** - * Set the full HTTP Referrer URL. This value is used to determine how someone - * got to your website (ie, through a website, search engine or campaign). - * - * @param referrerUrl the referrer url to set. A null value will remove this parameter - */ - public void setReferrerUrl(String referrerUrl) { - setParameter(REFERRER_URL, referrerUrl); - } - - /** - * Set the full HTTP Referrer URL. This value is used to determine how someone - * got to your website (ie, through a website, search engine or campaign). - * - * @param referrerUrl the referrer url to set. A null value will remove this parameter - * @deprecated Please use {@link #setReferrerUrl(String)} - */ - @Deprecated - public void setReferrerUrlWithString(String referrerUrl) { - setReferrerUrl(referrerUrl); - } - - /** - * Get the datetime of the request - * - * @return the datetime of the request - */ - @Nullable - public MatomoDate getRequestDatetime() { - return castOrNull(REQUEST_DATETIME); - } - - /** - * Set the datetime of the request (normally the current time is used). - * This can be used to record visits and page views in the past. The datetime - * must be sent in UTC timezone. Note: if you record data in the past, you will - * need to force Matomo to re-process - * reports for the past dates. If you set the Request Datetime to a datetime - * older than four hours then Auth Token must be set. If you set - * Request Datetime with a datetime in the last four hours then you - * don't need to pass Auth Token. - * - * @param datetime the datetime of the request to set. A null value will remove this parameter - */ - public void setRequestDatetime(MatomoDate datetime) { - if (datetime != null && new Date().getTime() - datetime.getTime() > REQUEST_DATETIME_AUTH_LIMIT && getAuthToken() == null) { - throw new IllegalStateException("Because you are trying to set RequestDatetime for a time greater than 4 hours ago, AuthToken must be set first."); - } - setParameter(REQUEST_DATETIME, datetime); - } - - /** - * Get if this request will be tracked. - * - * @return true if request will be tracked - */ - @Nullable - public Boolean getRequired() { - return getBooleanParameter(REQUIRED); - } - - /** - * Set if this request will be tracked by the Matomo server. - * - * @param required true if request will be tracked - */ - public void setRequired(Boolean required) { - setBooleanParameter(REQUIRED, required); - } - - /** - * Get if the response will be an image. - * - * @return true if the response will be an an image - */ - @Nullable - public Boolean getResponseAsImage() { - return getBooleanParameter(RESPONSE_AS_IMAGE); - } - - /** - * Set if the response will be an image. If set to false, Matomo will respond - * with a HTTP 204 response code instead of a GIF image. This improves performance - * and can fix errors if images are not allowed to be obtained directly - * (eg Chrome Apps). Available since Matomo 2.10.0. - * - * @param responseAsImage true if the response will be an image - */ - public void setResponseAsImage(Boolean responseAsImage) { - setBooleanParameter(RESPONSE_AS_IMAGE, responseAsImage); - } - - /** - * Get the search category - * - * @return the search category - */ - @Nullable - public String getSearchCategory() { - return castOrNull(SEARCH_CATEGORY); - } - - /** - * Specify a search category with this parameter. SearchQuery must first be - * set. - * - * @param searchCategory the search category to set. A null value will remove this parameter - */ - public void setSearchCategory(String searchCategory) { - if (searchCategory != null && getSearchQuery() == null) { - throw new IllegalStateException("SearchQuery must be set before SearchCategory can be set."); - } - setParameter(SEARCH_CATEGORY, searchCategory); - } - - /** - * Get the search query. - * - * @return the search query - */ - @Nullable - public String getSearchQuery() { - return castOrNull(SEARCH_QUERY); - } - - /** - * Set the search query. When specified, the request will not be tracked as - * a normal pageview but will instead be tracked as a Site Search request. - * - * @param searchQuery the search query to set. A null value will remove this parameter - */ - public void setSearchQuery(String searchQuery) { - setParameter(SEARCH_QUERY, searchQuery); - } - - /** - * Get the search results count. - * - * @return the search results count - */ - @Nullable - public Long getSearchResultsCount() { - return castOrNull(SEARCH_RESULTS_COUNT); - } - - /** - * We recommend to set the - * search count to the number of search results displayed on the results page. - * When keywords are tracked with {@code Search Results Count=0} they will appear in - * the "No Result Search Keyword" report. SearchQuery must first be set. - * - * @param searchResultsCount the search results count to set. A null value will remove this parameter - */ - public void setSearchResultsCount(Long searchResultsCount) { - if (searchResultsCount != null && getSearchQuery() == null) { - throw new IllegalStateException("SearchQuery must be set before SearchResultsCount can be set."); - } - setParameter(SEARCH_RESULTS_COUNT, searchResultsCount); - } - - /** - * Get the id of the website we're tracking. - * - * @return the id of the website - */ - @Nullable - public Integer getSiteId() { - return castOrNull(SITE_ID); - } - - /** - * Set the ID of the website we're tracking a visit/action for. - * - * @param siteId the id of the website to set. A null value will remove this parameter - */ - public void setSiteId(Integer siteId) { - setParameter(SITE_ID, siteId); - } - - /** - * Set if bot requests should be tracked - * - * @return true if bot requests should be tracked - */ - @Nullable - public Boolean getTrackBotRequests() { - return getBooleanParameter(TRACK_BOT_REQUESTS); - } - - /** - * By default Matomo does not track bots. If you use the Tracking Java API, - * you may be interested in tracking bot requests. To enable Bot Tracking in - * Matomo, set Track Bot Requests to true. - * - * @param trackBotRequests true if bot requests should be tracked - */ - public void setTrackBotRequests(Boolean trackBotRequests) { - setBooleanParameter(TRACK_BOT_REQUESTS, trackBotRequests); - } - - /** - * Get the visit custom variable at the specified key. - * - * @param key the key of the variable to get - * @return the variable at the specified key, null if key is not present - * @deprecated Use the {@link #getVisitCustomVariable(int)} method instead. - */ - @Nullable - @Deprecated - public String getUserCustomVariable(String key) { - return getCustomVariable(VISIT_CUSTOM_VARIABLE, key); - } - - /** - * Get the visit custom variable at the specified index. - * - * @param index the index of the variable to get - * @return the variable at the specified index, null if nothing at this index - */ - @Nullable - public CustomVariable getVisitCustomVariable(int index) { - return getCustomVariable(VISIT_CUSTOM_VARIABLE, index); - } - - /** - * Set a visit custom variable with the specified key and value at the first available index. - * All visit custom variables with this key will be overwritten or deleted - * - * @param key the key of the variable to set - * @param value the value of the variable to set at the specified key. A null value will remove this parameter - * @deprecated Use the {@link #setVisitCustomVariable(CustomVariable, int)} method instead. - */ - @Deprecated - public void setUserCustomVariable(String key, String value) { - if (value == null) { - removeCustomVariable(VISIT_CUSTOM_VARIABLE, key); - } else { - setCustomVariable(VISIT_CUSTOM_VARIABLE, new CustomVariable(key, value), null); - } - } - - /** - * Set a user custom variable at the specified key. - * - * @param customVariable the CustomVariable to set. A null value will remove the custom variable at the specified index - * @param index the index to set the customVariable at. - */ - public void setVisitCustomVariable(CustomVariable customVariable, int index) { - setCustomVariable(VISIT_CUSTOM_VARIABLE, customVariable, index); - } - - /** - * Get the user id for this request. - * - * @return the user id - */ - @Nullable - public String getUserId() { - return castOrNull(USER_ID); - } - - /** - * Set the user id for this request. - * User id is any non empty unique string identifying the user (such as an email - * address or a username). To access this value, users must be logged-in in your - * system so you can fetch this user id from your system, and pass it to Matomo. - * The user id appears in the visitor log, the Visitor profile, and you can - * Segment - * reports for one or several user ids. When specified, the user id will be - * "enforced". This means that if there is no recent visit with this user id, - * a new one will be created. If a visit is found in the last 30 minutes with - * your specified user id, then the new action will be recorded to this existing visit. - * - * @param userId the user id to set. A null value will remove this parameter - */ - public void setUserId(String userId) { - setNonEmptyStringParameter(USER_ID, userId); - } - - /** - * Get the visitor's city. - * - * @return the visitor's city - */ - @Nullable - public String getVisitorCity() { - return castOrNull(VISITOR_CITY); - } - - /** - * Set an override value for the city. The name of the city the visitor is - * located in, eg, Tokyo. AuthToken must first be set. - * - * @param city the visitor's city to set. A null value will remove this parameter - */ - public void setVisitorCity(String city) { - if (city != null) { - verifyAuthTokenSet(); - } - setParameter(VISITOR_CITY, city); - } - - /** - * Get the visitor's country. - * - * @return the visitor's country - */ - @Nullable - public MatomoLocale getVisitorCountry() { - return castOrNull(VISITOR_COUNTRY); - } - - /** - * Set an override value for the country. AuthToken must first be set. - * - * @param country the visitor's country to set. A null value will remove this parameter - */ - public void setVisitorCountry(MatomoLocale country) { - if (country != null) { - verifyAuthTokenSet(); - } - setParameter(VISITOR_COUNTRY, country); - } - - /** - * Get the visitor's custom id. - * - * @return the visitor's custom id - */ - @Nullable - public String getVisitorCustomId() { - return castOrNull(VISITOR_CUSTOM_ID); - } - - /** - * Set a custom visitor ID for this request. You must set this value to exactly - * a {@value #ID_LENGTH} character hexadecimal string (containing only characters 01234567890abcdefABCDEF). - * We recommended to set the UserId rather than the VisitorCustomId. - * - * @param visitorCustomId the visitor's custom id to set. A null value will remove this parameter - */ - public void setVisitorCustomId(String visitorCustomId) { - if (visitorCustomId != null) { - if (visitorCustomId.length() != ID_LENGTH) { - throw new IllegalArgumentException(visitorCustomId + " is not " + ID_LENGTH + " characters long."); - } - // Verify visitorID is a 16 character hexadecimal string - if (!VISITOR_ID_PATTERN.matcher(visitorCustomId).matches()) { - throw new IllegalArgumentException(visitorCustomId + " is not a hexadecimal string."); - } - } - setParameter(VISITOR_CUSTOM_ID, visitorCustomId); - } - - /** - * Get the timestamp of the visitor's first visit. - * - * @return the timestamp of the visitor's first visit - */ - @Nullable - public Long getVisitorFirstVisitTimestamp() { - return castOrNull(VISITOR_FIRST_VISIT_TIMESTAMP); - } - - /** - * Set the UNIX timestamp of this visitor's first visit. This could be set - * to the date where the user first started using your software/app, or when - * he/she created an account. This parameter is used to populate the - * Goals > Days to Conversion report. - * - * @param timestamp the timestamp of the visitor's first visit to set. A null value will remove this parameter - */ - public void setVisitorFirstVisitTimestamp(Long timestamp) { - setParameter(VISITOR_FIRST_VISIT_TIMESTAMP, timestamp); - } - - /** - * Get the visitor's id. - * - * @return the visitor's id - */ - @Nullable - public String getVisitorId() { - return castOrNull(VISITOR_ID); - } - - /** - * Set the unique visitor ID, must be a {@value #ID_LENGTH} characters hexadecimal string. - * Every unique visitor must be assigned a different ID and this ID must not - * change after it is assigned. If this value is not set Matomo will still - * track visits, but the unique visitors metric might be less accurate. - * - * @param visitorId the visitor id to set. A null value will remove this parameter - */ - public void setVisitorId(String visitorId) { - if (visitorId != null) { - if (visitorId.length() != ID_LENGTH) { - throw new IllegalArgumentException(visitorId + " is not " + ID_LENGTH + " characters long."); - } - // Verify visitorID is a 16 character hexadecimal string - if (!VISITOR_ID_PATTERN.matcher(visitorId).matches()) { - throw new IllegalArgumentException(visitorId + " is not a hexadecimal string."); - } - } - setParameter(VISITOR_ID, visitorId); - } - - /** - * Get the visitor's ip. - * - * @return the visitor's ip - */ - @Nullable - public String getVisitorIp() { - return castOrNull(VISITOR_IP); - } - - /** - * Set the override value for the visitor IP (both IPv4 and IPv6 notations - * supported). AuthToken must first be set. - * - * @param visitorIp the visitor's ip to set. A null value will remove this parameter - */ - public void setVisitorIp(String visitorIp) { - if (visitorIp != null) { - verifyAuthTokenSet(); - } - setParameter(VISITOR_IP, visitorIp); - } - - /** - * Get the visitor's latitude. - * - * @return the visitor's latitude - */ - @Nullable - public Double getVisitorLatitude() { - return castOrNull(VISITOR_LATITUDE); - } - - /** - * Set an override value for the visitor's latitude, eg 22.456. AuthToken - * must first be set. - * - * @param latitude the visitor's latitude to set. A null value will remove this parameter - */ - public void setVisitorLatitude(Double latitude) { - if (latitude != null) { - verifyAuthTokenSet(); - } - setParameter(VISITOR_LATITUDE, latitude); - } - - /** - * Get the visitor's longitude. - * - * @return the visitor's longitude - */ - @Nullable - public Double getVisitorLongitude() { - return castOrNull(VISITOR_LONGITUDE); - } - - /** - * Set an override value for the visitor's longitude, eg 22.456. AuthToken - * must first be set. - * - * @param longitude the visitor's longitude to set. A null value will remove this parameter - */ - public void setVisitorLongitude(Double longitude) { - if (longitude != null) { - verifyAuthTokenSet(); - } - setParameter(VISITOR_LONGITUDE, longitude); - } - - /** - * Get the timestamp of the visitor's previous visit. - * - * @return the timestamp of the visitor's previous visit - */ - @Nullable - public Long getVisitorPreviousVisitTimestamp() { - return castOrNull(VISITOR_PREVIOUS_VISIT_TIMESTAMP); - } - - /** - * Set the UNIX timestamp of this visitor's previous visit. This parameter - * is used to populate the report - * Visitors > Engagement > Visits by days since last visit. - * - * @param timestamp the timestamp of the visitor's previous visit to set. A null value will remove this parameter - */ - public void setVisitorPreviousVisitTimestamp(Long timestamp) { - setParameter(VISITOR_PREVIOUS_VISIT_TIMESTAMP, timestamp); - } - - /** - * Get the visitor's region. - * - * @return the visitor's region - */ - @Nullable - public String getVisitorRegion() { - return castOrNull(VISITOR_REGION); - } - - /** - * Set an override value for the region. Should be set to the two letter - * region code as defined by - * MaxMind's GeoIP databases. - * See here - * for a list of them for every country (the region codes are located in the - * second column, to the left of the region name and to the right of the country - * code). - * - * @param region the visitor's region to set. A null value will remove this parameter - */ - public void setVisitorRegion(String region) { - if (region != null) { - verifyAuthTokenSet(); - } - setParameter(VISITOR_REGION, region); - } - - /** - * Get the count of visits for this visitor. - * - * @return the count of visits for this visitor - */ - @Nullable - public Integer getVisitorVisitCount() { - return castOrNull(VISITOR_VISIT_COUNT); - } - - /** - * Set the current count of visits for this visitor. To set this value correctly, - * it would be required to store the value for each visitor in your application - * (using sessions or persisting in a database). Then you would manually increment - * the counts by one on each new visit or "session", depending on how you choose - * to define a visit. This value is used to populate the report - * Visitors > Engagement > Visits by visit number. - * - * @param visitorVisitCount the count of visits for this visitor to set. A null value will remove this parameter - */ - public void setVisitorVisitCount(Integer visitorVisitCount) { - setParameter(VISITOR_VISIT_COUNT, visitorVisitCount); - } - - public Map> getParameters() { - return parameters.asMap(); - } - - /** - * Get the query string represented by this object. - * - * @return the query string represented by this object - * @deprecated Use {@link URIBuilder} in conjunction with {@link #getParameters()} and {@link QueryParameters#fromMap(Map)} ()} instead - */ - @Nonnull @Deprecated - public String getQueryString() { - return parameters.entries().stream().map(parameter -> parameter.getKey() + '=' + parameter.getValue().toString()).collect(Collectors.joining("&")); - } - - /** - * Get the url encoded query string represented by this object. - * - * @return the url encoded query string represented by this object - * @deprecated Use {@link URIBuilder} in conjunction with {@link #getParameters()} and {@link QueryParameters#fromMap(Map)} ()} instead - */ - @Nonnull - @Deprecated - public String getUrlEncodedQueryString() { - String queryString = new URIBuilder().setParameters(QueryParameters.fromMap(getParameters())).toString(); - if (queryString.isEmpty()) { - return ""; - } - return queryString.substring(1); - } - - /** - * Get a random hexadecimal string of a specified length. - * - * @param length length of the string to produce - * @return a random string consisting only of hexadecimal characters - */ - @Nonnull - public static String getRandomHexString(int length) { - byte[] bytes = new byte[length / 2]; - new SecureRandom().nextBytes(bytes); - return BaseEncoding.base16().lowerCase().encode(bytes); - } - - /** - * Set a stored parameter. - * - * @param key the parameter's key - * @param value the parameter's value. Removes the parameter if null - */ - public void setParameter(@NonNull String key, @Nullable Object value) { - parameters.removeAll(key); - if (value != null) { - addParameter(key, value); - } - } - - /** - * Add more values to the given parameter - * - * @param key the parameter's key. Must not be null - * @param value the parameter's value. Must not be null - */ - public void addParameter(@NonNull String key, @NonNull Object value) { - parameters.put(key, value); - } - - - /** - * Get a stored parameter that and cast it if present - * - * @param key the parameter's key. Must not be null - * @return the stored parameter's value casted to the requested type or null if no value is present - */ - @Nullable - private T castOrNull(@NonNull String key) { - Collection values = parameters.get(key); - if (values.isEmpty()) { - return null; - } - return (T) values.iterator().next(); - } - - /** - * Set a stored parameter and verify it is a non-empty string. - * - * @param key the parameter's key - * @param value the parameter's value. Cannot be the empty. Removes the parameter if null - * string - */ - private void setNonEmptyStringParameter(@NonNull String key, String value) { - if (value != null && value.trim().isEmpty()) { - throw new IllegalArgumentException("Value cannot be empty."); - } - setParameter(key, value); - } - - /** - * Get a stored parameter that is a boolean. - * - * @param key the parameter's key - * @return the stored parameter's value - */ - @Nullable - private Boolean getBooleanParameter(@NonNull String key) { - MatomoBoolean matomoBoolean = castOrNull(key); - if (matomoBoolean == null) { - return null; - } - return matomoBoolean.isValue(); - } - - /** - * Set a stored parameter that is a boolean. - * - * @param key the parameter's key - * @param value the parameter's value. Removes the parameter if null - */ - private void setBooleanParameter(@NonNull String key, @Nullable Boolean value) { - if (value == null) { - setParameter(key, null); - } else { - setParameter(key, new MatomoBoolean(value)); - } - } - - /** - * Get a value that is stored in a json object at the specified parameter. - * - * @param parameter the parameter to retrieve the json object from - * @param index the index of the value. - * @return the value at the specified index - */ - @Nullable - private CustomVariable getCustomVariable(@NonNull String parameter, int index) { - CustomVariables customVariables = castOrNull(parameter); - if (customVariables == null) { - return null; - } - return customVariables.get(index); - } - - @Nullable - private String getCustomVariable(@NonNull String parameter, @NonNull String key) { - CustomVariables customVariables = castOrNull(parameter); - if (customVariables == null) { - return null; - } - return customVariables.get(key); - } - - /** - * Store a value in a json object at the specified parameter. - * - * @param parameter the parameter to store the json object at - * @param customVariable the value. Removes the parameter if null - * @param index the custom variable index - */ - private void setCustomVariable(@NonNull String parameter, @Nullable CustomVariable customVariable, Integer index) { - - if (customVariable == null && index == null) { - throw new IllegalArgumentException("Either custom variable or index must be set"); - } - CustomVariables customVariables = castOrNull(parameter); - if (customVariables == null) { - customVariables = new CustomVariables(); - setParameter(parameter, customVariables); - } - if (customVariable == null) { - customVariables.remove(index); - if (customVariables.isEmpty()) { - setParameter(parameter, null); - } - } else if (index == null) { - customVariables.add(customVariable); - } else { - customVariables.add(customVariable, index); - } - } - - private void removeCustomVariable(@NonNull String parameter, @NonNull String key) { - CustomVariables customVariables = castOrNull(parameter); - if (customVariables != null) { - customVariables.remove(key); - if (customVariables.isEmpty()) { - setParameter(parameter, null); + public void setVisitCustomVariable(@Nullable CustomVariable customVariable, int index) { + if (visitCustomVariables == null) { + if (customVariable == null) { + return; } + visitCustomVariables = new CustomVariables(); } + setCustomVariable(visitCustomVariables, customVariable, index); } } diff --git a/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java b/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java deleted file mode 100644 index 747791a4..00000000 --- a/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java +++ /dev/null @@ -1,643 +0,0 @@ -package org.matomo.java.tracking; - -import java.nio.charset.Charset; -import java.util.List; -import java.util.Map; - -public class MatomoRequestBuilder { - - private int siteId; - - private String actionUrl; - - private String actionName; - private Long actionTime; - private String apiVersion; - private String authToken; - private String campaignKeyword; - private String campaignName; - private Charset characterSet; - private String contentInteraction; - private String contentName; - private String contentPiece; - private String contentTarget; - private Integer currentHour; - private Integer currentMinute; - private Integer currentSecond; - - private Boolean customAction; - private String deviceResolution; - private String downloadUrl; - private Double ecommerceDiscount; - private String ecommerceId; - private Long ecommerceLastOrderTimestamp; - private Double ecommerceRevenue; - private Double ecommerceShippingCost; - private Double ecommerceSubtotal; - private Double ecommerceTax; - private String eventAction; - private String eventCategory; - private String eventName; - private Number eventValue; - private Integer goalId; - private Double goalRevenue; - private String headerAcceptLanguage; - private String headerUserAgent; - private Boolean newVisit; - private String outlinkUrl; - private Boolean pluginDirector; - private Boolean pluginFlash; - private Boolean pluginGears; - private Boolean pluginJava; - private Boolean pluginPDF; - private Boolean pluginQuicktime; - private Boolean pluginRealPlayer; - private Boolean pluginSilverlight; - private Boolean pluginWindowsMedia; - private String randomValue; - private String referrerUrl; - private MatomoDate requestDatetime; - private Boolean required; - private Boolean responseAsImage; - private String searchCategory; - private String searchQuery; - private Long searchResultsCount; - private Boolean trackBotRequests; - private String userId; - private String visitorCity; - private MatomoLocale visitorCountry; - private String visitorCustomId; - private Long visitorFirstVisitTimestamp; - private String visitorId; - private String visitorIp; - private Double visitorLatitude; - private Double visitorLongitude; - private Long visitorPreviousVisitTimestamp; - private String visitorRegion; - private Integer visitorVisitCount; - - private List visitCustomVariables; - - private List pageCustomVariables; - private Map customTrackingParameters; - - public MatomoRequestBuilder siteId(int siteId) { - this.siteId = siteId; - return this; - } - - public MatomoRequestBuilder actionUrl(String actionUrl) { - this.actionUrl = actionUrl; - return this; - } - - public MatomoRequestBuilder actionName(String actionName) { - this.actionName = actionName; - return this; - } - - public MatomoRequestBuilder actionTime(Long actionTime) { - this.actionTime = actionTime; - return this; - } - - public MatomoRequestBuilder apiVersion(String apiVersion) { - this.apiVersion = apiVersion; - return this; - } - - public MatomoRequestBuilder authToken(String authToken) { - this.authToken = authToken; - return this; - } - - public MatomoRequestBuilder campaignKeyword(String campaignKeyword) { - this.campaignKeyword = campaignKeyword; - return this; - } - - public MatomoRequestBuilder campaignName(String campaignName) { - this.campaignName = campaignName; - return this; - } - - public MatomoRequestBuilder characterSet(Charset characterSet) { - this.characterSet = characterSet; - return this; - } - - public MatomoRequestBuilder contentInteraction(String contentInteraction) { - this.contentInteraction = contentInteraction; - return this; - } - - public MatomoRequestBuilder contentName(String contentName) { - this.contentName = contentName; - return this; - } - - public MatomoRequestBuilder contentPiece(String contentPiece) { - this.contentPiece = contentPiece; - return this; - } - - public MatomoRequestBuilder contentTarget(String contentTarget) { - this.contentTarget = contentTarget; - return this; - } - - public MatomoRequestBuilder currentHour(Integer currentHour) { - this.currentHour = currentHour; - return this; - } - - public MatomoRequestBuilder currentMinute(Integer currentMinute) { - this.currentMinute = currentMinute; - return this; - } - - public MatomoRequestBuilder currentSecond(Integer currentSecond) { - this.currentSecond = currentSecond; - return this; - } - - public MatomoRequestBuilder customAction(Boolean customAction) { - this.customAction = customAction; - return this; - } - - public MatomoRequestBuilder deviceResolution(String deviceResolution) { - this.deviceResolution = deviceResolution; - return this; - } - - public MatomoRequestBuilder downloadUrl(String downloadUrl) { - this.downloadUrl = downloadUrl; - return this; - } - - public MatomoRequestBuilder ecommerceDiscount(Double ecommerceDiscount) { - this.ecommerceDiscount = ecommerceDiscount; - return this; - } - - public MatomoRequestBuilder ecommerceId(String ecommerceId) { - this.ecommerceId = ecommerceId; - return this; - } - - public MatomoRequestBuilder ecommerceLastOrderTimestamp(Long ecommerceLastOrderTimestamp) { - this.ecommerceLastOrderTimestamp = ecommerceLastOrderTimestamp; - return this; - } - - public MatomoRequestBuilder ecommerceRevenue(Double ecommerceRevenue) { - this.ecommerceRevenue = ecommerceRevenue; - return this; - } - - public MatomoRequestBuilder ecommerceShippingCost(Double ecommerceShippingCost) { - this.ecommerceShippingCost = ecommerceShippingCost; - return this; - } - - public MatomoRequestBuilder ecommerceSubtotal(Double ecommerceSubtotal) { - this.ecommerceSubtotal = ecommerceSubtotal; - return this; - } - - public MatomoRequestBuilder ecommerceTax(Double ecommerceTax) { - this.ecommerceTax = ecommerceTax; - return this; - } - - public MatomoRequestBuilder eventAction(String eventAction) { - this.eventAction = eventAction; - return this; - } - - public MatomoRequestBuilder eventCategory(String eventCategory) { - this.eventCategory = eventCategory; - return this; - } - - public MatomoRequestBuilder eventName(String eventName) { - this.eventName = eventName; - return this; - } - - public MatomoRequestBuilder eventValue(Number eventValue) { - this.eventValue = eventValue; - return this; - } - - public MatomoRequestBuilder goalId(Integer goalId) { - this.goalId = goalId; - return this; - } - - public MatomoRequestBuilder goalRevenue(Double goalRevenue) { - this.goalRevenue = goalRevenue; - return this; - } - - public MatomoRequestBuilder headerAcceptLanguage(String headerAcceptLanguage) { - this.headerAcceptLanguage = headerAcceptLanguage; - return this; - } - - public MatomoRequestBuilder headerUserAgent(String headerUserAgent) { - this.headerUserAgent = headerUserAgent; - return this; - } - - public MatomoRequestBuilder newVisit(Boolean newVisit) { - this.newVisit = newVisit; - return this; - } - - public MatomoRequestBuilder outlinkUrl(String outlinkUrl) { - this.outlinkUrl = outlinkUrl; - return this; - } - - public MatomoRequestBuilder pluginDirector(Boolean pluginDirector) { - this.pluginDirector = pluginDirector; - return this; - } - - public MatomoRequestBuilder pluginFlash(Boolean pluginFlash) { - this.pluginFlash = pluginFlash; - return this; - } - - public MatomoRequestBuilder pluginGears(Boolean pluginGears) { - this.pluginGears = pluginGears; - return this; - } - - public MatomoRequestBuilder pluginJava(Boolean pluginJava) { - this.pluginJava = pluginJava; - return this; - } - - public MatomoRequestBuilder pluginPDF(Boolean pluginPDF) { - this.pluginPDF = pluginPDF; - return this; - } - - public MatomoRequestBuilder pluginQuicktime(Boolean pluginQuicktime) { - this.pluginQuicktime = pluginQuicktime; - return this; - } - - public MatomoRequestBuilder pluginRealPlayer(Boolean pluginRealPlayer) { - this.pluginRealPlayer = pluginRealPlayer; - return this; - } - - public MatomoRequestBuilder pluginSilverlight(Boolean pluginSilverlight) { - this.pluginSilverlight = pluginSilverlight; - return this; - } - - public MatomoRequestBuilder pluginWindowsMedia(Boolean pluginWindowsMedia) { - this.pluginWindowsMedia = pluginWindowsMedia; - return this; - } - - public MatomoRequestBuilder randomValue(String randomValue) { - this.randomValue = randomValue; - return this; - } - - public MatomoRequestBuilder referrerUrl(String referrerUrl) { - this.referrerUrl = referrerUrl; - return this; - } - - public MatomoRequestBuilder requestDatetime(MatomoDate requestDatetime) { - this.requestDatetime = requestDatetime; - return this; - } - - public MatomoRequestBuilder required(Boolean required) { - this.required = required; - return this; - } - - public MatomoRequestBuilder responseAsImage(Boolean responseAsImage) { - this.responseAsImage = responseAsImage; - return this; - } - - public MatomoRequestBuilder searchCategory(String searchCategory) { - this.searchCategory = searchCategory; - return this; - } - - public MatomoRequestBuilder searchQuery(String searchQuery) { - this.searchQuery = searchQuery; - return this; - } - - public MatomoRequestBuilder searchResultsCount(Long searchResultsCount) { - this.searchResultsCount = searchResultsCount; - return this; - } - - public MatomoRequestBuilder trackBotRequests(Boolean trackBotRequests) { - this.trackBotRequests = trackBotRequests; - return this; - } - - public MatomoRequestBuilder userId(String userId) { - this.userId = userId; - return this; - } - - public MatomoRequestBuilder visitorCity(String visitorCity) { - this.visitorCity = visitorCity; - return this; - } - - public MatomoRequestBuilder visitorCountry(MatomoLocale visitorCountry) { - this.visitorCountry = visitorCountry; - return this; - } - - public MatomoRequestBuilder visitorCustomId(String visitorCustomId) { - this.visitorCustomId = visitorCustomId; - return this; - } - - public MatomoRequestBuilder visitorFirstVisitTimestamp(Long visitorFirstVisitTimestamp) { - this.visitorFirstVisitTimestamp = visitorFirstVisitTimestamp; - return this; - } - - public MatomoRequestBuilder visitorId(String visitorId) { - this.visitorId = visitorId; - return this; - } - - public MatomoRequestBuilder visitorIp(String visitorIp) { - this.visitorIp = visitorIp; - return this; - } - - public MatomoRequestBuilder visitorLatitude(Double visitorLatitude) { - this.visitorLatitude = visitorLatitude; - return this; - } - - public MatomoRequestBuilder visitorLongitude(Double visitorLongitude) { - this.visitorLongitude = visitorLongitude; - return this; - } - - public MatomoRequestBuilder visitorPreviousVisitTimestamp(Long visitorPreviousVisitTimestamp) { - this.visitorPreviousVisitTimestamp = visitorPreviousVisitTimestamp; - return this; - } - - public MatomoRequestBuilder visitorRegion(String visitorRegion) { - this.visitorRegion = visitorRegion; - return this; - } - - public MatomoRequestBuilder visitorVisitCount(Integer visitorVisitCount) { - this.visitorVisitCount = visitorVisitCount; - return this; - } - - public MatomoRequestBuilder visitCustomVariables(List visitCustomVariables) { - this.visitCustomVariables = visitCustomVariables; - return this; - } - - public MatomoRequestBuilder pageCustomVariables(List pageCustomVariables) { - this.pageCustomVariables = pageCustomVariables; - return this; - } - - public MatomoRequestBuilder customTrackingParameters(Map customTrackingParameters) { - this.customTrackingParameters = customTrackingParameters; - return this; - } - - public MatomoRequest build() { - MatomoRequest matomoRequest = new MatomoRequest(siteId, actionUrl); - if (actionName != null) { - matomoRequest.setActionName(actionName); - } - if (actionTime != null) { - matomoRequest.setActionTime(actionTime); - } - if (apiVersion != null) { - matomoRequest.setApiVersion(apiVersion); - } - if (authToken != null) { - matomoRequest.setAuthToken(authToken); - } - if (campaignKeyword != null) { - matomoRequest.setCampaignKeyword(campaignKeyword); - } - if (campaignName != null) { - matomoRequest.setCampaignName(campaignName); - } - if (characterSet != null) { - matomoRequest.setCharacterSet(characterSet); - } - if (contentInteraction != null) { - matomoRequest.setContentInteraction(contentInteraction); - } - if (contentName != null) { - matomoRequest.setContentName(contentName); - } - if (contentPiece != null) { - matomoRequest.setContentPiece(contentPiece); - } - if (contentTarget != null) { - matomoRequest.setContentTarget(contentTarget); - } - if (currentHour != null) { - matomoRequest.setCurrentHour(currentHour); - } - if (currentMinute != null) { - matomoRequest.setCurrentMinute(currentMinute); - } - if (currentSecond != null) { - matomoRequest.setCurrentSecond(currentSecond); - } - if (customAction != null) { - matomoRequest.setCustomAction(customAction); - } - if (customTrackingParameters != null) { - for (Map.Entry customTrackingParameter : customTrackingParameters.entrySet()) { - matomoRequest.addCustomTrackingParameter(customTrackingParameter.getKey(), customTrackingParameter.getValue()); - } - } - if (deviceResolution != null) { - matomoRequest.setDeviceResolution(deviceResolution); - } - if (downloadUrl != null) { - matomoRequest.setDownloadUrl(downloadUrl); - } - if (ecommerceDiscount != null) { - matomoRequest.setEcommerceDiscount(ecommerceDiscount); - } - if (ecommerceId != null) { - matomoRequest.setEcommerceId(ecommerceId); - } - if (ecommerceLastOrderTimestamp != null) { - matomoRequest.setEcommerceLastOrderTimestamp(ecommerceLastOrderTimestamp); - } - if (ecommerceRevenue != null) { - matomoRequest.setEcommerceRevenue(ecommerceRevenue); - } - if (ecommerceShippingCost != null) { - matomoRequest.setEcommerceShippingCost(ecommerceShippingCost); - } - if (ecommerceSubtotal != null) { - matomoRequest.setEcommerceSubtotal(ecommerceSubtotal); - } - if (ecommerceTax != null) { - matomoRequest.setEcommerceTax(ecommerceTax); - } - if (eventAction != null) { - matomoRequest.setEventAction(eventAction); - } - if (eventCategory != null) { - matomoRequest.setEventCategory(eventCategory); - } - if (eventName != null) { - matomoRequest.setEventName(eventName); - } - if (eventValue != null) { - matomoRequest.setEventValue(eventValue); - } - if (goalId != null) { - matomoRequest.setGoalId(goalId); - } - if (goalRevenue != null) { - matomoRequest.setGoalRevenue(goalRevenue); - } - if (headerAcceptLanguage != null) { - matomoRequest.setHeaderAcceptLanguage(headerAcceptLanguage); - } - if (headerUserAgent != null) { - matomoRequest.setHeaderUserAgent(headerUserAgent); - } - if (newVisit != null) { - matomoRequest.setNewVisit(newVisit); - } - if (outlinkUrl != null) { - matomoRequest.setOutlinkUrl(outlinkUrl); - } - if (pageCustomVariables != null) { - for (int i = 0; i < pageCustomVariables.size(); i++) { - CustomVariable pageCustomVariable = pageCustomVariables.get(i); - matomoRequest.setPageCustomVariable(pageCustomVariable, i + 1); - } - } - if (pluginDirector != null) { - matomoRequest.setPluginDirector(pluginDirector); - } - if (pluginFlash != null) { - matomoRequest.setPluginFlash(pluginFlash); - } - if (pluginGears != null) { - matomoRequest.setPluginGears(pluginGears); - } - if (pluginJava != null) { - matomoRequest.setPluginJava(pluginJava); - } - if (pluginPDF != null) { - matomoRequest.setPluginPDF(pluginPDF); - } - if (pluginQuicktime != null) { - matomoRequest.setPluginQuicktime(pluginQuicktime); - } - if (pluginRealPlayer != null) { - matomoRequest.setPluginRealPlayer(pluginRealPlayer); - } - if (pluginSilverlight != null) { - matomoRequest.setPluginSilverlight(pluginSilverlight); - } - if (pluginWindowsMedia != null) { - matomoRequest.setPluginWindowsMedia(pluginWindowsMedia); - } - if (randomValue != null) { - matomoRequest.setRandomValue(randomValue); - } - if (referrerUrl != null) { - matomoRequest.setReferrerUrl(referrerUrl); - } - if (requestDatetime != null) { - matomoRequest.setRequestDatetime(requestDatetime); - } - if (required != null) { - matomoRequest.setRequired(required); - } - if (responseAsImage != null) { - matomoRequest.setResponseAsImage(responseAsImage); - } - if (searchQuery != null) { - matomoRequest.setSearchQuery(searchQuery); - } - if (searchCategory != null) { - matomoRequest.setSearchCategory(searchCategory); - } - if (searchResultsCount != null) { - matomoRequest.setSearchResultsCount(searchResultsCount); - } - if (trackBotRequests != null) { - matomoRequest.setTrackBotRequests(trackBotRequests); - } - if (visitCustomVariables != null) { - for (int i = 0; i < visitCustomVariables.size(); i++) { - CustomVariable visitCustomVariable = visitCustomVariables.get(i); - matomoRequest.setVisitCustomVariable(visitCustomVariable, i + 1); - } - } - if (userId != null) { - matomoRequest.setUserId(userId); - } - if (visitorCity != null) { - matomoRequest.setVisitorCity(visitorCity); - } - if (visitorCountry != null) { - matomoRequest.setVisitorCountry(visitorCountry); - } - if (visitorCustomId != null) { - matomoRequest.setVisitorCustomId(visitorCustomId); - } - if (visitorFirstVisitTimestamp != null) { - matomoRequest.setVisitorFirstVisitTimestamp(visitorFirstVisitTimestamp); - } - if (visitorId != null) { - matomoRequest.setVisitorId(visitorId); - } - if (visitorIp != null) { - matomoRequest.setVisitorIp(visitorIp); - } - if (visitorLatitude != null) { - matomoRequest.setVisitorLatitude(visitorLatitude); - } - if (visitorLongitude != null) { - matomoRequest.setVisitorLongitude(visitorLongitude); - } - if (visitorPreviousVisitTimestamp != null) { - matomoRequest.setVisitorPreviousVisitTimestamp(visitorPreviousVisitTimestamp); - } - if (visitorRegion != null) { - matomoRequest.setVisitorRegion(visitorRegion); - } - if (visitorVisitCount != null) { - matomoRequest.setVisitorVisitCount(visitorVisitCount); - } - return matomoRequest; - } - -} diff --git a/src/main/java/org/matomo/java/tracking/MatomoTracker.java b/src/main/java/org/matomo/java/tracking/MatomoTracker.java index d5995beb..8819db90 100644 --- a/src/main/java/org/matomo/java/tracking/MatomoTracker.java +++ b/src/main/java/org/matomo/java/tracking/MatomoTracker.java @@ -4,30 +4,21 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.matomo.java.tracking; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.NonNull; import lombok.extern.slf4j.Slf4j; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.concurrent.FutureCallback; -import org.apache.http.entity.ContentType; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; -import java.util.concurrent.Future; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.function.Consumer; + +import static java.util.Objects.requireNonNull; /** * A class that sends {@link MatomoRequest}s to a specified Matomo server. @@ -37,15 +28,9 @@ @Slf4j public class MatomoTracker { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final TrackerConfiguration trackerConfiguration; - private static final String AUTH_TOKEN = "token_auth"; - private static final String REQUESTS = "requests"; - private static final int DEFAULT_TIMEOUT = 5000; - private final URI hostUrl; - private final int timeout; - private final String proxyHost; - private final int proxyPort; + private final Sender sender; /** * Creates a tracker that will send {@link MatomoRequest}s to the specified @@ -53,9 +38,11 @@ public class MatomoTracker { * * @param hostUrl url endpoint to send requests to. Usually in the format * http://your-matomo-domain.tld/matomo.php. Must not be null + * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)} */ - public MatomoTracker(@NonNull final String hostUrl) { - this(hostUrl, DEFAULT_TIMEOUT); + @Deprecated + public MatomoTracker(@NotNull String hostUrl) { + this(requireNonNull(hostUrl, "Host URL must not be null"), 0); } /** @@ -64,67 +51,83 @@ public MatomoTracker(@NonNull final String hostUrl) { * * @param hostUrl url endpoint to send requests to. Usually in the format * http://your-matomo-domain.tld/matomo.php. - * @param timeout the timeout of the sent request in milliseconds + * @param timeout the timeout of the sent request in milliseconds or -1 if not set + * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)} */ - public MatomoTracker(@NonNull final String hostUrl, final int timeout) { - this(hostUrl, null, 0, timeout); - } - - public MatomoTracker(@NonNull final String hostUrl, @Nullable final String proxyHost, final int proxyPort, final int timeout) { - this.hostUrl = URI.create(hostUrl); - this.proxyHost = proxyHost; - this.proxyPort = proxyPort; - this.timeout = timeout; + @Deprecated + public MatomoTracker(@NotNull String hostUrl, int timeout) { + this(requireNonNull(hostUrl, "Host URL must not be null"), null, 0, timeout); } /** * Creates a tracker that will send {@link MatomoRequest}s to the specified - * Tracking HTTP API endpoint via the provided proxy + * Tracking HTTP API endpoint. * * @param hostUrl url endpoint to send requests to. Usually in the format * http://your-matomo-domain.tld/matomo.php. - * @param proxyHost url endpoint for the proxy - * @param proxyPort proxy server port number + * @param proxyHost The hostname or IP address of an optional HTTP proxy, null allowed + * @param proxyPort The port of an HTTP proxy or -1 if not set + * @param timeout the timeout of the request in milliseconds or -1 if not set + * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)} */ - public MatomoTracker(@NonNull final String hostUrl, @Nullable final String proxyHost, final int proxyPort) { - this(hostUrl, proxyHost, proxyPort, DEFAULT_TIMEOUT); + @Deprecated + public MatomoTracker(@NotNull String hostUrl, @Nullable String proxyHost, int proxyPort, int timeout) { + this(TrackerConfiguration.builder().enabled(true).apiEndpoint( + URI.create(requireNonNull(hostUrl, "Host URL must not be null"))).proxyHost(proxyHost).proxyPort(proxyPort) + .connectTimeout(timeout == -1 ? Duration.ofSeconds(5L) : Duration.ofSeconds(timeout)) + .socketTimeout(timeout == -1 ? Duration.ofSeconds(5L) : Duration.ofSeconds(timeout)).build()); } /** - * Sends a tracking request to Matomo + * Creates a new Matomo Tracker instance. * - * @param request request to send. must not be null - * @return the response from this request - * @deprecated use sendRequestAsync instead + * @param trackerConfiguration Configurations parameters (you can use a builder) */ - @Deprecated - public HttpResponse sendRequest(@NonNull final MatomoRequest request) { - final HttpClient client = getHttpClient(); - HttpUriRequest get = createGetRequest(request); - log.debug("Sending request via GET: {}", request); - try { - return client.execute(get); - } catch (IOException e) { - throw new MatomoException("Could not send request to Matomo", e); - } + public MatomoTracker(@NotNull TrackerConfiguration trackerConfiguration) { + requireNonNull(trackerConfiguration, "Tracker configuration must not be null"); + trackerConfiguration.validate(); + this.trackerConfiguration = trackerConfiguration; + ScheduledThreadPoolExecutor threadPoolExecutor = createThreadPoolExecutor(); + sender = new Sender(trackerConfiguration, new QueryCreator(trackerConfiguration), threadPoolExecutor); } - @Nonnull - private HttpUriRequest createGetRequest(@NonNull MatomoRequest request) { - try { - return new HttpGet(new URIBuilder(hostUrl).addParameters(QueryParameters.fromMap(request.getParameters())).build()); - } catch (URISyntaxException e) { - throw new InvalidUrlException(e); - } + @NotNull + private static ScheduledThreadPoolExecutor createThreadPoolExecutor() { + DaemonThreadFactory threadFactory = new DaemonThreadFactory(); + ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(1, threadFactory); + threadPoolExecutor.setRemoveOnCancelPolicy(true); + return threadPoolExecutor; + } + + /** + * Creates a tracker that will send {@link MatomoRequest}s to the specified + * Tracking HTTP API endpoint via the provided proxy. + * + * @param hostUrl url endpoint to send requests to. Usually in the format + * http://your-matomo-domain.tld/matomo.php. + * @param proxyHost url endpoint for the proxy, null allowed + * @param proxyPort proxy server port number or -1 if not set + * @deprecated Please use {@link MatomoTracker#MatomoTracker(TrackerConfiguration)} + */ + @Deprecated + public MatomoTracker(@NotNull String hostUrl, @Nullable String proxyHost, int proxyPort) { + this(hostUrl, proxyHost, proxyPort, -1); } /** - * Get a HTTP client. With proxy if a proxy is provided in the constructor. + * Sends a tracking request to Matomo. * - * @return a HTTP client + * @param request request to send. must not be null + * @deprecated use sendRequestAsync instead */ - protected HttpClient getHttpClient() { - return HttpClientFactory.getInstanceFor(proxyHost, proxyPort, timeout); + @Deprecated + public void sendRequest(@NonNull MatomoRequest request) { + if (trackerConfiguration.isEnabled()) { + log.debug("Sending request via GET: {}", request); + sender.sendSingle(request); + } else { + log.warn("Not sending request, because tracker is disabled"); + } } /** @@ -133,7 +136,7 @@ protected HttpClient getHttpClient() { * @param request request to send * @return future with response from this request */ - public Future sendRequestAsync(@NonNull final MatomoRequest request) { + public CompletableFuture sendRequestAsync(@NotNull MatomoRequest request) { return sendRequestAsync(request, null); } @@ -141,24 +144,27 @@ public Future sendRequestAsync(@NonNull final MatomoRequest reques * Send a request. * * @param request request to send - * @param callback callback that gets executed when response arrives + * @param callback callback that gets executed when response arrives, null allowed * @return future with response from this request */ - public Future sendRequestAsync(@NonNull final MatomoRequest request, @Nullable FutureCallback callback) { - final CloseableHttpAsyncClient client = getHttpAsyncClient(); - client.start(); - HttpUriRequest get = createGetRequest(request); - log.debug("Sending async request via GET: {}", request); - return client.execute(get, callback); + public CompletableFuture sendRequestAsync(@NotNull MatomoRequest request, @Nullable Consumer callback) { + if (trackerConfiguration.isEnabled()) { + validate(request); + log.debug("Sending async request via GET: {}", request); + CompletableFuture future = sender.sendSingleAsync(request); + if (callback != null) { + return future.thenAccept(callback); + } + return future; + } + log.warn("Not sending request, because tracker is disabled"); + return CompletableFuture.completedFuture(null); } - /** - * Get an async HTTP client. With proxy if a proxy is provided in the constructor. - * - * @return an async HTTP client - */ - protected CloseableHttpAsyncClient getHttpAsyncClient() { - return HttpClientFactory.getAsyncInstanceFor(proxyHost, proxyPort, timeout); + private void validate(@NotNull MatomoRequest request) { + if (trackerConfiguration.getDefaultSiteId() == null && request.getSiteId() == null) { + throw new IllegalArgumentException("No default site ID and no request site ID is given"); + } } /** @@ -166,12 +172,11 @@ protected CloseableHttpAsyncClient getHttpAsyncClient() { * several individual requests. * * @param requests the requests to send - * @return the response from these requests * @deprecated use sendBulkRequestAsync instead */ @Deprecated - public HttpResponse sendBulkRequest(@NonNull final Iterable requests) { - return sendBulkRequest(requests, null); + public void sendBulkRequest(@NonNull Iterable requests) { + sendBulkRequest(requests, null); } /** @@ -180,37 +185,20 @@ public HttpResponse sendBulkRequest(@NonNull final Iterable requests, @Nullable final String authToken) { - if (authToken != null && authToken.length() != MatomoRequest.AUTH_TOKEN_LENGTH) { - throw new IllegalArgumentException(authToken + " is not " + MatomoRequest.AUTH_TOKEN_LENGTH + " characters long."); - } - HttpPost post = buildPost(requests, authToken); - final HttpClient client = getHttpClient(); - log.debug("Sending requests via POST: {}", requests); - try { - return client.execute(post); - } catch (IOException e) { - throw new MatomoException("Could not send bulk request", e); - } - } - - private HttpPost buildPost(@NonNull Iterable requests, @Nullable String authToken) { - ObjectNode objectNode = OBJECT_MAPPER.createObjectNode(); - ArrayNode requestsNode = objectNode.putArray(REQUESTS); - for (final MatomoRequest request : requests) { - requestsNode.add(new URIBuilder().addParameters(QueryParameters.fromMap(request.getParameters())).toString()); - } - if (authToken != null) { - objectNode.put(AUTH_TOKEN, authToken); + public void sendBulkRequest(@NonNull Iterable requests, @Nullable String authToken) { + if (trackerConfiguration.isEnabled()) { + for (MatomoRequest request : requests) { + validate(request); + } + log.debug("Sending requests via POST: {}", requests); + sender.sendBulk(requests, authToken); + } else { + log.warn("Not sending request, because tracker is disabled"); } - HttpPost post = new HttpPost(hostUrl); - post.setEntity(new StringEntity(objectNode.toString(), ContentType.APPLICATION_JSON)); - return post; } /** @@ -220,7 +208,7 @@ private HttpPost buildPost(@NonNull Iterable requests, * @param requests the requests to send * @return future with response from these requests */ - public Future sendBulkRequestAsync(@NonNull final Iterable requests) { + public CompletableFuture sendBulkRequestAsync(@NotNull Iterable requests) { return sendBulkRequestAsync(requests, null, null); } @@ -230,19 +218,26 @@ public Future sendBulkRequestAsync(@NonNull final Iterable sendBulkRequestAsync(@NonNull final Iterable requests, @Nullable final String authToken, @Nullable FutureCallback callback) { - if (authToken != null && authToken.length() != MatomoRequest.AUTH_TOKEN_LENGTH) { - throw new IllegalArgumentException(authToken + " is not " + MatomoRequest.AUTH_TOKEN_LENGTH + " characters long."); + public CompletableFuture sendBulkRequestAsync( + @NotNull Iterable requests, @Nullable String authToken, @Nullable Consumer callback + ) { + if (trackerConfiguration.isEnabled()) { + for (MatomoRequest request : requests) { + validate(request); + } + log.debug("Sending async requests via POST: {}", requests); + CompletableFuture future = sender.sendBulkAsync(requests, authToken); + if (callback != null) { + return future.thenAccept(callback); + } + return future; } - HttpPost post = buildPost(requests, authToken); - final CloseableHttpAsyncClient client = getHttpAsyncClient(); - client.start(); - log.debug("Sending async requests via POST: {}", requests); - return client.execute(post, callback); + log.warn("Tracker is disabled"); + return CompletableFuture.completedFuture(null); } /** @@ -250,10 +245,12 @@ public Future sendBulkRequestAsync(@NonNull final Iterable sendBulkRequestAsync(@NonNull final Iterable requests, @Nullable FutureCallback callback) { + public CompletableFuture sendBulkRequestAsync( + @NotNull Iterable requests, @Nullable Consumer callback + ) { return sendBulkRequestAsync(requests, null, callback); } @@ -263,10 +260,12 @@ public Future sendBulkRequestAsync(@NonNull final Iterable sendBulkRequestAsync(@NonNull final Iterable requests, @Nullable final String authToken) { + public CompletableFuture sendBulkRequestAsync( + @NotNull Iterable requests, @Nullable String authToken + ) { return sendBulkRequestAsync(requests, authToken, null); } } diff --git a/src/main/java/org/matomo/java/tracking/ProxyAuthenticator.java b/src/main/java/org/matomo/java/tracking/ProxyAuthenticator.java new file mode 100644 index 00000000..1b6641a6 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/ProxyAuthenticator.java @@ -0,0 +1,35 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; + +@RequiredArgsConstructor +class ProxyAuthenticator extends Authenticator { + + @NonNull + private final String user; + + @NonNull + private final String password; + + @Nullable + @Override + protected PasswordAuthentication getPasswordAuthentication() { + if (getRequestorType() == RequestorType.PROXY) { + return new PasswordAuthentication(user, password.toCharArray()); + } + return null; + } + +} diff --git a/src/main/java/org/matomo/java/tracking/QueryCreator.java b/src/main/java/org/matomo/java/tracking/QueryCreator.java new file mode 100644 index 00000000..17a91a5d --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/QueryCreator.java @@ -0,0 +1,144 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +@RequiredArgsConstructor +class QueryCreator { + + private static final TrackingParameterMethod[] TRACKING_PARAMETER_METHODS = initializeTrackingParameterMethods(); + + private final TrackerConfiguration trackerConfiguration; + + private static TrackingParameterMethod[] initializeTrackingParameterMethods() { + Field[] declaredFields = MatomoRequest.class.getDeclaredFields(); + List methods = new ArrayList<>(declaredFields.length); + for (Field field : declaredFields) { + if (field.isAnnotationPresent(TrackingParameter.class)) { + addMethods(methods, field, field.getAnnotation(TrackingParameter.class)); + } + } + return methods.toArray(new TrackingParameterMethod[0]); + } + + private static void addMethods( + Collection methods, Member member, TrackingParameter trackingParameter + ) { + try { + for (PropertyDescriptor pd : Introspector.getBeanInfo(MatomoRequest.class).getPropertyDescriptors()) { + if (member.getName().equals(pd.getName())) { + String regex = trackingParameter.regex(); + methods.add(TrackingParameterMethod.builder() + .parameterName(trackingParameter.name()) + .method(pd.getReadMethod()) + .pattern(regex == null || regex.isEmpty() || regex.trim().isEmpty() ? null : + Pattern.compile(trackingParameter.regex())) + .build()); + } + } + } catch (IntrospectionException e) { + throw new MatomoException("Could not initialize read methods", e); + } + } + + String createQuery(@NotNull MatomoRequest request, @Nullable String authToken) { + StringBuilder query = new StringBuilder(100); + if (request.getSiteId() == null) { + appendAmpersand(query); + query.append("idsite=").append(trackerConfiguration.getDefaultSiteId()); + } + if (authToken != null) { + if (authToken.length() != 32) { + throw new IllegalArgumentException("Auth token must be exactly 32 characters long"); + } + query.append("token_auth=").append(authToken); + } + for (TrackingParameterMethod method : TRACKING_PARAMETER_METHODS) { + appendParameter(method, request, query); + } + if (request.getCustomTrackingParameters() != null) { + for (Entry> entry : request.getCustomTrackingParameters().entrySet()) { + for (Object value : entry.getValue()) { + if (value != null && !value.toString().trim().isEmpty()) { + appendAmpersand(query); + query.append(encode(entry.getKey())).append('=').append(encode(value.toString())); + } + } + } + } + if (request.getDimensions() != null) { + int i = 0; + for (Object dimension : request.getDimensions()) { + appendAmpersand(query); + query.append("dimension").append(i + 1).append('=').append(dimension); + i++; + } + } + return query.toString(); + } + + private static void appendAmpersand(StringBuilder query) { + if (query.length() != 0) { + query.append('&'); + } + } + + private static void appendParameter(TrackingParameterMethod method, MatomoRequest request, StringBuilder query) { + try { + Object parameterValue = method.getMethod().invoke(request); + if (parameterValue != null) { + method.validateParameterValue(parameterValue); + appendAmpersand(query); + query.append(method.getParameterName()).append('='); + if (parameterValue instanceof Boolean) { + query.append((boolean) parameterValue ? '1' : '0'); + } else if (parameterValue instanceof Charset) { + query.append(((Charset) parameterValue).name()); + } else if (parameterValue instanceof Instant) { + query.append(((Instant) parameterValue).getEpochSecond()); + } else { + String parameterValueString = parameterValue.toString(); + if (!parameterValueString.trim().isEmpty()) { + query.append(encode(parameterValueString)); + } + } + } + } catch (Exception e) { + throw new MatomoException("Could not append parameter", e); + } + } + + @NotNull + private static String encode(@NotNull String parameterValue) { + try { + return URLEncoder.encode(parameterValue, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new MatomoException("Could not encode parameter", e); + } + } + + +} diff --git a/src/main/java/org/matomo/java/tracking/QueryParameters.java b/src/main/java/org/matomo/java/tracking/QueryParameters.java deleted file mode 100644 index 2e1f94c3..00000000 --- a/src/main/java/org/matomo/java/tracking/QueryParameters.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.matomo.java.tracking; - -import lombok.NonNull; -import org.apache.http.NameValuePair; -import org.apache.http.message.BasicNameValuePair; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -public final class QueryParameters { - - private QueryParameters() { - // utility - } - - @Nonnull - public static List fromMap(@NonNull Map> map) { - List queryParameters = new ArrayList<>(); - for (Map.Entry> entries : map.entrySet()) { - for (Object value : entries.getValue()) { - queryParameters.add(new BasicNameValuePair(entries.getKey(), value.toString())); - } - } - queryParameters.sort(Comparator.comparing(NameValuePair::getName)); - return queryParameters; - } - -} diff --git a/src/main/java/org/matomo/java/tracking/RequestValidator.java b/src/main/java/org/matomo/java/tracking/RequestValidator.java new file mode 100644 index 00000000..d28d6385 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/RequestValidator.java @@ -0,0 +1,55 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static java.util.Objects.requireNonNull; + +final class RequestValidator { + + private RequestValidator() { + // utility + } + + static void validate(@NotNull MatomoRequest request, @Nullable CharSequence authToken) { + requireNonNull(request, "Request must not be null"); + if (request.getSiteId() != null && request.getSiteId() < 0) { + throw new IllegalArgumentException("Site ID must not be negative"); + } + if (request.getGoalId() == null && (request.getEcommerceId() != null || request.getEcommerceRevenue() != null + || request.getEcommerceDiscount() != null || request.getEcommerceItems() != null + || request.getEcommerceLastOrderTimestamp() != null || request.getEcommerceShippingCost() != null + || request.getEcommerceSubtotal() != null || request.getEcommerceTax() != null)) { + throw new MatomoException("Goal ID must be set if ecommerce parameters are used"); + } + if (request.getSearchResultsCount() != null && request.getSearchQuery() == null) { + throw new MatomoException("Search query must be set if search results count is set"); + } + if (authToken == null) { + if (request.getVisitorLongitude() != null || request.getVisitorLatitude() != null + || request.getVisitorRegion() != null || request.getVisitorCity() != null + || request.getVisitorCountry() != null) { + throw new MatomoException("Auth token must be present if longitude, latitude, region, city or country are set"); + } + if (request.getRequestTimestamp() != null + && request.getRequestTimestamp().isBefore(Instant.now().minus(4, ChronoUnit.HOURS))) { + throw new MatomoException("Auth token must be present if request timestamp is more than four hours ago"); + } + } else { + if (authToken.length() != 32) { + throw new IllegalArgumentException("Auth token must be exactly 32 characters long"); + } + } + } +} + diff --git a/src/main/java/org/matomo/java/tracking/Sender.java b/src/main/java/org/matomo/java/tracking/Sender.java new file mode 100644 index 00000000..ce97d669 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/Sender.java @@ -0,0 +1,219 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Authenticator; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.Proxy; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; + +@Slf4j +@RequiredArgsConstructor +class Sender { + + private final TrackerConfiguration trackerConfiguration; + + private final QueryCreator queryCreator; + + private final Collection queries = new ArrayList<>(16); + + private final Executor executor; + + @NotNull CompletableFuture sendSingleAsync(@NotNull MatomoRequest request) { + return CompletableFuture.supplyAsync(() -> { + sendSingle(request); + return null; + }, executor); + } + + void sendSingle(@NotNull MatomoRequest request) { + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + RequestValidator.validate(request, authToken); + HttpURLConnection connection; + URI apiEndpoint = trackerConfiguration.getApiEndpoint(); + try { + connection = openConnection( + apiEndpoint.resolve(String.format("%s?%s", apiEndpoint.getPath(), queryCreator.createQuery(request, authToken))) + .toURL()); + } catch (MalformedURLException e) { + throw new InvalidUrlException(e); + } + configureAgentsAndTimeouts(connection); + log.debug("Sending single request using URI {} asynchronously", apiEndpoint); + try { + connection.connect(); + checkResponse(connection); + } catch (IOException e) { + throw new MatomoException("Could not send request via GET", e); + } finally { + connection.disconnect(); + } + } + + private HttpURLConnection openConnection(URL url) { + try { + if (isEmpty(trackerConfiguration.getProxyHost()) || trackerConfiguration.getProxyPort() <= 0) { + log.debug("Proxy host or proxy port not configured. Will create connection without proxy"); + return (HttpURLConnection) url.openConnection(); + } + InetSocketAddress proxyAddress = + new InetSocketAddress(trackerConfiguration.getProxyHost(), trackerConfiguration.getProxyPort()); + Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyAddress); + if (!isEmpty(trackerConfiguration.getProxyUserName()) && !isEmpty(trackerConfiguration.getProxyPassword())) { + Authenticator.setDefault( + new ProxyAuthenticator(trackerConfiguration.getProxyUserName(), trackerConfiguration.getProxyPassword())); + } + return (HttpURLConnection) url.openConnection(proxy); + } catch (IOException e) { + throw new MatomoException("Could not open connection", e); + } + } + + private void configureAgentsAndTimeouts(HttpURLConnection connection) { + connection.setUseCaches(false); + connection.setRequestProperty("User-Agent", trackerConfiguration.getUserAgent()); + if (trackerConfiguration.getConnectTimeout() != null) { + connection.setConnectTimeout((int) trackerConfiguration.getConnectTimeout().toMillis()); + } + if (trackerConfiguration.getSocketTimeout() != null) { + connection.setReadTimeout((int) trackerConfiguration.getSocketTimeout().toMillis()); + } + } + + private void checkResponse(HttpURLConnection connection) throws IOException { + if (connection.getResponseCode() > 399) { + if (trackerConfiguration.isLogFailedTracking()) { + log.error("Received error code {}", connection.getResponseCode()); + } + throw new MatomoException("Tracking endpoint responded with code " + connection.getResponseCode()); + } + } + + private static boolean isEmpty(@Nullable String str) { + return str == null || str.isEmpty() || str.trim().isEmpty(); + } + + void sendBulk(@NotNull Iterable requests, @Nullable String overrideAuthToken) { + String authToken = AuthToken.determineAuthToken(overrideAuthToken, requests, trackerConfiguration); + sendBulk(StreamSupport.stream(requests.spliterator(), false).map(request -> { + RequestValidator.validate(request, authToken); + return queryCreator.createQuery(request, null); + }).collect(Collectors.toList()), authToken); + } + + private void sendBulk(@NotNull Collection queries, @Nullable String authToken) { + requireNonNull(queries, "Queries must not be null"); + HttpURLConnection connection; + try { + connection = openConnection(trackerConfiguration.getApiEndpoint().toURL()); + } catch (MalformedURLException e) { + throw new InvalidUrlException(e); + } + preparePostConnection(connection); + configureAgentsAndTimeouts(connection); + log.debug("Sending bulk request using URI {} asynchronously", trackerConfiguration.getApiEndpoint()); + OutputStream outputStream = null; + try { + connection.connect(); + outputStream = connection.getOutputStream(); + outputStream.write(createPayload(queries, authToken)); + outputStream.flush(); + checkResponse(connection); + } catch (IOException e) { + throw new MatomoException("Could not send requests via POST", e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + // ignore + } + } + connection.disconnect(); + } + } + + private static void preparePostConnection(HttpURLConnection connection) { + try { + connection.setRequestMethod("POST"); + } catch (ProtocolException e) { + throw new MatomoException("Could not set request method", e); + } + connection.setDoOutput(true); + connection.setRequestProperty("Accept", "*/*"); + connection.setRequestProperty("Content-Type", "application/json"); + + } + + private static byte[] createPayload(@NotNull Collection queries, @Nullable String authToken) { + requireNonNull(queries, "Queries must not be null"); + StringBuilder payload = new StringBuilder("{\"requests\":["); + Iterator iterator = queries.iterator(); + while (iterator.hasNext()) { + String query = iterator.next(); + payload.append("\"?").append(query).append('"'); + if (iterator.hasNext()) { + payload.append(','); + } + } + payload.append(']'); + if (authToken != null) { + payload.append(",\"token_auth\":\"").append(authToken).append('"'); + } + return payload.append('}').toString().getBytes(StandardCharsets.UTF_8); + } + + @NotNull CompletableFuture sendBulkAsync( + @NotNull Iterable requests, + @Nullable String overrideAuthToken + ) { + String authToken = AuthToken.determineAuthToken(overrideAuthToken, requests, trackerConfiguration); + synchronized (queries) { + for (MatomoRequest request : requests) { + RequestValidator.validate(request, authToken); + String query = queryCreator.createQuery(request, null); + queries.add(query); + } + } + return CompletableFuture.supplyAsync(() -> sendBulkAsync(authToken), executor); + } + + @Nullable + private Void sendBulkAsync(@Nullable String authToken) { + synchronized (queries) { + if (!queries.isEmpty()) { + sendBulk(queries, authToken); + queries.clear(); + } + return null; + } + } + +} diff --git a/src/main/java/org/matomo/java/tracking/TrackerConfiguration.java b/src/main/java/org/matomo/java/tracking/TrackerConfiguration.java new file mode 100644 index 00000000..083156d1 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/TrackerConfiguration.java @@ -0,0 +1,118 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import java.net.URI; +import java.time.Duration; +import java.util.regex.Pattern; +import lombok.Builder; +import lombok.NonNull; +import lombok.Value; +import org.jetbrains.annotations.Nullable; + +/** + * Defines configuration settings for the Matomo tracking. + */ +@Builder +@Value +public class TrackerConfiguration { + + private static final Pattern AUTH_TOKEN_PATTERN = Pattern.compile("[a-z0-9]+"); + /** + * The Matomo Tracking HTTP API endpoint, e.g. https://your-matomo-domain.example/matomo.php + */ + @NonNull URI apiEndpoint; + + /** + * The default ID of the website that will be used if not specified explicitly. + */ + Integer defaultSiteId; + + /** + * The authorization token (parameter token_auth) to use if not specified explicitly. + */ + String defaultAuthToken; + + /** + * Allows to stop the tracker to send requests to the Matomo endpoint. + */ + @Builder.Default + boolean enabled = true; + + /** + * The timeout until a connection is established. + * + *

A timeout value of zero is interpreted as an infinite timeout. + * A `null` value is interpreted as undefined (system default if applicable).

+ * + *

Default: 10 seconds

+ */ + @Builder.Default + Duration connectTimeout = Duration.ofSeconds(5L); + + /** + * The socket timeout ({@code SO_TIMEOUT}), which is the timeout for waiting for data or, put differently, a maximum + * period inactivity between two consecutive data packets. + * + *

A timeout value of zero is interpreted as an infinite timeout. + * A `null value is interpreted as undefined (system default if applicable).

+ * + *

Default: 30 seconds

+ */ + @Builder.Default + Duration socketTimeout = Duration.ofSeconds(5L); + + /** + * The hostname or IP address of an optional HTTP proxy. {@code proxyPort} must be configured as well + */ + @Nullable + String proxyHost; + + /** + * The port of an HTTP proxy. {@code proxyHost} must be configured as well. + */ + int proxyPort; + + /** + * If the HTTP proxy requires a username for basic authentication, it can be configured here. Proxy host, port and + * password must also be set. + */ + @Nullable + String proxyUserName; + + /** + * The corresponding password for the basic auth proxy user. The proxy host, port and username must be set as well. + */ + @Nullable + String proxyPassword; + + /** + * A custom user agent to be set. Defaults to "MatomoJavaClient" + */ + @Builder.Default + @NonNull String userAgent = "MatomoJavaClient"; + + /** + * Logs if the Matomo Tracking API endpoint responds with an erroneous HTTP code. + */ + boolean logFailedTracking; + + /** + * Validates the auth token. The auth token must be exactly 32 characters long. + */ + public void validate() { + if (defaultAuthToken != null) { + if (defaultAuthToken.trim().length() != 32) { + throw new IllegalArgumentException("Auth token must be exactly 32 characters long"); + } + if (!AUTH_TOKEN_PATTERN.matcher(defaultAuthToken).matches()) { + throw new IllegalArgumentException("Auth token must contain only lowercase letters and numbers"); + } + } + } +} diff --git a/src/main/java/org/matomo/java/tracking/TrackingParameter.java b/src/main/java/org/matomo/java/tracking/TrackingParameter.java new file mode 100644 index 00000000..5e32184a --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/TrackingParameter.java @@ -0,0 +1,23 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@interface TrackingParameter { + + String name(); + + String regex() default ""; + +} diff --git a/src/main/java/org/matomo/java/tracking/TrackingParameterMethod.java b/src/main/java/org/matomo/java/tracking/TrackingParameterMethod.java new file mode 100644 index 00000000..01da635e --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/TrackingParameterMethod.java @@ -0,0 +1,37 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking; + +import lombok.Builder; +import lombok.Value; + +import java.lang.reflect.Method; +import java.util.regex.Pattern; + +@Builder +@Value +class TrackingParameterMethod { + + String parameterName; + + Method method; + + Pattern pattern; + + void validateParameterValue(Object parameterValue) { + if (pattern != null && parameterValue instanceof CharSequence && !pattern.matcher((CharSequence) parameterValue) + .matches()) { + throw new IllegalArgumentException(String.format( + "Invalid value for %s. Must match regex %s", + parameterName, + pattern + )); + } + } + +} diff --git a/src/main/java/org/matomo/java/tracking/package-info.java b/src/main/java/org/matomo/java/tracking/package-info.java new file mode 100644 index 00000000..ae38e3ef --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/package-info.java @@ -0,0 +1,14 @@ +/** + * This package contains classes that allow you to specify a {@link org.matomo.java.tracking.MatomoTracker} + * with the corresponding {@link org.matomo.java.tracking.TrackerConfiguration}. You can then send a + * {@link org.matomo.java.tracking.MatomoRequest} as a single HTTP GET request or multiple requests as a bulk HTTP POST + * request synchronously or asynchronously. If an exception occurs, {@link org.matomo.java.tracking.MatomoException} + * will be thrown. + * + *

For more information about the Matomo Tracking HTTP API, see the Matomo Tracking HTTP API. + * + */ + +package org.matomo.java.tracking; + + diff --git a/src/main/java/org/matomo/java/tracking/parameters/AcceptLanguage.java b/src/main/java/org/matomo/java/tracking/parameters/AcceptLanguage.java new file mode 100644 index 00000000..71a74020 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/AcceptLanguage.java @@ -0,0 +1,68 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import java.util.List; +import java.util.Locale.LanguageRange; +import java.util.Objects; +import java.util.stream.Collectors; +import lombok.Builder; +import lombok.Singular; +import lombok.Value; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Describes the content for the Accept-Language header field that can be overridden by a custom parameter. The format + * is specified in the corresponding RFC 4647 Matching of Language Tags + * + *

Example: "en-US,en;q=0.8,de;q=0.6" + */ +@Builder +@Value +public class AcceptLanguage { + + @Singular + List languageRanges; + + /** + * Creates the Accept-Language definition for a given header. + * + *

Please see {@link LanguageRange#parse(String)} for more information. Example: "en-US,en;q=0.8,de;q=0.6" + * + * @param header A header that can be null + * @return The parsed header (probably reformatted). null if the header is null. + * @see LanguageRange#parse(String) + */ + @Nullable + public static AcceptLanguage fromHeader(@Nullable String header) { + if (header == null || header.trim().isEmpty()) { + return null; + } + return new AcceptLanguage(LanguageRange.parse(header)); + } + + /** + * Returns the Accept Language header value. + * + * @return The header value, e.g. "en-US,en;q=0.8,de;q=0.6" + */ + @NotNull + public String toString() { + return languageRanges.stream() + .filter(Objects::nonNull) + .map(AcceptLanguage::format) + .collect(Collectors.joining(",")); + } + + private static String format(@NotNull LanguageRange languageRange) { + return languageRange.getWeight() == LanguageRange.MAX_WEIGHT ? languageRange.getRange() : + String.format("%s;q=%s", languageRange.getRange(), languageRange.getWeight()); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/Country.java b/src/main/java/org/matomo/java/tracking/parameters/Country.java new file mode 100644 index 00000000..959f38af --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/Country.java @@ -0,0 +1,112 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import java.util.List; +import java.util.Locale; +import java.util.Locale.LanguageRange; +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A two-letter country code representing a country. + * + *

See ISO 3166-1 alpha-2 for a list of valid codes. + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class Country { + + @NonNull + private String code; + + /** + * Only for internal use to grant downwards compatibility to {@link org.matomo.java.tracking.MatomoLocale}. + * + * @param locale A locale that must contain a country code + */ + @Deprecated + protected Country(@NotNull Locale locale) { + setLocale(locale); + } + + /** + * Creates a country from a given code. + * + * @param code Must consist of two lower letters or simply null. Case is ignored + * @return The country or null if code was null + */ + @Nullable + public static Country fromCode(@Nullable String code) { + if (code == null || code.isEmpty() || code.trim().isEmpty()) { + return null; + } + if (code.length() == 2) { + return new Country(code.toLowerCase(Locale.ROOT)); + } + throw new IllegalArgumentException("Invalid country code"); + } + + /** + * Extracts the country from the given accept language header. + * + * @param ranges A language range list. See {@link LanguageRange#parse(String)} + * @return The country or null if ranges was null + */ + @Nullable + public static Country fromLanguageRanges(@Nullable String ranges) { + if (ranges == null || ranges.isEmpty() || ranges.trim().isEmpty()) { + return null; + } + List languageRanges = LanguageRange.parse(ranges); + for (LanguageRange languageRange : languageRanges) { + String range = languageRange.getRange(); + String[] split = range.split("-"); + if (split.length == 2 && split[1].length() == 2) { + return new Country(split[1].toLowerCase(Locale.ROOT)); + } + } + throw new IllegalArgumentException("Invalid country code"); + } + + /** + * Returns the locale for this country. + * + * @return The locale for this country + * @see Locale#forLanguageTag(String) + * @deprecated Since you instantiate this class, you can determine the language on your own + * using {@link Locale#forLanguageTag(String)} + */ + @Deprecated + public Locale getLocale() { + return Locale.forLanguageTag(code); + } + + /** + * Sets the locale for this country. + * + * @param locale A locale that must contain a country code + * @see Locale#getCountry() + * @deprecated Since you instantiate this class, you can determine the language on your own + * using {@link Locale#getCountry()} + */ + public final void setLocale(Locale locale) { + if (locale == null || locale.getCountry() == null || locale.getCountry().isEmpty()) { + throw new IllegalArgumentException("Invalid locale"); + } + code = locale.getCountry().toLowerCase(Locale.ENGLISH); + } + + @Override + public String toString() { + return code; + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/CustomVariable.java b/src/main/java/org/matomo/java/tracking/parameters/CustomVariable.java new file mode 100644 index 00000000..570c3579 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/CustomVariable.java @@ -0,0 +1,57 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; + +/** + * A key-value pair that represents custom information. See + * How do I use Custom Variables? + * + *

If you are not already using Custom Variables to measure your custom data, Matomo recommends to use the + * Custom Dimensions feature instead. + * There are many advantages of Custom Dimensions over Custom + * variables. Custom variables will be deprecated in the future. + * + * @deprecated Should not be used according to the Matomo FAQ: How do I use Custom Variables? + */ +@Getter +@Setter +@AllArgsConstructor +@ToString +@EqualsAndHashCode(exclude = "index") +@Deprecated +public class CustomVariable { + + private int index; + + @NonNull + private String key; + + @NonNull + private String value; + + /** + * Instantiates a new custom variable. + * + * @param key the key of the custom variable (required) + * @param value the value of the custom variable (required) + */ + public CustomVariable(@NonNull String key, @NonNull String value) { + this.key = key; + this.value = value; + } +} + + + diff --git a/src/main/java/org/matomo/java/tracking/parameters/CustomVariables.java b/src/main/java/org/matomo/java/tracking/parameters/CustomVariables.java new file mode 100644 index 00000000..7cbd7cce --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/CustomVariables.java @@ -0,0 +1,151 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import org.jetbrains.annotations.Nullable; + +/** + * A bunch of key-value pairs that represent custom information. See How do I use Custom Variables? + * + * @deprecated Should not be used according to the Matomo FAQ: How do I use Custom Variables? + */ +@EqualsAndHashCode +@Deprecated +public class CustomVariables { + + private final Map variables = new LinkedHashMap<>(); + + /** + * Adds a custom variable to the list with the next available index. + * + * @param variable The custom variable to add + * @return This object for method chaining + */ + public CustomVariables add(@NonNull CustomVariable variable) { + if (variable.getKey().isEmpty()) { + throw new IllegalArgumentException("Custom variable key must not be null or empty"); + } + if (variable.getValue().isEmpty()) { + throw new IllegalArgumentException("Custom variable value must not be null or empty"); + } + boolean found = false; + for (Entry entry : variables.entrySet()) { + CustomVariable customVariable = entry.getValue(); + if (customVariable.getKey().equals(variable.getKey())) { + variables.put(entry.getKey(), variable); + found = true; + } + } + if (!found) { + int i = 1; + while (variables.putIfAbsent(i, variable) != null) { + i++; + } + } + return this; + } + + /** + * Adds a custom variable to the list with the given index. + * + * @param cv The custom variable to add + * @param index The index to add the custom variable at + * @return This object for method chaining + */ + public CustomVariables add(@NonNull CustomVariable cv, int index) { + validateIndex(index); + variables.put(index, cv); + return this; + } + + private static void validateIndex(int index) { + if (index <= 0) { + throw new IllegalArgumentException("Index must be greater than 0"); + } + } + + /** + * Returns the custom variable at the given index. + * + * @param index The index of the custom variable + * @return The custom variable at the given index + */ + @Nullable + public CustomVariable get(int index) { + validateIndex(index); + return variables.get(index); + } + + /** + * Returns the value of the custom variable with the given key. If there are multiple custom variables with the same + * key, the first one is returned. If there is no custom variable with the given key, null is returned. + * + * @param key The key of the custom variable. Must not be null. + * @return The value of the custom variable with the given key. null if there is no variable with the given key. + */ + @Nullable + public String get(@NonNull String key) { + if (key.isEmpty()) { + throw new IllegalArgumentException("key must not be null or empty"); + } + return variables.values().stream().filter(variable -> variable.getKey().equals(key)).findFirst() + .map(CustomVariable::getValue).orElse(null); + } + + /** + * Removes the custom variable at the given index. If there is no custom variable at the given index, nothing happens. + * + * @param index The index of the custom variable to remove. Must be greater than 0. + */ + public void remove(int index) { + validateIndex(index); + variables.remove(index); + } + + /** + * Removes the custom variable with the given key. If there is no custom variable with the given key, nothing happens. + * + * @param key The key of the custom variable to remove. Must not be null. + */ + public void remove(@NonNull String key) { + variables.entrySet().removeIf(entry -> entry.getValue().getKey().equals(key)); + } + + boolean isEmpty() { + return variables.isEmpty(); + } + + /** + * Creates a JSON representation of the custom variables. The format is as follows: + * {@code {"1":["key1","value1"],"2":["key2","value2"]}} + * + * @return A JSON representation of the custom variables + */ + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder("{"); + Iterator> iterator = variables.entrySet().iterator(); + while (iterator.hasNext()) { + Entry entry = iterator.next(); + stringBuilder.append('"').append(entry.getKey()).append("\":[\"").append(entry.getValue().getKey()) + .append("\",\"").append(entry.getValue().getValue()).append("\"]"); + if (iterator.hasNext()) { + stringBuilder.append(','); + } + } + stringBuilder.append('}'); + return stringBuilder.toString(); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java b/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java new file mode 100644 index 00000000..d1643938 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java @@ -0,0 +1,51 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.Nullable; + +/** + * The resolution (width and height) of the user's output device (monitor / phone). + */ +@Builder +@RequiredArgsConstructor +public class DeviceResolution { + + private final int width; + + private final int height; + + /** + * Creates a device resolution from a string representation. + * + *

The string must be in the format "widthxheight", e.g. "1920x1080". + * + * @param deviceResolution The string representation of the device resolution, e.g. "1920x1080" + * @return The device resolution representation + */ + @Nullable + public static DeviceResolution fromString(@Nullable String deviceResolution) { + if (deviceResolution == null) { + return null; + } + String[] dimensions = deviceResolution.split("x"); + if (dimensions.length != 2) { + throw new IllegalArgumentException("Wrong dimension size"); + } + return builder().width(Integer.parseInt(dimensions[0])).height(Integer.parseInt(dimensions[1])) + .build(); + } + + @Override + public String toString() { + return String.format("%dx%d", width, height); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java b/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java new file mode 100644 index 00000000..7f5ba051 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java @@ -0,0 +1,41 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +/** + * Represents an item in an ecommerce order. + */ +@Builder +@AllArgsConstructor +@Getter +@Setter +public class EcommerceItem { + + private String sku; + + @Builder.Default + private String name = ""; + + @Builder.Default + private String category = ""; + + @Builder.Default + private Double price = 0.0; + + @Builder.Default + private Integer quantity = 0; + + public String toString() { + return String.format("[\"%s\",\"%s\",\"%s\",%s,%d]", sku, name, category, price, quantity); + } +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java b/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java new file mode 100644 index 00000000..d9b85754 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java @@ -0,0 +1,42 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.Singular; +import lombok.experimental.Delegate; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Multiple things that you can buy online. + */ +@Builder +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode +@Getter +@Setter +public class EcommerceItems { + + @Delegate + @Singular + private List items = new ArrayList<>(); + + public String toString() { + return items.stream().map(String::valueOf).collect(Collectors.joining(",", "[", "]")); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/Hex.java b/src/main/java/org/matomo/java/tracking/parameters/Hex.java new file mode 100644 index 00000000..3387afe5 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/Hex.java @@ -0,0 +1,24 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +final class Hex { + + private Hex() { + // utility class + } + + static String fromBytes(byte[] bytes) { + StringBuilder result = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java b/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java new file mode 100644 index 00000000..2b000b6e --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java @@ -0,0 +1,55 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import java.security.SecureRandom; +import java.util.Random; + +/** + * A random value to avoid the tracking request being cached by the browser or a proxy. + */ +public class RandomValue { + + private static final Random RANDOM = new SecureRandom(); + + private final byte[] representation = new byte[10]; + + private String override; + + /** + * Static factory to generate a random value. + * + * @return A randomly generated value + */ + public static RandomValue random() { + RandomValue randomValue = new RandomValue(); + RANDOM.nextBytes(randomValue.representation); + return randomValue; + } + + /** + * Static factory to generate a random value from a given string. The string will be used as is and not hashed. + * + * @param override The string to use as random value + * @return A random value from the given string + */ + public static RandomValue fromString(String override) { + RandomValue randomValue = new RandomValue(); + randomValue.override = override; + return randomValue; + } + + @Override + public String toString() { + if (override != null) { + return override; + } + return Hex.fromBytes(representation); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java b/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java new file mode 100644 index 00000000..e66f2717 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java @@ -0,0 +1,55 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.security.SecureRandom; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * A six character unique ID consisting of the characters [0-9a-Z]. + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public final class UniqueId { + + private static final String CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + private static final Random RANDOM = new SecureRandom(); + + private final long value; + + /** + * Static factory to generate a random unique id. + * + * @return A randomly generated unique id + */ + public static UniqueId random() { + return fromValue(RANDOM.nextLong()); + } + + /** + * Creates a unique id from a number. + * + * @param value A number to create this unique id from + * @return The unique id for the given value + */ + public static UniqueId fromValue(long value) { + return new UniqueId(value); + } + + @Override + public String toString() { + return IntStream.range(0, 6).map(i -> (int) (value >> i * 8)).mapToObj( + codePoint -> String.valueOf(CHARS.charAt(Math.abs(codePoint % CHARS.length())))).collect(Collectors.joining()); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java b/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java new file mode 100644 index 00000000..bb267ee3 --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java @@ -0,0 +1,98 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import java.security.SecureRandom; +import java.util.Random; +import java.util.regex.Pattern; + +/** + * The unique visitor ID, must be a 16 characters hexadecimal string. Every unique visitor must be assigned a different + * ID and this ID must not change after it is assigned. If this value is not set Matomo will still track visits, but the + * unique visitors metric might be less accurate. + */ +public class VisitorId { + + private static final Random RANDOM = new SecureRandom(); + private static final Pattern HEX_DIGITS = Pattern.compile("[0-9a-fA-F]+"); + + private final byte[] representation = new byte[8]; + + /** + * Static factory to generate a random visitor id. + * + *

Please consider creating a fixed id for each visitor by getting a hash code from e.g. the username and + * using {@link #fromHash(long)} + * + * @return A randomly generated visitor id + */ + public static VisitorId random() { + VisitorId visitorId = new VisitorId(); + RANDOM.nextBytes(visitorId.representation); + return visitorId; + } + + /** + * Creates always the same visitor id for the given input. + * + *

You can use e.g. {@link Object#hashCode()} to generate a hash code for an object, e.g. a username + * string as input. + * + * @param hash A number (e.g. a hash code) to create the visitor id from + * @return Always the same visitor id for the same input + */ + public static VisitorId fromHash(long hash) { + VisitorId visitorId = new VisitorId(); + long remainingHash = hash; + for (int i = visitorId.representation.length - 1; i >= 0; i--) { + visitorId.representation[i] = (byte) (remainingHash & 0xFF); + remainingHash >>= Byte.SIZE; + } + return visitorId; + } + + /** + * Creates a visitor id from a hexadecimal string. + * + *

The input must be a valid hexadecimal string with a maximum length of 16 characters. If the input is shorter + * than 16 characters it will be padded with zeros.

+ * + * @param inputHex A hexadecimal string to create the visitor id from + * @return The visitor id for the given input + */ + public static VisitorId fromHex(String inputHex) { + if (inputHex == null || inputHex.trim().isEmpty()) { + throw new IllegalArgumentException("Hex string must not be null or empty"); + } + if (inputHex.length() > 16) { + throw new IllegalArgumentException("Hex string must not be longer than 16 characters"); + } + if (!HEX_DIGITS.matcher(inputHex).matches()) { + throw new IllegalArgumentException("Input must be a valid hex string"); + } + VisitorId visitorId = new VisitorId(); + for (int charIndex = inputHex.length() - 1, representationIndex = visitorId.representation.length - 1; + charIndex >= 0; charIndex -= 2, representationIndex--) { + String hex = inputHex.substring(Math.max(0, charIndex - 1), charIndex + 1); + try { + visitorId.representation[representationIndex] = (byte) Integer.parseInt(hex, 16); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Input must be a valid hex string", e); + } + } + + + return visitorId; + } + + @Override + public String toString() { + return Hex.fromBytes(representation); + } + +} diff --git a/src/main/java/org/matomo/java/tracking/parameters/package-info.java b/src/main/java/org/matomo/java/tracking/parameters/package-info.java new file mode 100644 index 00000000..f709edce --- /dev/null +++ b/src/main/java/org/matomo/java/tracking/parameters/package-info.java @@ -0,0 +1,9 @@ +/** + * Contains types for Matomo Tracking Parameters according to + * the Matomo Tracking HTTP API. + * + *

The types help you to use the correct format for the tracking parameters. The package was introduced in Matomo + * Java Tracker version 3 to let the tracker be more self-explanatory and better maintainable. + */ + +package org.matomo.java.tracking.parameters; diff --git a/src/main/java/org/piwik/java/tracking/CustomVariable.java b/src/main/java/org/piwik/java/tracking/CustomVariable.java index 48b3813a..20845bed 100644 --- a/src/main/java/org/piwik/java/tracking/CustomVariable.java +++ b/src/main/java/org/piwik/java/tracking/CustomVariable.java @@ -4,19 +4,26 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ -package org.piwik.java.tracking; -import org.matomo.java.tracking.MatomoRequest; +package org.piwik.java.tracking; /** + * A user defined custom variable. + * + *

Renamed to {@link org.matomo.java.tracking.parameters.CustomVariable} in 3.0.0. + * * @author brettcsorba - * @deprecated Use {@link org.matomo.java.tracking.CustomVariable} instead. + * @deprecated Use {@link org.matomo.java.tracking.parameters.CustomVariable} instead. */ @Deprecated -public class CustomVariable extends org.matomo.java.tracking.CustomVariable { +public class CustomVariable extends org.matomo.java.tracking.parameters.CustomVariable { /** - * @deprecated Use {@link MatomoRequest} instead. + * Instantiates a new custom variable. + * + * @param key the key of the custom variable (required) + * @param value the value of the custom variable (required) + * @deprecated Use {@link org.matomo.java.tracking.parameters.CustomVariable} instead. */ @Deprecated public CustomVariable(String key, String value) { diff --git a/src/main/java/org/piwik/java/tracking/EcommerceItem.java b/src/main/java/org/piwik/java/tracking/EcommerceItem.java index 65b21446..49c3a83c 100644 --- a/src/main/java/org/piwik/java/tracking/EcommerceItem.java +++ b/src/main/java/org/piwik/java/tracking/EcommerceItem.java @@ -4,22 +4,31 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ -package org.piwik.java.tracking; -import org.matomo.java.tracking.MatomoRequest; +package org.piwik.java.tracking; /** + * Describes an item in an ecommerce transaction. + * * @author brettcsorba - * @deprecated Use {@link org.matomo.java.tracking.EcommerceItem} instead. + * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. */ @Deprecated -public class EcommerceItem extends org.matomo.java.tracking.EcommerceItem { +public class EcommerceItem extends org.matomo.java.tracking.parameters.EcommerceItem { /** - * @deprecated Use {@link MatomoRequest} instead. + * Creates a new ecommerce item. + * + * @param sku the sku (Stock Keeping Unit) of the item. + * @param name the name of the item. + * @param category the category of the item. + * @param price the price of the item. + * @param quantity the quantity of the item. + * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. */ @Deprecated public EcommerceItem(String sku, String name, String category, Double price, Integer quantity) { super(sku, name, category, price, quantity); } + } diff --git a/src/main/java/org/piwik/java/tracking/PiwikDate.java b/src/main/java/org/piwik/java/tracking/PiwikDate.java index 6105bd8d..c463543b 100644 --- a/src/main/java/org/piwik/java/tracking/PiwikDate.java +++ b/src/main/java/org/piwik/java/tracking/PiwikDate.java @@ -4,39 +4,47 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.piwik.java.tracking; import org.matomo.java.tracking.MatomoDate; -import java.time.ZoneId; +import java.time.Instant; +import java.time.ZonedDateTime; import java.util.TimeZone; /** + * A date object that can be used to send dates to Matomo. This class is deprecated and will be removed in a future. + * * @author brettcsorba - * @deprecated Use {@link MatomoDate} instead. + * @deprecated Please use {@link Instant} */ @Deprecated public class PiwikDate extends MatomoDate { /** - * @author brettcsorba - * @deprecated Use {@link MatomoDate} instead. + * Creates a new date object with the current time. + * + * @deprecated Use {@link Instant} instead. */ public PiwikDate() { - super(); } /** - * @author brettcsorba - * @deprecated Use {@link MatomoDate} instead. + * Creates a new date object with the specified time. The time is specified in milliseconds since the epoch. + * + * @param epochMilli The time in milliseconds since the epoch + * @deprecated Use {@link Instant} instead. */ public PiwikDate(long epochMilli) { super(epochMilli); } /** - * @author brettcsorba - * @deprecated Use {@link MatomoDate#setTimeZone(ZoneId)} instead. + * Sets the time zone for this date object. This is used to convert the date to UTC before sending it to Matomo. + * + * @param zone the time zone to use + * @deprecated Use {@link ZonedDateTime#toInstant()} instead. */ @Deprecated public void setTimeZone(TimeZone zone) { diff --git a/src/main/java/org/piwik/java/tracking/PiwikLocale.java b/src/main/java/org/piwik/java/tracking/PiwikLocale.java index 686bbfd7..6b5bf1f9 100644 --- a/src/main/java/org/piwik/java/tracking/PiwikLocale.java +++ b/src/main/java/org/piwik/java/tracking/PiwikLocale.java @@ -4,21 +4,28 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.piwik.java.tracking; -import org.matomo.java.tracking.MatomoLocale; +import org.matomo.java.tracking.parameters.Country; import java.util.Locale; /** + * A locale object that can be used to send visitor country to Matomo. This class is deprecated and will be removed in + * the future. + * * @author brettcsorba - * @deprecated Use {@link org.matomo.java.tracking.MatomoLocale} instead. + * @deprecated Use {@link Country} instead. */ @Deprecated -public class PiwikLocale extends MatomoLocale { +public class PiwikLocale extends Country { /** - * @deprecated Use {@link MatomoLocale} instead. + * Creates a new Piwik locale object with the specified locale. + * + * @param locale the locale to use + * @deprecated Use {@link Country} instead. */ @Deprecated public PiwikLocale(Locale locale) { diff --git a/src/main/java/org/piwik/java/tracking/PiwikRequest.java b/src/main/java/org/piwik/java/tracking/PiwikRequest.java index bab72b01..e4d64fe4 100644 --- a/src/main/java/org/piwik/java/tracking/PiwikRequest.java +++ b/src/main/java/org/piwik/java/tracking/PiwikRequest.java @@ -4,14 +4,18 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.piwik.java.tracking; -import lombok.NonNull; -import org.matomo.java.tracking.MatomoRequest; +import static java.util.Objects.requireNonNull; import java.net.URL; +import org.matomo.java.tracking.MatomoRequest; /** + * A request object that can be used to send requests to Matomo. This class is deprecated and will be removed in the + * future. + * * @author brettcsorba * @deprecated Use {@link MatomoRequest} instead. */ @@ -19,10 +23,14 @@ public class PiwikRequest extends MatomoRequest { /** + * Creates a new request object with the specified site ID and action URL. + * + * @param siteId the site ID + * @param actionUrl the action URL. Must not be null. * @deprecated Use {@link MatomoRequest} instead. */ @Deprecated - public PiwikRequest(int siteId, @NonNull URL actionUrl) { - super(siteId, actionUrl.toString()); + public PiwikRequest(int siteId, URL actionUrl) { + super(siteId, requireNonNull(actionUrl, "Action URL must not be null").toString()); } } diff --git a/src/main/java/org/piwik/java/tracking/PiwikTracker.java b/src/main/java/org/piwik/java/tracking/PiwikTracker.java index 4e91e0b0..95acad84 100644 --- a/src/main/java/org/piwik/java/tracking/PiwikTracker.java +++ b/src/main/java/org/piwik/java/tracking/PiwikTracker.java @@ -4,11 +4,14 @@ * @link https://github.com/matomo/matomo-java-tracker * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause */ + package org.piwik.java.tracking; import org.matomo.java.tracking.MatomoTracker; /** + * Creates a new PiwikTracker instance. This class is deprecated and will be removed in the future. + * * @author brettcsorba * @deprecated Use {@link MatomoTracker} instead. */ @@ -16,34 +19,53 @@ public class PiwikTracker extends MatomoTracker { /** + * Creates a new PiwikTracker instance with the given host URL. + * + * @param hostUrl the host URL of the Matomo server * @deprecated Use {@link MatomoTracker} instead. */ @Deprecated - public PiwikTracker(final String hostUrl) { + public PiwikTracker(String hostUrl) { super(hostUrl); } /** + * Creates a new PiwikTracker instance with the given host URL and timeout in milliseconds. Use -1 for no timeout. + * + * @param hostUrl the host URL of the Matomo server + * @param timeout the timeout in milliseconds or -1 for no timeout * @deprecated Use {@link MatomoTracker} instead. */ @Deprecated - public PiwikTracker(final String hostUrl, final int timeout) { + public PiwikTracker(String hostUrl, int timeout) { super(hostUrl, timeout); } /** + * Creates a new PiwikTracker instance with the given host URL and proxy settings. + * + * @param hostUrl the host URL of the Matomo server + * @param proxyHost the proxy host + * @param proxyPort the proxy port * @deprecated Use {@link MatomoTracker} instead. */ @Deprecated - public PiwikTracker(final String hostUrl, final String proxyHost, final int proxyPort) { + public PiwikTracker(String hostUrl, String proxyHost, int proxyPort) { super(hostUrl, proxyHost, proxyPort); } /** + * Creates a new PiwikTracker instance with the given host URL, proxy settings and timeout in milliseconds. Use -1 for + * no timeout. + * + * @param hostUrl the host URL of the Matomo server + * @param proxyHost the proxy host + * @param proxyPort the proxy port + * @param timeout the timeout in milliseconds or -1 for no timeout * @deprecated Use {@link MatomoTracker} instead. */ @Deprecated - public PiwikTracker(final String hostUrl, final String proxyHost, final int proxyPort, final int timeout) { + public PiwikTracker(String hostUrl, String proxyHost, int proxyPort, int timeout) { super(hostUrl, proxyHost, proxyPort, timeout); } diff --git a/src/main/java/org/piwik/java/tracking/package-info.java b/src/main/java/org/piwik/java/tracking/package-info.java new file mode 100644 index 00000000..c6427194 --- /dev/null +++ b/src/main/java/org/piwik/java/tracking/package-info.java @@ -0,0 +1,6 @@ +/** + * Piwik Java Tracking API. Renamed to {@link org.matomo.java.tracking} in 3.0.0. + * + */ + +package org.piwik.java.tracking; diff --git a/src/test/java/org/matomo/java/tracking/AuthTokenTest.java b/src/test/java/org/matomo/java/tracking/AuthTokenTest.java new file mode 100644 index 00000000..da161596 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/AuthTokenTest.java @@ -0,0 +1,68 @@ +package org.matomo.java.tracking; + +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import org.junit.jupiter.api.Test; + +class AuthTokenTest { + + @Test + void determineAuthTokenReturnsAuthTokenFromRequest() { + + MatomoRequest request = MatomoRequest.builder().authToken("bdeca231a312ab12cde124131bedfa23") + .build(); + + String authToken = AuthToken.determineAuthToken(null, singleton(request), null); + + assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); + + } + + @Test + void determineAuthTokenReturnsAuthTokenFromTrackerConfiguration() { + + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder() + .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo.")) + .defaultAuthToken("bdeca231a312ab12cde124131bedfa23") + .build(); + + String authToken = AuthToken.determineAuthToken(null, null, trackerConfiguration); + + assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); + } + + @Test + void determineAuthTokenFromTrackerConfigurationIfRequestTokenIsEmpty() { + + MatomoRequest request = MatomoRequest.builder() + .authToken("").build(); + + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder() + .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo")) + .defaultAuthToken("bdeca231a312ab12cde124131bedfa23").build(); + + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + + assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); + + } + + @Test + void determineAuthTokenFromTrackerConfigurationIfRequestTokenIsBlank() { + + MatomoRequest request = MatomoRequest.builder() + .authToken(" ").build(); + + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder() + .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo")) + .defaultAuthToken("bdeca231a312ab12cde124131bedfa23").build(); + + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + + assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); + + } + +} diff --git a/src/test/java/org/matomo/java/tracking/CustomVariableTest.java b/src/test/java/org/matomo/java/tracking/CustomVariableTest.java index 9818bc8d..a9499bda 100644 --- a/src/test/java/org/matomo/java/tracking/CustomVariableTest.java +++ b/src/test/java/org/matomo/java/tracking/CustomVariableTest.java @@ -1,54 +1,37 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ package org.matomo.java.tracking; -import org.junit.Before; -import org.junit.Test; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; +import org.junit.jupiter.api.Test; -/** - * @author Katie - */ -public class CustomVariableTest { - private CustomVariable customVariable; - - @Before - public void setUp() { - customVariable = new CustomVariable("key", "value"); - } +class CustomVariableTest { @Test - public void testConstructorNullKey() { - try { - new CustomVariable(null, null); - fail("Exception should have been throw."); - } catch (NullPointerException e) { - assertEquals("key is marked non-null but is null", e.getLocalizedMessage()); - } + void createsCustomVariable() { + CustomVariable customVariable = new CustomVariable("key", "value"); + + assertThat(customVariable.getKey()).isEqualTo("key"); + assertThat(customVariable.getValue()).isEqualTo("value"); } @Test - public void testConstructorNullValue() { - try { - new CustomVariable("key", null); - fail("Exception should have been throw."); - } catch (NullPointerException e) { - assertEquals("value is marked non-null but is null", e.getLocalizedMessage()); - } + void failsOnNullKey() { + assertThatThrownBy(() -> new CustomVariable(null, "value")).isInstanceOf( + NullPointerException.class); } @Test - public void testGetKey() { - assertEquals("key", customVariable.getKey()); + void failsOnNullValue() { + assertThatThrownBy(() -> new CustomVariable("key", null)).isInstanceOf( + NullPointerException.class); } @Test - public void testGetValue() { - assertEquals("value", customVariable.getValue()); + void failsOnNullKeyAndValue() { + assertThatThrownBy(() -> new CustomVariable(null, null)).isInstanceOf( + NullPointerException.class); } + + } diff --git a/src/test/java/org/matomo/java/tracking/CustomVariablesTest.java b/src/test/java/org/matomo/java/tracking/CustomVariablesTest.java deleted file mode 100644 index bdc4e8d1..00000000 --- a/src/test/java/org/matomo/java/tracking/CustomVariablesTest.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -package org.matomo.java.tracking; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -/** - * @author Katie - */ -public class CustomVariablesTest { - private final CustomVariables customVariables = new CustomVariables(); - - @Test - public void testAdd_CustomVariable() { - CustomVariable a = new CustomVariable("a", "b"); - CustomVariable b = new CustomVariable("c", "d"); - CustomVariable c = new CustomVariable("a", "e"); - CustomVariable d = new CustomVariable("a", "f"); - - assertTrue(customVariables.isEmpty()); - customVariables.add(a); - assertFalse(customVariables.isEmpty()); - assertEquals("b", customVariables.get("a")); - assertEquals(a, customVariables.get(1)); - assertEquals("{\"1\":[\"a\",\"b\"]}", customVariables.toString()); - - customVariables.add(b); - assertEquals("d", customVariables.get("c")); - assertEquals(b, customVariables.get(2)); - assertEquals("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"]}", customVariables.toString()); - - customVariables.add(c, 5); - assertEquals("b", customVariables.get("a")); - assertEquals(c, customVariables.get(5)); - assertNull(customVariables.get(3)); - assertEquals("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"e\"]}", customVariables.toString()); - - customVariables.add(d); - assertEquals("f", customVariables.get("a")); - assertEquals(d, customVariables.get(1)); - assertEquals(d, customVariables.get(5)); - assertEquals("{\"1\":[\"a\",\"f\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"f\"]}", customVariables.toString()); - - customVariables.remove("a"); - assertNull(customVariables.get("a")); - assertNull(customVariables.get(1)); - assertNull(customVariables.get(5)); - assertEquals("{\"2\":[\"c\",\"d\"]}", customVariables.toString()); - - customVariables.remove(2); - assertNull(customVariables.get("c")); - assertNull(customVariables.get(2)); - assertTrue(customVariables.isEmpty()); - assertEquals("{}", customVariables.toString()); - } - - @Test - public void testAddCustomVariableIndexLessThan1() { - try { - customVariables.add(new CustomVariable("a", "b"), 0); - fail("Exception should have been throw."); - } catch (IllegalArgumentException e) { - assertEquals("Index must be greater than 0.", e.getLocalizedMessage()); - } - } - - @Test - public void testGetCustomVariableIntegerLessThan1() { - try { - customVariables.get(0); - fail("Exception should have been throw."); - } catch (IllegalArgumentException e) { - assertEquals("Index must be greater than 0.", e.getLocalizedMessage()); - } - } -} diff --git a/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java b/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java new file mode 100644 index 00000000..81dd76ed --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java @@ -0,0 +1,37 @@ +package org.matomo.java.tracking; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.ThreadFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +class DaemonThreadFactoryTest { + + private final ThreadFactory daemonThreadFactory = new DaemonThreadFactory(); + + private Thread thread; + + @Test + void threadIsDaemonThread() { + + whenCreatesThread(); + + assertThat(thread.isDaemon()).isTrue(); + + } + + private void whenCreatesThread() { + thread = daemonThreadFactory.newThread(null); + } + + @Test + void threadHasName() { + + whenCreatesThread(); + + assertThat(thread.getName()).isEqualTo("MatomoJavaTracker"); + + } + +} diff --git a/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java b/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java index 986fb305..97349e6c 100644 --- a/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java +++ b/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java @@ -1,108 +1,68 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ package org.matomo.java.tracking; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -/** - * @author brettcsorba - */ -public class EcommerceItemTest { - EcommerceItem ecommerceItem; +class EcommerceItemTest { - public EcommerceItemTest() { - } - - @BeforeClass - public static void setUpClass() { - } - - @AfterClass - public static void tearDownClass() { - } - - @Before - public void setUp() { - ecommerceItem = new EcommerceItem(null, null, null, null, null); - } - - @After - public void tearDown() { - } + private EcommerceItem ecommerceItem = new EcommerceItem(null, null, null, null, null); /** * Test of constructor, of class EcommerceItem. */ @Test - public void testConstructor() { - EcommerceItem ecommerceItem = new EcommerceItem("sku", "name", "category", 1.0, 1); - - assertEquals("sku", ecommerceItem.getSku()); - assertEquals("name", ecommerceItem.getName()); - assertEquals("category", ecommerceItem.getCategory()); - assertEquals(new Double(1.0), ecommerceItem.getPrice()); - assertEquals(new Integer(1), ecommerceItem.getQuantity()); + void testConstructor() { + EcommerceItem ecommerceItem = new EcommerceItem("sku", "name", "category", 2.0, 2); + assertThat(ecommerceItem.getSku()).isEqualTo("sku"); + assertThat(ecommerceItem.getName()).isEqualTo("name"); + assertThat(ecommerceItem.getCategory()).isEqualTo("category"); + assertThat(ecommerceItem.getPrice()).isEqualTo(2.0); + assertThat(ecommerceItem.getQuantity()).isEqualTo(2); } /** * Test of getSku method, of class EcommerceItem. */ @Test - public void testGetSku() { + void testGetSku() { ecommerceItem.setSku("sku"); - - assertEquals("sku", ecommerceItem.getSku()); + assertThat(ecommerceItem.getSku()).isEqualTo("sku"); } /** * Test of getName method, of class EcommerceItem. */ @Test - public void testGetName() { + void testGetName() { ecommerceItem.setName("name"); - - assertEquals("name", ecommerceItem.getName()); + assertThat(ecommerceItem.getName()).isEqualTo("name"); } /** * Test of getCategory method, of class EcommerceItem. */ @Test - public void testGetCategory() { + void testGetCategory() { ecommerceItem.setCategory("category"); - - assertEquals("category", ecommerceItem.getCategory()); + assertThat(ecommerceItem.getCategory()).isEqualTo("category"); } /** * Test of getPrice method, of class EcommerceItem. */ @Test - public void testGetPrice() { - ecommerceItem.setPrice(1.0); - - assertEquals(new Double(1.0), ecommerceItem.getPrice()); + void testGetPrice() { + ecommerceItem.setPrice(2.0); + assertThat(ecommerceItem.getPrice()).isEqualTo(2.0); } /** * Test of getQuantity method, of class EcommerceItem. */ @Test - public void testGetQuantity() { - ecommerceItem.setQuantity(1); - - assertEquals(new Integer(1), ecommerceItem.getQuantity()); + void testGetQuantity() { + ecommerceItem.setQuantity(2); + assertThat(ecommerceItem.getQuantity()).isEqualTo(2); } - - } diff --git a/src/test/java/org/matomo/java/tracking/EcommerceItemsTest.java b/src/test/java/org/matomo/java/tracking/EcommerceItemsTest.java deleted file mode 100644 index ee76b7ce..00000000 --- a/src/test/java/org/matomo/java/tracking/EcommerceItemsTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.matomo.java.tracking; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class EcommerceItemsTest { - - @Test - public void formatsJson() { - - EcommerceItems ecommerceItems = new EcommerceItems(); - ecommerceItems.add(new EcommerceItem("sku", "name", "category", 1.0, 1)); - - assertEquals("[[\"sku\",\"name\",\"category\",1.0,1]]", ecommerceItems.toString()); - - } -} diff --git a/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java b/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java new file mode 100644 index 00000000..17605f04 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java @@ -0,0 +1,18 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class InvalidUrlExceptionTest { + + @Test + void createsInvalidUrlException() { + InvalidUrlException invalidUrlException = new InvalidUrlException(new Throwable()); + + assertThat(invalidUrlException).isNotNull(); + assertThat(invalidUrlException.getCause()).isNotNull(); + + } + +} diff --git a/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java b/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java new file mode 100644 index 00000000..9c998a37 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java @@ -0,0 +1,25 @@ +package org.matomo.java.tracking; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class MatomoExceptionTest { + + @Test + void createsMatomoExceptionWithMessage() { + MatomoException matomoException = new MatomoException("message"); + + assertEquals("message", matomoException.getMessage()); + } + + @Test + void createsMatomoExceptionWithMessageAndCause() { + Throwable cause = new Throwable(); + MatomoException matomoException = new MatomoException("message", cause); + + assertEquals("message", matomoException.getMessage()); + assertEquals(cause, matomoException.getCause()); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java b/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java new file mode 100644 index 00000000..9dce779e --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java @@ -0,0 +1,23 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Locale; +import org.junit.jupiter.api.Test; + +class MatomoLocaleTest { + + @Test + void createsMatomoLocaleFromLocale() { + MatomoLocale locale = new MatomoLocale(Locale.US); + assertThat(locale.toString()).isEqualTo("us"); + } + + @Test + void failsIfLocaleIsNull() { + assertThatThrownBy(() -> new MatomoLocale(null)).isInstanceOf(NullPointerException.class) + .hasMessage("Locale must not be null"); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java b/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java index 9d2804ed..ec2a71d0 100644 --- a/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java +++ b/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java @@ -1,49 +1,43 @@ package org.matomo.java.tracking; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.CustomVariables; -import java.util.Collection; import java.util.Collections; -import java.util.Map; -import static org.hamcrest.CoreMatchers.hasItem; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; -public class MatomoRequestBuilderTest { + +class MatomoRequestBuilderTest { @Test - public void buildsRequest() { - - CustomVariable customVariable = new CustomVariable("pageCustomVariableName", "pageCustomVariableValue"); - MatomoRequest matomoRequest = MatomoRequest.builder() - .siteId(42) - .actionName("ACTION_NAME") - .actionUrl("https://www.your-domain.tld/some/page?query=foo") - .referrerUrl("https://referrer.com") - .customTrackingParameters(Collections.singletonMap("trackingParameterName", "trackingParameterValue")) - .pageCustomVariables(Collections.singletonList(customVariable)) - .visitCustomVariables(Collections.singletonList(customVariable)) - .customAction(true) - .build(); - - Map> parameters = matomoRequest.getParameters(); - assertThat(parameters.get("idsite"), hasItem(42)); - assertThat(parameters.get("action_name"), hasItem("ACTION_NAME")); - assertThat(parameters.get("apiv"), hasItem("1")); - assertThat(parameters.get("url"), hasItem("https://www.your-domain.tld/some/page?query=foo")); - assertThat(parameters.get("_id").isEmpty(), is(false)); - assertThat(parameters.get("rand").isEmpty(), is(false)); - assertThat(parameters.get("send_image"), hasItem(new MatomoBoolean(false))); - assertThat(parameters.get("rec"), hasItem(new MatomoBoolean(true))); - assertThat(parameters.get("urlref"), hasItem("https://referrer.com")); - assertThat(parameters.get("trackingParameterName"), hasItem("trackingParameterValue")); - CustomVariables customVariables = new CustomVariables(); - customVariables.add(customVariable); - assertThat(parameters.get("cvar"), hasItem(customVariables)); - assertThat(parameters.get("_cvar"), hasItem(customVariables)); - assertThat(parameters.get("ca"), hasItem(new MatomoBoolean(true))); + void buildsRequest() { + CustomVariable pageCustomVariable = new CustomVariable("pageCustomVariableName", "pageCustomVariableValue"); + CustomVariable visitCustomVariable = new CustomVariable("visitCustomVariableName", "visitCustomVariableValue"); + + MatomoRequest matomoRequest = MatomoRequest.builder().siteId(42).actionName("ACTION_NAME") + .actionUrl("https://www.your-domain.tld/some/page?query=foo").referrerUrl("https://referrer.com") + .customTrackingParameters(Collections.singletonMap("trackingParameterName", singleton("trackingParameterValue"))) + .pageCustomVariables(new CustomVariables().add(pageCustomVariable, 2)) + .visitCustomVariables(new CustomVariables().add(visitCustomVariable, 3)).customAction(true).build(); + + assertThat(matomoRequest.getSiteId()).isEqualTo(42); + assertThat(matomoRequest.getActionName()).isEqualTo("ACTION_NAME"); + assertThat(matomoRequest.getApiVersion()).isEqualTo("1"); + assertThat(matomoRequest.getActionUrl()).isEqualTo("https://www.your-domain.tld/some/page?query=foo"); + assertThat(matomoRequest.getVisitorId().toString()).hasSize(16).isHexadecimal(); + assertThat(matomoRequest.getRandomValue().toString()).hasSize(20).isHexadecimal(); + assertThat(matomoRequest.getResponseAsImage()).isFalse(); + assertThat(matomoRequest.getRequired()).isTrue(); + assertThat(matomoRequest.getReferrerUrl()).isEqualTo("https://referrer.com"); + assertThat(matomoRequest.getCustomTrackingParameter("trackingParameterName")) + .containsExactly("trackingParameterValue"); + assertThat(matomoRequest.getPageCustomVariables()) + .hasToString("{\"2\":[\"pageCustomVariableName\",\"pageCustomVariableValue\"]}"); + assertThat(matomoRequest.getVisitCustomVariables()) + .hasToString("{\"3\":[\"visitCustomVariableName\",\"visitCustomVariableValue\"]}"); + assertThat(matomoRequest.getCustomAction()).isTrue(); } - } diff --git a/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java b/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java new file mode 100644 index 00000000..45abd2ac --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java @@ -0,0 +1,95 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class MatomoRequestTest { + + private MatomoRequest request = new MatomoRequest(); + + @Test + void returnsEmptyListWhenCustomTrackingParametersDoesNotContainKey() { + + request.setCustomTrackingParameter("foo", "bar"); + + assertThat(request.getCustomTrackingParameter("baz")).isEmpty(); + assertThat(request.getCustomTrackingParameters()).isNotEmpty(); + assertThat(request.getCustomTrackingParameter("foo")).isNotEmpty(); + } + + @Test + void getPageCustomVariableReturnsNullIfPageCustomVariablesIsNull() { + assertThat(request.getPageCustomVariable("foo")).isNull(); + } + + @Test + void getPageCustomVariableReturnsValueIfPageCustomVariablesIsNotNull() { + request.setPageCustomVariable("foo", "bar"); + assertThat(request.getPageCustomVariable("foo")).isEqualTo("bar"); + } + + @Test + void setPageCustomVariableRequiresNonNullKey() { + assertThatThrownBy(() -> request.setPageCustomVariable(null, "bar")) + .isInstanceOf(NullPointerException.class); + } + + @Test + void setPageCustomVariableDoesNothingIfValueIsNull() { + request.setPageCustomVariable("foo", null); + assertThat(request.getPageCustomVariable("foo")).isNull(); + } + + @Test + void setPageCustomVariableRemovesValueIfValueIsNull() { + request.setPageCustomVariable("foo", "bar"); + request.setPageCustomVariable("foo", null); + assertThat(request.getPageCustomVariable("foo")).isNull(); + } + + @Test + void setPageCustomVariableAddsCustomVariableIfValueIsNotNull() { + request.setPageCustomVariable("foo", "bar"); + assertThat(request.getPageCustomVariable("foo")).isEqualTo("bar"); + } + + @Test + void setPageCustomVariableDoesNothingIfCustomVariableParameterIsNullAndIndexIsPositive() { + request.setPageCustomVariable(null, 1); + assertThat(request.getPageCustomVariable(1)).isNull(); + } + + @Test + void setPageCustomVariableInitializesPageCustomVariablesIfCustomVariableParameterIsNullAndIndexIsPositive() { + request.setPageCustomVariable(new CustomVariable("key", "value"), 1); + assertThat(request.getPageCustomVariables()).isNotNull(); + } + + @Test + void setUserCustomVariableDoesNothingIfValueIsNull() { + request.setUserCustomVariable("foo", null); + assertThat(request.getUserCustomVariable("foo")).isNull(); + } + + @Test + void setUserCustomVariableRemovesValueIfValueIsNull() { + request.setUserCustomVariable("foo", "bar"); + request.setUserCustomVariable("foo", null); + assertThat(request.getUserCustomVariable("foo")).isNull(); + } + + @Test + void setVisitCustomVariableDoesNothingIfCustomVariableParameterIsNullAndIndexIsPositive() { + request.setVisitCustomVariable(null, 1); + assertThat(request.getVisitCustomVariable(1)).isNull(); + } + + @Test + void setVisitCustomVariableInitializesVisitCustomVariablesIfCustomVariableParameterIsNullAndIndexIsPositive() { + request.setVisitCustomVariable(new CustomVariable("key", "value"), 1); + assertThat(request.getVisitCustomVariables()).isNotNull(); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java b/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java new file mode 100644 index 00000000..1e18b942 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/MatomoTrackerIT.java @@ -0,0 +1,526 @@ +package org.matomo.java.tracking; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.URI; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Locale.LanguageRange; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.MatomoRequest.MatomoRequestBuilder; +import org.matomo.java.tracking.TrackerConfiguration.TrackerConfigurationBuilder; +import org.matomo.java.tracking.parameters.AcceptLanguage; +import org.matomo.java.tracking.parameters.Country; +import org.matomo.java.tracking.parameters.CustomVariable; +import org.matomo.java.tracking.parameters.CustomVariables; +import org.matomo.java.tracking.parameters.DeviceResolution; +import org.matomo.java.tracking.parameters.EcommerceItem; +import org.matomo.java.tracking.parameters.EcommerceItems; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.UniqueId; +import org.matomo.java.tracking.parameters.VisitorId; + +class MatomoTrackerIT { + + private static final WireMockServer wireMockServer = new WireMockServer( + WireMockConfiguration.options().dynamicPort()); + + private static final int SITE_ID = 42; + + private final TrackerConfigurationBuilder trackerConfigurationBuilder = + TrackerConfiguration.builder(); + + private final MatomoRequestBuilder requestBuilder = + MatomoRequest.builder().visitorId(VisitorId.fromHex("bbccddeeff1122")) + .randomValue(RandomValue.fromString("someRandom")); + + private CompletableFuture future; + + @BeforeAll + static void beforeAll() { + wireMockServer.start(); + } + + @BeforeEach + void givenStub() { + wireMockServer.resetRequests(); + wireMockServer.stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + wireMockServer.stubFor(get(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + } + + @Test + void requiresApiEndpoint() { + + assertThatThrownBy(() -> trackerConfigurationBuilder.defaultSiteId(SITE_ID).build()) + .isInstanceOf( + NullPointerException.class).hasMessage("apiEndpoint is marked non-null but is null"); + + } + + @Test + void requiresSiteId() { + + trackerConfigurationBuilder.apiEndpoint(URI.create("http://localhost:8099/matomo.php")).build(); + + assertThatThrownBy(this::whenSendsRequestAsync).isInstanceOf(IllegalArgumentException.class) + .hasMessage("No default site ID and no request site ID is given"); + + } + + private void whenSendsRequestAsync() { + future = new MatomoTracker(trackerConfigurationBuilder.build()).sendRequestAsync( + requestBuilder.build()); + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void whenSendsSingleRequest() { + new MatomoTracker(trackerConfigurationBuilder.build()).sendRequest(requestBuilder.build()); + } + + private void whenSendsBulkRequest() { + new MatomoTracker(trackerConfigurationBuilder.build()).sendBulkRequest( + singleton(requestBuilder.build())); + } + + @Test + void usesDefaultSiteId() { + + givenTrackerConfigurationWithDefaultSiteId(); + + whenSendsRequestAsync(); + + thenGetsRequest("idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom"); + + } + + private void givenTrackerConfigurationWithDefaultSiteId() { + trackerConfigurationBuilder.apiEndpoint(URI.create(String.format( + "http://localhost:%s/matomo.php", wireMockServer.port()))).defaultSiteId(SITE_ID); + } + + private void thenGetsRequest(String expectedQuery) { + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify( + getRequestedFor(urlEqualTo(String.format("/matomo.php?%s", expectedQuery))) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))); + } + + @Test + void overridesDefaultSiteId() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.siteId(123); + + whenSendsRequestAsync(); + + thenGetsRequest("rec=1&idsite=123&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom"); + + } + + @Test + void validatesTokenAuth() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.authToken("invalid-token-auth"); + + assertThatThrownBy(this::whenSendsRequestAsync).hasRootCauseInstanceOf( + IllegalArgumentException.class) + .hasRootCauseMessage("Auth token must be exactly 32 characters long"); + + } + + @Test + void convertsTrueBooleanTo1() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.pluginFlash(true); + + whenSendsRequestAsync(); + + thenGetsRequest( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&fla=1&send_image=0&rand=someRandom" + ); + + } + + @Test + void convertsFalseBooleanTo0() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.pluginJava(false); + + whenSendsRequestAsync(); + + thenGetsRequest( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&java=0&send_image=0&rand=someRandom" + ); + + } + + @Test + void encodesUrl() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.actionUrl("https://www.daniel-heid.de/some/page?foo=bar"); + + whenSendsRequestAsync(); + + thenGetsRequest( + "idsite=42&rec=1&url=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fpage%3Ffoo%3Dbar&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom" + ); + + } + + @Test + void encodesReferrerUrl() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.referrerUrl("https://www.daniel-heid.de/some/referrer?foo2=bar2"); + + whenSendsRequestAsync(); + + thenGetsRequest( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Freferrer%3Ffoo2%3Dbar2&send_image=0&rand=someRandom" + ); + + } + + @Test + void encodesLink() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.outlinkUrl("https://www.daniel-heid.de/some/external/link#"); + + whenSendsBulkRequestAsync(); + + thenPostsRequestWithoutAuthToken( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&link=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fexternal%2Flink%23&send_image=0&rand=someRandom", + "156" + ); + + } + + private void whenSendsBulkRequestAsync() { + future = + new MatomoTracker(trackerConfigurationBuilder.build()).sendBulkRequestAsync( + singleton(requestBuilder.build())); + try { + future.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void thenPostsRequestWithoutAuthToken(String expectedQuery, String contentLength) { + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo(contentLength)) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody( + equalToJson("{\"requests\":[\"?" + expectedQuery + "\"]}"))); + } + + @Test + void encodesDownloadUrl() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.downloadUrl("https://www.daniel-heid.de/some/download.pdf"); + + whenSendsBulkRequestAsync(); + + thenPostsRequestWithoutAuthToken( + "idsite=42&rec=1&apiv=1&_id=00bbccddeeff1122&download=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fdownload.pdf&send_image=0&rand=someRandom", + "154" + ); + + } + + @Test + void getContainsHeaders() { + + givenTrackerConfigurationWithDefaultSiteId(); + + whenSendsRequestAsync(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(getRequestedFor(urlPathEqualTo("/matomo.php")) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))); + + } + + @Test + void postContainsHeaders() { + + givenTrackerConfigurationWithDefaultSiteId(); + + whenSendsBulkRequestAsync(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(postRequestedFor(urlPathEqualTo("/matomo.php")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Length", equalTo("90")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))); + + } + + @Test + void allowsToOverrideUserAgent() { + + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.userAgent("Mozilla/5.0"); + + whenSendsRequestAsync(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(getRequestedFor(urlPathEqualTo("/matomo.php")) + .withHeader("User-Agent", equalTo("Mozilla/5.0"))); + + } + + @Test + void tracksMinimalRequest() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.actionName("Help / Feedback").actionUrl("https://www.daniel-heid.de/portfolio") + .visitorId(VisitorId.fromHash(3434343434343434343L)).referrerUrl( + "https://www.daniel-heid.de/referrer") + .visitCustomVariables( + new CustomVariables() + .add(new CustomVariable("customVariable1Key", "customVariable1Value"), 4) + .add(new CustomVariable("customVariable2Key", "customVariable2Value"), 5)) + .visitorVisitCount(2) + .visitorFirstVisitTimestamp( + LocalDateTime.of(2022, 8, 9, 18, 34, 12).toInstant(ZoneOffset.UTC)) + .deviceResolution(DeviceResolution.builder().width(1024).height(768).build()) + .headerAcceptLanguage( + AcceptLanguage.builder().languageRange(new LanguageRange("de")).languageRange( + new LanguageRange("de-DE", 0.9)) + .languageRange(new LanguageRange("en", 0.8)).build()).pageViewId( + UniqueId.fromValue(999999999999999999L)) + .goalId(0).ecommerceRevenue(12.34).ecommerceItems( + EcommerceItems.builder().item( + org.matomo.java.tracking.parameters.EcommerceItem.builder().sku("SKU").build()) + .item(EcommerceItem.builder().sku("SKU").name("NAME").category("CATEGORY").price(123.4) + .build()).build()) + .authToken("fdf6e8461ea9de33176b222519627f78") + .visitorCountry( + Country.fromLanguageRanges("en-GB;q=0.7,de,de-DE;q=0.9,en;q=0.8,en-US;q=0.6")); + + whenSendsBulkRequestAsync(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")).withHeader( + "Content-Length", equalTo("711")) + .withHeader("Accept", equalTo("*/*")).withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody( + equalToJson("{\"requests\":[\"?" + + "idsite=42&rec=1&action_name=Help+%2F+Feedback&url=https%3A%2F%2Fwww.daniel-heid.de%2Fportfolio&apiv=1&_id=2fa93d2858bc4867&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Freferrer&_cvar=%7B%224%22%3A%5B%22customVariable1Key%22%2C%22customVariable1Value%22%5D%2C%225%22%3A%5B%22customVariable2Key%22%2C%22customVariable2Value%22%5D%7D&_idvc=2&_idts=1660070052&res=1024x768&lang=de%2Cde-de%3Bq%3D0.9%2Cen%3Bq%3D0.8&pv_id=lbBbxG&idgoal=0&revenue=12.34&ec_items=%5B%5B%22SKU%22%2C%22%22%2C%22%22%2C0.0%2C0%5D%2C%5B%22SKU%22%2C%22NAME%22%2C%22CATEGORY%22%2C123.4%2C0%5D%5D&token_auth=fdf6e8461ea9de33176b222519627f78&country=de&send_image=0&rand=someRandom" + + "\"],\"token_auth\" : \"" + "fdf6e8461ea9de33176b222519627f78" + "\"}"))); + + } + + @Test + void doesNothingIfNotEnabled() { + + wireMockServer.resetRequests(); + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsRequestAsync(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + + } + + @Test + void exampleWorks() { + + TrackerConfiguration config = + TrackerConfiguration.builder().apiEndpoint( + URI.create("https://your-domain.net/matomo/matomo.php")) + .defaultSiteId(42) // if not explicitly specified by action + .build(); + + // Prepare the tracker (stateless - can be used for multiple actions) + MatomoTracker tracker = new MatomoTracker(config); + + // Track an action + CompletableFuture future = tracker.sendRequestAsync( + MatomoRequest.builder().actionName("User Profile / Upload Profile Picture") + .actionUrl("https://your-domain.net/user/profile/picture") + .visitorId(VisitorId.fromHash("some@email-adress.org".hashCode())) + // ... + .build()); + + // If you want to ensure the request has been handled: + if (future.isCompletedExceptionally()) { + // log, throw, ... + } + } + + @Test + void reportsErrors() { + + wireMockServer.stubFor(get(urlPathEqualTo("/failing")).willReturn(status(500))); + trackerConfigurationBuilder.apiEndpoint(URI.create(String.format( + "http://localhost:%d/failing", + wireMockServer.port() + ))).defaultSiteId(SITE_ID); + + assertThatThrownBy(this::whenSendsRequestAsync).hasRootCauseInstanceOf(MatomoException.class) + .hasRootCauseMessage("Tracking endpoint responded with code 500"); + + assertThat(future).isCompletedExceptionally(); + + } + + @Test + void includesDefaultTokenAuth() { + + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.defaultAuthToken("fdf6e8461ea9de33176b222519627f78"); + + whenSendsRequestAsync(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify( + getRequestedFor( + urlEqualTo( + "/matomo.php?idsite=42token_auth=fdf6e8461ea9de33176b222519627f78&rec=1&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom")) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))); + + } + + @Test + void includesMultipleQueriesInBulkRequest() throws Exception { + + givenTrackerConfigurationWithDefaultSiteId(); + MatomoTracker tracker = new MatomoTracker(trackerConfigurationBuilder.build()); + + CompletableFuture future1 = tracker.sendBulkRequestAsync( + Arrays.asList( + requestBuilder.actionName("First").build(), + requestBuilder.actionName("Second").build(), + requestBuilder.actionName("Third").build() + )); + future1.get(); + + assertThat(future1).isNotCompletedExceptionally(); + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")).withHeader( + "Content-Length", equalTo("297")) + .withHeader("Accept", equalTo("*/*")).withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")).withRequestBody(equalToJson( + "{\"requests\" : [ \"?idsite=42&rec=1&action_name=First&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\", \"?idsite=42&rec=1&action_name=Second&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\", \"?idsite=42&rec=1&action_name=Third&apiv=1&_id=00bbccddeeff1122&send_image=0&rand=someRandom\" ]}"))); + + } + + @Test + void failsOnNegativeSiteId() { + + givenTrackerConfigurationWithDefaultSiteId(); + requestBuilder.siteId(-1); + + assertThatThrownBy(this::whenSendsRequestAsync).hasRootCauseInstanceOf( + IllegalArgumentException.class) + .hasRootCauseMessage("Site ID must not be negative"); + } + + @Test + void doesNotSendRequestAsyncIfTrackerConfigurationIsDisabled() { + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsRequestAsync(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(0, getRequestedFor(urlPathEqualTo("/matomo.php"))); + + } + + @Test + void doesNotSendRequestIfTrackerConfigurationIsDisabled() { + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsSingleRequest(); + + wireMockServer.verify(0, getRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + @Test + void doesNotSendBulkRequestIfTrackerConfigurationIsDisabled() { + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsBulkRequest(); + + wireMockServer.verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + @Test + void doesNotSendBulkRequestAsyncIfTrackerConfigurationIsDisabled() { + givenTrackerConfigurationWithDefaultSiteId(); + trackerConfigurationBuilder.enabled(false); + + whenSendsBulkRequestAsync(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(0, postRequestedFor(urlPathEqualTo("/matomo.php"))); + } + + @Test + void sendsRequestAsyncAndAcceptsCallback() throws Exception { + givenTrackerConfigurationWithDefaultSiteId(); + MatomoTracker tracker = new MatomoTracker(trackerConfigurationBuilder.build()); + AtomicBoolean success = new AtomicBoolean(); + CompletableFuture future = tracker.sendRequestAsync(requestBuilder.build(), v -> { + success.set(true); + }); + future.get(); + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(getRequestedFor(urlPathEqualTo("/matomo.php"))); + assertThat(success).isTrue(); + } + + @Test + void sendsRequestsAsyncAndAcceptsCallback() throws Exception { + givenTrackerConfigurationWithDefaultSiteId(); + MatomoTracker tracker = new MatomoTracker(trackerConfigurationBuilder.build()); + AtomicBoolean success = new AtomicBoolean(); + CompletableFuture future = tracker.sendBulkRequestAsync(singleton(requestBuilder.build()), v -> { + success.set(true); + }); + future.get(); + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(postRequestedFor(urlPathEqualTo("/matomo.php"))); + assertThat(success).isTrue(); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/PiwikDateTest.java b/src/test/java/org/matomo/java/tracking/PiwikDateTest.java index 0ba201d5..848b402d 100644 --- a/src/test/java/org/matomo/java/tracking/PiwikDateTest.java +++ b/src/test/java/org/matomo/java/tracking/PiwikDateTest.java @@ -1,56 +1,44 @@ -/* - * Matomo Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ package org.matomo.java.tracking; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.piwik.java.tracking.PiwikDate; import java.util.TimeZone; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.assertj.core.api.Assertions.assertThat; + + +class PiwikDateTest { -/** - * @author brettcsorba - */ -public class PiwikDateTest { /** * Test of constructor, of class PiwikDate. */ @Test - public void testConstructor0() { + void testConstructor0() { PiwikDate date = new PiwikDate(); - - assertNotNull(date); + assertThat(date).isNotNull(); } @Test - public void testConstructor1() { + void testConstructor1() { PiwikDate date = new PiwikDate(1433186085092L); + assertThat(date).isNotNull(); + assertThat(date.getTime()).isEqualTo(1433186085092L); + } - assertNotNull(date); - - assertEquals("2015-06-01 19:14:45", date.toString()); - - date = new PiwikDate(1467437553000L); - - assertEquals("2016-07-02 05:32:33", date.toString()); + @Test + void testConstructor2() { + PiwikDate date = new PiwikDate(1467437553000L); + assertThat(date.getTime()).isEqualTo(1467437553000L); } /** * Test of setTimeZone method, of class PiwikDate. */ @Test - public void testSetTimeZone() { + void testSetTimeZone() { PiwikDate date = new PiwikDate(1433186085092L); - date.setTimeZone(TimeZone.getTimeZone("America/New_York")); - - assertEquals("2015-06-01 15:14:45", date.toString()); + assertThat(date.getTime()).isEqualTo(1433186085092L); } - } diff --git a/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java b/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java index 1931e5e6..318e111d 100644 --- a/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java +++ b/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java @@ -1,71 +1,30 @@ -/* - * Piwik Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ package org.matomo.java.tracking; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.piwik.java.tracking.PiwikLocale; import java.util.Locale; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; -/** - * @author brettcsorba - */ -public class PiwikLocaleTest { - PiwikLocale locale; +class PiwikLocaleTest { - public PiwikLocaleTest() { - } - - @BeforeClass - public static void setUpClass() { - } - - @AfterClass - public static void tearDownClass() { - } - - @Before - public void setUp() { - locale = new PiwikLocale(Locale.US); - } - - @After - public void tearDown() { - } - - /** - * Test of getLocale method, of class PiwikLocale. - */ - @Test - public void testConstructor() { - assertEquals(Locale.US, locale.getLocale()); - } + private final PiwikLocale locale = new PiwikLocale(Locale.US); /** * Test of setLocale method, of class PiwikLocale. */ @Test - public void testLocale() { + void testLocale() { locale.setLocale(Locale.GERMANY); - assertEquals(Locale.GERMANY, locale.getLocale()); + assertThat(locale.getLocale()).isEqualTo(Locale.GERMAN); } /** * Test of toString method, of class PiwikLocale. */ @Test - public void testToString() { - assertEquals("us", locale.toString()); + void testToString() { + assertThat(locale).hasToString("us"); } - } diff --git a/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java b/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java index e50fc017..245bf4e3 100644 --- a/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java +++ b/src/test/java/org/matomo/java/tracking/PiwikRequestTest.java @@ -1,523 +1,349 @@ -/* - * Piwik Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ package org.matomo.java.tracking; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.piwik.java.tracking.PiwikDate; -import org.piwik.java.tracking.PiwikLocale; -import org.piwik.java.tracking.PiwikRequest; +import static java.time.temporal.ChronoUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.within; import java.net.URL; import java.nio.charset.Charset; +import java.time.Instant; +import java.time.ZonedDateTime; import java.util.List; import java.util.Locale; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.AcceptLanguage; +import org.matomo.java.tracking.parameters.DeviceResolution; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.VisitorId; +import org.piwik.java.tracking.PiwikDate; +import org.piwik.java.tracking.PiwikLocale; +import org.piwik.java.tracking.PiwikRequest; + +class PiwikRequestTest { -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -/** - * @author brettcsorba - */ -public class PiwikRequestTest { private PiwikRequest request; - @Before - public void setUp() throws Exception { + @BeforeEach + void setUp() throws Exception { request = new PiwikRequest(3, new URL("http://test.com")); } - @After - public void tearDown() { - } - @Test - public void testConstructor() throws Exception { + void testConstructor() throws Exception { request = new PiwikRequest(3, new URL("http://test.com")); - assertEquals(Integer.valueOf(3), request.getSiteId()); - assertTrue(request.getRequired()); - assertEquals(new URL("http://test.com"), request.getActionUrl()); - assertNotNull(request.getVisitorId()); - assertNotNull(request.getRandomValue()); - assertEquals("1", request.getApiVersion()); - assertFalse(request.getResponseAsImage()); + assertThat(request.getSiteId()).isEqualTo(Integer.valueOf(3)); + assertThat(request.getRequired()).isTrue(); + assertThat(request.getActionUrl()).isEqualTo("http://test.com"); + assertThat(request.getVisitorId()).isNotNull(); + assertThat(request.getRandomValue()).isNotNull(); + assertThat(request.getApiVersion()).isEqualTo("1"); + assertThat(request.getResponseAsImage()).isFalse(); } /** * Test of getActionName method, of class PiwikRequest. */ @Test - public void testActionName() { + void testActionName() { request.setActionName("action"); - assertEquals("action", request.getActionName()); + assertThat(request.getActionName()).isEqualTo("action"); request.setActionName(null); - assertNull(request.getActionName()); - } - - /** - * Test of getActionTime method, of class PiwikRequest. - */ - @Test - public void testActionTime() { - request.setActionTime(1000L); - assertEquals(Long.valueOf(1000L), request.getActionTime()); + assertThat(request.getActionName()).isNull(); } /** * Test of getActionUrl method, of class PiwikRequest. */ @Test - public void testActionUrl() throws Exception { - request.setActionUrl((String) null); - assertNull(request.getActionUrl()); - assertNull(request.getActionUrlAsString()); - - URL url = new URL("http://action.com"); - request.setActionUrl(url); - assertEquals(url, request.getActionUrl()); - assertEquals("http://action.com", request.getActionUrlAsString()); - - request.setActionUrlWithString(null); - assertNull(request.getActionUrl()); - assertNull(request.getActionUrlAsString()); - - request.setActionUrlWithString("http://actionstring.com"); - assertEquals("http://actionstring.com", request.getActionUrlAsString()); - assertEquals(new URL("http://actionstring.com"), request.getActionUrl()); + void testActionUrl() { + request.setActionUrl(null); + assertThat(request.getActionUrl()).isNull(); + request.setActionUrl("http://action.com"); + assertThat(request.getActionUrl()).isEqualTo("http://action.com"); } /** * Test of getApiVersion method, of class PiwikRequest. */ @Test - public void testApiVersion() { + void testApiVersion() { request.setApiVersion("2"); - assertEquals("2", request.getApiVersion()); + assertThat(request.getApiVersion()).isEqualTo("2"); } - /** - * Test of getAuthToken method, of class PiwikRequest. - */ @Test - public void testAuthTokenTT() { - try { - request.setAuthToken("1234"); - fail("Exception should have been thrown."); - } catch (IllegalArgumentException e) { - assertEquals("1234 is not 32 characters long.", e.getLocalizedMessage()); - } - } - - @Test - public void testAuthTokenTF() { + void testAuthTokenTF() { request.setAuthToken("12345678901234567890123456789012"); - assertEquals("12345678901234567890123456789012", request.getAuthToken()); + assertThat(request.getAuthToken()).isEqualTo("12345678901234567890123456789012"); } @Test - public void testAuthTokenF() { + void testAuthTokenF() { request.setAuthToken("12345678901234567890123456789012"); request.setAuthToken(null); - assertNull(request.getAuthToken()); - } - - /** - * Test of verifyAuthTokenSet method, of class PiwikRequest. - */ - @Test - public void testVerifyAuthTokenSet() { - try { - request.verifyAuthTokenSet(); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("AuthToken must be set before this value can be set.", e.getLocalizedMessage()); - } + assertThat(request.getAuthToken()).isNull(); } /** * Test of getCampaignKeyword method, of class PiwikRequest. */ @Test - public void testCampaignKeyword() { + void testCampaignKeyword() { request.setCampaignKeyword("keyword"); - assertEquals("keyword", request.getCampaignKeyword()); + assertThat(request.getCampaignKeyword()).isEqualTo("keyword"); } /** * Test of getCampaignName method, of class PiwikRequest. */ @Test - public void testCampaignName() { + void testCampaignName() { request.setCampaignName("name"); - assertEquals("name", request.getCampaignName()); + assertThat(request.getCampaignName()).isEqualTo("name"); } /** * Test of getCharacterSet method, of class PiwikRequest. */ @Test - public void testCharacterSet() { + void testCharacterSet() { Charset charset = Charset.defaultCharset(); request.setCharacterSet(charset); - assertEquals(charset, request.getCharacterSet()); + assertThat(request.getCharacterSet()).isEqualTo(charset); } /** * Test of getContentInteraction method, of class PiwikRequest. */ @Test - public void testContentInteraction() { + void testContentInteraction() { request.setContentInteraction("interaction"); - assertEquals("interaction", request.getContentInteraction()); + assertThat(request.getContentInteraction()).isEqualTo("interaction"); } /** * Test of getContentName method, of class PiwikRequest. */ @Test - public void testContentName() { + void testContentName() { request.setContentName("name"); - assertEquals("name", request.getContentName()); + assertThat(request.getContentName()).isEqualTo("name"); } /** * Test of getContentPiece method, of class PiwikRequest. */ @Test - public void testContentPiece() { + void testContentPiece() { request.setContentPiece("piece"); - assertEquals("piece", request.getContentPiece()); + assertThat(request.getContentPiece()).isEqualTo("piece"); } /** * Test of getContentTarget method, of class PiwikRequest. */ @Test - public void testContentTarget() throws Exception { - URL url = new URL("http://target.com"); - request.setContentTarget(url); - assertEquals(url, request.getContentTarget()); - assertEquals("http://target.com", request.getContentTargetAsString()); - - request.setContentTargetWithString("http://targetstring.com"); - assertEquals("http://targetstring.com", request.getContentTargetAsString()); - assertEquals(new URL("http://targetstring.com"), request.getContentTarget()); - + void testContentTarget() { + request.setContentTarget("http://target.com"); + assertThat(request.getContentTarget()).isEqualTo("http://target.com"); } /** * Test of getCurrentHour method, of class PiwikRequest. */ @Test - public void testCurrentHour() { + void testCurrentHour() { request.setCurrentHour(1); - assertEquals(Integer.valueOf(1), request.getCurrentHour()); + assertThat(request.getCurrentHour()).isEqualTo(Integer.valueOf(1)); } /** * Test of getCurrentMinute method, of class PiwikRequest. */ @Test - public void testCurrentMinute() { + void testCurrentMinute() { request.setCurrentMinute(2); - assertEquals(Integer.valueOf(2), request.getCurrentMinute()); + assertThat(request.getCurrentMinute()).isEqualTo(Integer.valueOf(2)); } /** * Test of getCurrentSecond method, of class PiwikRequest. */ @Test - public void testCurrentSecond() { + void testCurrentSecond() { request.setCurrentSecond(3); - assertEquals(Integer.valueOf(3), request.getCurrentSecond()); + assertThat(request.getCurrentSecond()).isEqualTo(Integer.valueOf(3)); } /** * Test of getCustomTrackingParameter method, of class PiwikRequest. */ @Test - public void testGetCustomTrackingParameter_T() { + void testGetCustomTrackingParameter_T() { try { request.getCustomTrackingParameter(null); fail("Exception should have been thrown."); } catch (NullPointerException e) { - assertEquals("key is marked non-null but is null", e.getLocalizedMessage()); + assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); } } @Test - public void testGetCustomTrackingParameter_FT() { - assertTrue(request.getCustomTrackingParameter("key").isEmpty()); + void testGetCustomTrackingParameter_FT() { + assertThat(request.getCustomTrackingParameter("key")).isEmpty(); } @Test - public void testSetCustomTrackingParameter_T() { + void testSetCustomTrackingParameter_T() { try { request.setCustomTrackingParameter(null, null); fail("Exception should have been thrown."); } catch (NullPointerException e) { - assertEquals("key is marked non-null but is null", e.getLocalizedMessage()); + assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); } } @Test - public void testSetCustomTrackingParameter_F() { + void testSetCustomTrackingParameter1() { request.setCustomTrackingParameter("key", "value"); - List l = request.getCustomTrackingParameter("key"); - assertEquals(1, l.size()); - assertEquals("value", l.get(0)); + List l = request.getCustomTrackingParameter("key"); + assertThat(l).hasSize(1); + assertThat(l.get(0)).isEqualTo("value"); + request.setCustomTrackingParameter("key", "value2"); + } + @Test + void testSetCustomTrackingParameter2() { request.setCustomTrackingParameter("key", "value2"); + List l = request.getCustomTrackingParameter("key"); + assertThat(l).hasSize(1); + assertThat(l.get(0)).isEqualTo("value2"); + request.setCustomTrackingParameter("key", null); l = request.getCustomTrackingParameter("key"); - assertEquals(1, l.size()); - assertEquals("value2", l.get(0)); + assertThat(l).isEmpty(); + } + @Test + void testSetCustomTrackingParameter3() { request.setCustomTrackingParameter("key", null); - l = request.getCustomTrackingParameter("key"); - assertTrue(l.isEmpty()); + List l = request.getCustomTrackingParameter("key"); + assertThat(l).isEmpty(); } @Test - public void testAddCustomTrackingParameter_T() { + void testAddCustomTrackingParameter_T() { try { request.addCustomTrackingParameter(null, null); fail("Exception should have been thrown."); } catch (NullPointerException e) { - assertEquals("key is marked non-null but is null", e.getLocalizedMessage()); + assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); } } @Test - public void testAddCustomTrackingParameter_FT() { + void testAddCustomTrackingParameter_FT() { try { request.addCustomTrackingParameter("key", null); fail("Exception should have been thrown."); } catch (NullPointerException e) { - assertEquals("value is marked non-null but is null", e.getLocalizedMessage()); + assertThat(e.getLocalizedMessage()).isEqualTo("value is marked non-null but is null"); } } @Test - public void testAddCustomTrackingParameter_FF() { + void testAddCustomTrackingParameter1() { request.addCustomTrackingParameter("key", "value"); - List l = request.getCustomTrackingParameter("key"); - assertEquals(1, l.size()); - assertEquals("value", l.get(0)); + List l = request.getCustomTrackingParameter("key"); + assertThat(l).hasSize(1); + assertThat(l.get(0)).isEqualTo("value"); + } + @Test + void testAddCustomTrackingParameter2() { + request.addCustomTrackingParameter("key", "value"); request.addCustomTrackingParameter("key", "value2"); - l = request.getCustomTrackingParameter("key"); - assertEquals(2, l.size()); - assertTrue(l.contains("value")); - assertTrue(l.contains("value2")); + List l = request.getCustomTrackingParameter("key"); + assertThat(l).hasSize(2) + .contains(new String[] {"value"}) + .contains(new String[] {"value2"}); } @Test - public void testClearCustomTrackingParameter() { + void testClearCustomTrackingParameter() { request.setCustomTrackingParameter("key", "value"); request.clearCustomTrackingParameter(); - List l = request.getCustomTrackingParameter("key"); - assertTrue(l.isEmpty()); + List l = request.getCustomTrackingParameter("key"); + assertThat(l).isEmpty(); } /** * Test of getDeviceResolution method, of class PiwikRequest. */ @Test - public void testDeviceResolution() { - request.setDeviceResolution("1x2"); - assertEquals("1x2", request.getDeviceResolution()); + void testDeviceResolution() { + request.setDeviceResolution(DeviceResolution.fromString("100x200")); + assertThat(request.getDeviceResolution()).hasToString("100x200"); } /** * Test of getDownloadUrl method, of class PiwikRequest. */ @Test - public void testDownloadUrl() throws Exception { - URL url = new URL("http://download.com"); - request.setDownloadUrl(url); - assertEquals(url, request.getDownloadUrl()); - assertEquals("http://download.com", request.getDownloadUrlAsString()); - - request.setDownloadUrlWithString("http://downloadstring.com"); - assertEquals("http://downloadstring.com", request.getDownloadUrlAsString()); - assertEquals(new URL("http://downloadstring.com"), request.getDownloadUrl()); + void testDownloadUrl() { + request.setDownloadUrl("http://download.com"); + assertThat(request.getDownloadUrl()).isEqualTo("http://download.com"); } /** * Test of enableEcommerce method, of class PiwikRequest. */ @Test - public void testEnableEcommerce() { - request.enableEcommerce(); - assertEquals(Integer.valueOf(0), request.getGoalId()); - } - - /** - * Test of verifyEcommerceEnabled method, of class PiwikRequest. - */ - @Test - public void testVerifyEcommerceEnabledT() { - try { - request.verifyEcommerceEnabled(); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } - } - - @Test - public void testVerifyEcommerceEnabledFT() { - try { - request.setGoalId(1); - request.verifyEcommerceEnabled(); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } - } - - @Test - public void testVerifyEcommerceEnabledFF() { - request.enableEcommerce(); - request.verifyEcommerceEnabled(); - } - - /** - * Test of verifyEcommerceState method, of class PiwikRequest. - */ - @Test - public void testVerifyEcommerceStateE() { - try { - request.verifyEcommerceState(); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } - } - - @Test - public void testVerifyEcommerceStateT() { - try { - request.enableEcommerce(); - request.verifyEcommerceState(); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("EcommerceId must be set before this value can be set.", - e.getLocalizedMessage()); - } - } - - @Test - public void testVerifyEcommerceStateFT() { - try { - request.enableEcommerce(); - request.setEcommerceId("1"); - request.verifyEcommerceState(); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("EcommerceRevenue must be set before this value can be set.", - e.getLocalizedMessage()); - } - } - - @Test - public void testVerifyEcommerceStateFF() { + void testEnableEcommerce() { request.enableEcommerce(); - request.setEcommerceId("1"); - request.setEcommerceRevenue(2.0); - request.verifyEcommerceState(); + assertThat(request.getGoalId()).isEqualTo(Integer.valueOf(0)); } /** * Test of getEcommerceDiscount method, of class PiwikRequest. */ @Test - public void testEcommerceDiscountT() { + void testEcommerceDiscountT() { request.enableEcommerce(); request.setEcommerceId("1"); request.setEcommerceRevenue(2.0); request.setEcommerceDiscount(1.0); - - assertEquals(Double.valueOf(1.0), request.getEcommerceDiscount()); + assertThat(request.getEcommerceDiscount()).isEqualTo(Double.valueOf(1.0)); } - @Test - public void testEcommerceDiscountTE() { - try { - request.setEcommerceDiscount(1.0); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } - } @Test - public void testEcommerceDiscountF() { + void testEcommerceDiscountF() { request.setEcommerceDiscount(null); - - assertNull(request.getEcommerceDiscount()); + assertThat(request.getEcommerceDiscount()).isNull(); } /** * Test of getEcommerceId method, of class PiwikRequest. */ @Test - public void testEcommerceIdT() { + void testEcommerceIdT() { request.enableEcommerce(); request.setEcommerceId("1"); - - assertEquals("1", request.getEcommerceId()); - } - - @Test - public void testEcommerceIdTE() { - try { - request.setEcommerceId("1"); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } + assertThat(request.getEcommerceId()).isEqualTo("1"); } @Test - public void testEcommerceIdF() { + void testEcommerceIdF() { request.setEcommerceId(null); - - assertNull(request.getEcommerceId()); - } - - /** - * Test of getEcommerceItem method, of class PiwikRequest. - */ - @Test - public void testEcommerceItemE() { - try { - EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 2); - request.addEcommerceItem(item); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } + assertThat(request.getEcommerceId()).isNull(); } @Test - public void testEcommerceItemE2() { + void testEcommerceItemE2() { try { request.enableEcommerce(); request.setEcommerceId("1"); @@ -525,1013 +351,603 @@ public void testEcommerceItemE2() { request.addEcommerceItem(null); fail("Exception should have been thrown."); } catch (NullPointerException e) { - assertEquals("item is marked non-null but is null", e.getLocalizedMessage()); + assertThat(e.getLocalizedMessage()).isEqualTo("item is marked non-null but is null"); } } @Test - public void testEcommerceItem() { - assertNull(request.getEcommerceItem(0)); - + void testEcommerceItem() { + assertThat(request.getEcommerceItem(0)).isNull(); EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 2); request.enableEcommerce(); request.setEcommerceId("1"); request.setEcommerceRevenue(2.0); request.addEcommerceItem(item); - - assertEquals(item, request.getEcommerceItem(0)); - + assertThat(request.getEcommerceItem(0)).isEqualTo(item); request.clearEcommerceItems(); - assertNull(request.getEcommerceItem(0)); + assertThat(request.getEcommerceItem(0)).isNull(); } /** * Test of getEcommerceLastOrderTimestamp method, of class PiwikRequest. */ @Test - public void testEcommerceLastOrderTimestampT() { + void testEcommerceLastOrderTimestampT() { request.enableEcommerce(); request.setEcommerceId("1"); request.setEcommerceRevenue(2.0); - request.setEcommerceLastOrderTimestamp(1000L); - - assertEquals(Long.valueOf(1000L), request.getEcommerceLastOrderTimestamp()); - } - - @Test - public void testEcommerceLastOrderTimestampTE() { - try { - request.setEcommerceLastOrderTimestamp(1000L); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } + request.setEcommerceLastOrderTimestamp(Instant.ofEpochSecond(1000L)); + assertThat(request.getEcommerceLastOrderTimestamp()).isEqualTo("1970-01-01T00:16:40Z"); } @Test - public void testEcommerceLastOrderTimestampF() { + void testEcommerceLastOrderTimestampF() { request.setEcommerceLastOrderTimestamp(null); - - assertNull(request.getEcommerceLastOrderTimestamp()); + assertThat(request.getEcommerceLastOrderTimestamp()).isNull(); } /** * Test of getEcommerceRevenue method, of class PiwikRequest. */ @Test - public void testEcommerceRevenueT() { + void testEcommerceRevenueT() { request.enableEcommerce(); request.setEcommerceId("1"); request.setEcommerceRevenue(20.0); - - assertEquals(Double.valueOf(20.0), request.getEcommerceRevenue()); + assertThat(request.getEcommerceRevenue()).isEqualTo(Double.valueOf(20.0)); } - @Test - public void testEcommerceRevenueTE() { - try { - request.setEcommerceRevenue(20.0); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } - } @Test - public void testEcommerceRevenueF() { + void testEcommerceRevenueF() { request.setEcommerceRevenue(null); - - assertNull(request.getEcommerceRevenue()); + assertThat(request.getEcommerceRevenue()).isNull(); } /** * Test of getEcommerceShippingCost method, of class PiwikRequest. */ @Test - public void testEcommerceShippingCostT() { + void testEcommerceShippingCostT() { request.enableEcommerce(); request.setEcommerceId("1"); request.setEcommerceRevenue(2.0); request.setEcommerceShippingCost(20.0); - - assertEquals(Double.valueOf(20.0), request.getEcommerceShippingCost()); - } - - @Test - public void testEcommerceShippingCostTE() { - try { - request.setEcommerceShippingCost(20.0); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } + assertThat(request.getEcommerceShippingCost()).isEqualTo(Double.valueOf(20.0)); } @Test - public void testEcommerceShippingCostF() { + void testEcommerceShippingCostF() { request.setEcommerceShippingCost(null); - - assertNull(request.getEcommerceShippingCost()); + assertThat(request.getEcommerceShippingCost()).isNull(); } /** * Test of getEcommerceSubtotal method, of class PiwikRequest. */ @Test - public void testEcommerceSubtotalT() { + void testEcommerceSubtotalT() { request.enableEcommerce(); request.setEcommerceId("1"); request.setEcommerceRevenue(2.0); request.setEcommerceSubtotal(20.0); - - assertEquals(Double.valueOf(20.0), request.getEcommerceSubtotal()); - } - - @Test - public void testEcommerceSubtotalTE() { - try { - request.setEcommerceSubtotal(20.0); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } + assertThat(request.getEcommerceSubtotal()).isEqualTo(Double.valueOf(20.0)); } @Test - public void testEcommerceSubtotalF() { + void testEcommerceSubtotalF() { request.setEcommerceSubtotal(null); - - assertNull(request.getEcommerceSubtotal()); + assertThat(request.getEcommerceSubtotal()).isNull(); } /** * Test of getEcommerceTax method, of class PiwikRequest. */ @Test - public void testEcommerceTaxT() { + void testEcommerceTaxT() { request.enableEcommerce(); request.setEcommerceId("1"); request.setEcommerceRevenue(2.0); request.setEcommerceTax(20.0); - - assertEquals(Double.valueOf(20.0), request.getEcommerceTax()); - } - - @Test - public void testEcommerceTaxTE() { - try { - request.setEcommerceTax(20.0); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be \"0\". Try calling enableEcommerce first before calling this method.", - e.getLocalizedMessage()); - } + assertThat(request.getEcommerceTax()).isEqualTo(Double.valueOf(20.0)); } @Test - public void testEcommerceTaxF() { + void testEcommerceTaxF() { request.setEcommerceTax(null); - - assertNull(request.getEcommerceTax()); + assertThat(request.getEcommerceTax()).isNull(); } /** * Test of getEventAction method, of class PiwikRequest. */ @Test - public void testEventAction() { + void testEventAction() { request.setEventAction("action"); - assertEquals("action", request.getEventAction()); + assertThat(request.getEventAction()).isEqualTo("action"); request.setEventAction(null); - assertNull(request.getEventAction()); - } - - @Test - public void testEventActionException() { - try { - request.setEventAction(""); - fail("Exception should have been thrown"); - } catch (IllegalArgumentException e) { - assertEquals("Value cannot be empty.", e.getLocalizedMessage()); - } + assertThat(request.getEventAction()).isNull(); } /** * Test of getEventCategory method, of class PiwikRequest. */ @Test - public void testEventCategory() { + void testEventCategory() { request.setEventCategory("category"); - assertEquals("category", request.getEventCategory()); + assertThat(request.getEventCategory()).isEqualTo("category"); } /** * Test of getEventName method, of class PiwikRequest. */ @Test - public void testEventName() { + void testEventName() { request.setEventName("name"); - assertEquals("name", request.getEventName()); + assertThat(request.getEventName()).isEqualTo("name"); } - /** * Test of getEventValue method, of class PiwikRequest. */ @Test - public void testEventValue() { - request.setEventValue(1); - assertEquals(1, request.getEventValue()); + void testEventValue() { + request.setEventValue(1.0); + assertThat(request.getEventValue()).isOne(); } /** * Test of getGoalId method, of class PiwikRequest. */ @Test - public void testGoalId() { + void testGoalId() { request.setGoalId(1); - assertEquals(Integer.valueOf(1), request.getGoalId()); - } - - /** - * Test of getGoalRevenue method, of class PiwikRequest. - */ - @Test - public void testGoalRevenueTT() { - try { - request.setGoalRevenue(20.0); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("GoalId must be set before GoalRevenue can be set.", - e.getLocalizedMessage()); - } - } - - @Test - public void testGoalRevenueTF() { - request.setGoalId(1); - request.setGoalRevenue(20.0); - - assertEquals(Double.valueOf(20.0), request.getGoalRevenue()); - } - - @Test - public void testGoalRevenueF() { - request.setGoalRevenue(null); - - assertNull(request.getGoalRevenue()); + assertThat(request.getGoalId()).isEqualTo(Integer.valueOf(1)); } /** * Test of getHeaderAcceptLanguage method, of class PiwikRequest. */ @Test - public void testHeaderAcceptLanguage() { - request.setHeaderAcceptLanguage("language"); - assertEquals("language", request.getHeaderAcceptLanguage()); + void testHeaderAcceptLanguage() { + request.setHeaderAcceptLanguage(AcceptLanguage.fromHeader("en")); + assertThat(request.getHeaderAcceptLanguage()).hasToString("en"); } /** * Test of getHeaderUserAgent method, of class PiwikRequest. */ @Test - public void testHeaderUserAgent() { + void testHeaderUserAgent() { request.setHeaderUserAgent("agent"); - assertEquals("agent", request.getHeaderUserAgent()); + assertThat(request.getHeaderUserAgent()).isEqualTo("agent"); } /** * Test of getNewVisit method, of class PiwikRequest. */ @Test - public void testNewVisit() { + void testNewVisit() { request.setNewVisit(true); - assertEquals(true, request.getNewVisit()); + assertThat(request.getNewVisit()).isTrue(); request.setNewVisit(null); - assertNull(request.getNewVisit()); + assertThat(request.getNewVisit()).isNull(); } /** * Test of getOutlinkUrl method, of class PiwikRequest. */ @Test - public void testOutlinkUrl() throws Exception { - URL url = new URL("http://outlink.com"); - request.setOutlinkUrl(url); - assertEquals(url, request.getOutlinkUrl()); - assertEquals("http://outlink.com", request.getOutlinkUrlAsString()); - - request.setOutlinkUrlWithString("http://outlinkstring.com"); - assertEquals("http://outlinkstring.com", request.getOutlinkUrlAsString()); - assertEquals(new URL("http://outlinkstring.com"), request.getOutlinkUrl()); - + void testOutlinkUrl() { + request.setOutlinkUrl("http://outlink.com"); + assertThat(request.getOutlinkUrl()).isEqualTo("http://outlink.com"); } /** * Test of getPageCustomVariable method, of class PiwikRequest. */ @Test - public void testPageCustomVariableStringStringE() { - try { - request.setPageCustomVariable(null, null); - fail("Exception should have been thrown"); - } catch (NullPointerException e) { - assertEquals("key is marked non-null but is null", e.getLocalizedMessage()); - } - } - - @Test - public void testPageCustomVariableStringStringE2() { - try { - request.setPageCustomVariable(null, "pageVal"); - fail("Exception should have been thrown"); - } catch (NullPointerException e) { - assertEquals("key is marked non-null but is null", e.getLocalizedMessage()); - } - } - - @Test - public void testPageCustomVariableStringStringE3() { - try { - request.getPageCustomVariable(null); - fail("Exception should have been thrown"); - } catch (NullPointerException e) { - assertEquals("key is marked non-null but is null", e.getLocalizedMessage()); - } + void testPageCustomVariableStringStringE() { + assertThatThrownBy(() -> request.setPageCustomVariable(null, null)); } @Test - public void testPageCustomVariableStringString() { - assertNull(request.getPageCustomVariable("pageKey")); - request.setPageCustomVariable("pageKey", "pageVal"); - assertEquals("pageVal", request.getPageCustomVariable("pageKey")); - request.setPageCustomVariable("pageKey", null); - assertNull(request.getPageCustomVariable("pageKey")); - request.setPageCustomVariable("pageKey", "pageVal"); - assertEquals("pageVal", request.getPageCustomVariable("pageKey")); + void testPageCustomVariableStringStringE2() { + assertThatThrownBy(() -> request.setPageCustomVariable(null, "pageVal")); } @Test - public void testPageCustomVariableCustomVariable() { - assertNull(request.getPageCustomVariable(1)); + void testPageCustomVariableCustomVariable() { + assertThat(request.getPageCustomVariable(1)).isNull(); CustomVariable cv = new CustomVariable("pageKey", "pageVal"); request.setPageCustomVariable(cv, 1); - assertEquals(cv, request.getPageCustomVariable(1)); + assertThat(request.getPageCustomVariable(1)).isEqualTo(cv); request.setPageCustomVariable(null, 1); - assertNull(request.getPageCustomVariable(1)); + assertThat(request.getPageCustomVariable(1)).isNull(); request.setPageCustomVariable(cv, 2); - assertEquals(cv, request.getPageCustomVariable(2)); + assertThat(request.getPageCustomVariable(2)).isEqualTo(cv); } /** * Test of getPluginDirector method, of class PiwikRequest. */ @Test - public void testPluginDirector() { + void testPluginDirector() { request.setPluginDirector(true); - assertEquals(true, request.getPluginDirector()); + assertThat(request.getPluginDirector()).isTrue(); } /** * Test of getPluginFlash method, of class PiwikRequest. */ @Test - public void testPluginFlash() { + void testPluginFlash() { request.setPluginFlash(true); - assertEquals(true, request.getPluginFlash()); + assertThat(request.getPluginFlash()).isTrue(); } /** * Test of getPluginGears method, of class PiwikRequest. */ @Test - public void testPluginGears() { + void testPluginGears() { request.setPluginGears(true); - assertEquals(true, request.getPluginGears()); + assertThat(request.getPluginGears()).isTrue(); } /** * Test of getPluginJava method, of class PiwikRequest. */ @Test - public void testPluginJava() { + void testPluginJava() { request.setPluginJava(true); - assertEquals(true, request.getPluginJava()); + assertThat(request.getPluginJava()).isTrue(); } /** * Test of getPluginPDF method, of class PiwikRequest. */ @Test - public void testPluginPDF() { + void testPluginPDF() { request.setPluginPDF(true); - assertEquals(true, request.getPluginPDF()); + assertThat(request.getPluginPDF()).isTrue(); } /** * Test of getPluginQuicktime method, of class PiwikRequest. */ @Test - public void testPluginQuicktime() { + void testPluginQuicktime() { request.setPluginQuicktime(true); - assertEquals(true, request.getPluginQuicktime()); + assertThat(request.getPluginQuicktime()).isTrue(); } /** * Test of getPluginRealPlayer method, of class PiwikRequest. */ @Test - public void testPluginRealPlayer() { + void testPluginRealPlayer() { request.setPluginRealPlayer(true); - assertEquals(true, request.getPluginRealPlayer()); + assertThat(request.getPluginRealPlayer()).isTrue(); } /** * Test of getPluginSilverlight method, of class PiwikRequest. */ @Test - public void testPluginSilverlight() { + void testPluginSilverlight() { request.setPluginSilverlight(true); - assertEquals(true, request.getPluginSilverlight()); + assertThat(request.getPluginSilverlight()).isTrue(); } /** * Test of getPluginWindowsMedia method, of class PiwikRequest. */ @Test - public void testPluginWindowsMedia() { + void testPluginWindowsMedia() { request.setPluginWindowsMedia(true); - assertEquals(true, request.getPluginWindowsMedia()); + assertThat(request.getPluginWindowsMedia()).isTrue(); } /** * Test of getRandomValue method, of class PiwikRequest. */ @Test - public void testRandomValue() { - request.setRandomValue("value"); - assertEquals("value", request.getRandomValue()); + void testRandomValue() { + request.setRandomValue(RandomValue.fromString("value")); + assertThat(request.getRandomValue()).hasToString("value"); } /** * Test of setReferrerUrl method, of class PiwikRequest. */ @Test - public void testReferrerUrl() throws Exception { - URL url = new URL("http://referrer.com"); - request.setReferrerUrl(url); - assertEquals(url, request.getReferrerUrl()); - assertEquals("http://referrer.com", request.getReferrerUrlAsString()); - - request.setReferrerUrlWithString("http://referrerstring.com"); - assertEquals("http://referrerstring.com", request.getReferrerUrlAsString()); - assertEquals(new URL("http://referrerstring.com"), request.getReferrerUrl()); - + void testReferrerUrl() { + request.setReferrerUrl("http://referrer.com"); + assertThat(request.getReferrerUrl()).isEqualTo("http://referrer.com"); } /** * Test of getRequestDatetime method, of class PiwikRequest. */ @Test - public void testRequestDatetimeTTT() { + void testRequestDatetimeTTT() { request.setAuthToken("12345678901234567890123456789012"); PiwikDate date = new PiwikDate(1000L); request.setRequestDatetime(date); - - assertEquals(date, request.getRequestDatetime()); + assertThat(request.getRequestDatetime().getTime()).isEqualTo(1000L); } - @Test - public void testRequestDatetimeTTF() { - try { - PiwikDate date = new PiwikDate(1000L); - request.setRequestDatetime(date); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("Because you are trying to set RequestDatetime for a time greater than 4 hours ago, AuthToken must be set first.", - e.getLocalizedMessage()); - } - } @Test - public void testRequestDatetimeTF() { - PiwikDate date = new PiwikDate(); - request.setRequestDatetime(date); - assertEquals(date, request.getRequestDatetime()); + void testRequestDatetimeTF() { + request.setRequestDatetime(new PiwikDate()); + assertThat(request.getRequestDatetime().getZonedDateTime()).isCloseTo(ZonedDateTime.now(), within(2, MINUTES)); } @Test - public void testRequestDatetimeF() { + void testRequestDatetimeF() { PiwikDate date = new PiwikDate(); request.setRequestDatetime(date); request.setRequestDatetime(null); - assertNull(request.getRequestDatetime()); + assertThat(request.getRequestDatetime()).isNull(); } /** * Test of getRequired method, of class PiwikRequest. */ @Test - public void testRequired() { + void testRequired() { request.setRequired(false); - assertEquals(false, request.getRequired()); + assertThat(request.getRequired()).isFalse(); } /** * Test of getResponseAsImage method, of class PiwikRequest. */ @Test - public void testResponseAsImage() { + void testResponseAsImage() { request.setResponseAsImage(true); - assertEquals(true, request.getResponseAsImage()); - } - - /** - * Test of getSearchCategory method, of class PiwikRequest. - */ - @Test - public void testSearchCategoryTT() { - try { - request.setSearchCategory("category"); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("SearchQuery must be set before SearchCategory can be set.", - e.getLocalizedMessage()); - } + assertThat(request.getResponseAsImage()).isTrue(); } @Test - public void testSearchCategoryTF() { + void testSearchCategoryTF() { request.setSearchQuery("query"); request.setSearchCategory("category"); - assertEquals("category", request.getSearchCategory()); + assertThat(request.getSearchCategory()).isEqualTo("category"); } @Test - public void testSearchCategoryF() { + void testSearchCategoryF() { request.setSearchCategory(null); - assertNull(request.getSearchCategory()); + assertThat(request.getSearchCategory()).isNull(); } /** * Test of getSearchQuery method, of class PiwikRequest. */ @Test - public void testSearchQuery() { + void testSearchQuery() { request.setSearchQuery("query"); - assertEquals("query", request.getSearchQuery()); - } - - /** - * Test of getSearchResultsCount method, of class PiwikRequest. - */ - @Test - public void testSearchResultsCountTT() { - try { - request.setSearchResultsCount(100L); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("SearchQuery must be set before SearchResultsCount can be set.", - e.getLocalizedMessage()); - } + assertThat(request.getSearchQuery()).isEqualTo("query"); } @Test - public void testSearchResultsCountTF() { + void testSearchResultsCountTF() { request.setSearchQuery("query"); request.setSearchResultsCount(100L); - assertEquals(Long.valueOf(100L), request.getSearchResultsCount()); + assertThat(request.getSearchResultsCount()).isEqualTo(Long.valueOf(100L)); } @Test - public void testSearchResultsCountF() { + void testSearchResultsCountF() { request.setSearchResultsCount(null); - assertNull(request.getSearchResultsCount()); + assertThat(request.getSearchResultsCount()).isNull(); } /** * Test of getSiteId method, of class PiwikRequest. */ @Test - public void testSiteId() { + void testSiteId() { request.setSiteId(2); - assertEquals(Integer.valueOf(2), request.getSiteId()); + assertThat(request.getSiteId()).isEqualTo(Integer.valueOf(2)); } /** * Test of setTrackBotRequest method, of class PiwikRequest. */ @Test - public void testTrackBotRequests() { + void testTrackBotRequests() { request.setTrackBotRequests(true); - assertEquals(true, request.getTrackBotRequests()); + assertThat(request.getTrackBotRequests()).isTrue(); } - /** - * Test of getUserrCustomVariable method, of class PiwikRequest. + * Test of getUserCustomVariable method, of class PiwikRequest. */ @Test - public void testUserCustomVariableStringString() { + void testUserCustomVariableStringString() { request.setUserCustomVariable("userKey", "userValue"); - assertEquals("userValue", request.getUserCustomVariable("userKey")); + assertThat(request.getUserCustomVariable("userKey")).isEqualTo("userValue"); } - @Test - public void testVisitCustomVariableCustomVariable() { - request.setRandomValue("random"); - request.setVisitorId("1234567890123456"); - - assertNull(request.getVisitCustomVariable(1)); - CustomVariable cv = new CustomVariable("visitKey", "visitVal"); - request.setVisitCustomVariable(cv, 1); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&_cvar={\"1\":[\"visitKey\",\"visitVal\"]}", request.getQueryString()); - - request.setUserCustomVariable("key", "val"); - assertEquals(cv, request.getVisitCustomVariable(1)); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&_cvar={\"1\":[\"visitKey\",\"visitVal\"],\"2\":[\"key\",\"val\"]}", request.getQueryString()); - - request.setVisitCustomVariable(null, 1); - assertNull(request.getVisitCustomVariable(1)); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&_cvar={\"2\":[\"key\",\"val\"]}", request.getQueryString()); - - request.setVisitCustomVariable(cv, 2); - assertEquals(cv, request.getVisitCustomVariable(2)); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&_cvar={\"2\":[\"visitKey\",\"visitVal\"]}", request.getQueryString()); - - request.setUserCustomVariable("visitKey", null); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456", request.getQueryString()); - } /** * Test of getUserId method, of class PiwikRequest. */ @Test - public void testUserId() { + void testUserId() { request.setUserId("id"); - assertEquals("id", request.getUserId()); + assertThat(request.getUserId()).isEqualTo("id"); } /** * Test of getVisitorCity method, of class PiwikRequest. */ @Test - public void testVisitorCityT() { + void testVisitorCityT() { request.setAuthToken("12345678901234567890123456789012"); request.setVisitorCity("city"); - assertEquals("city", request.getVisitorCity()); - } - - @Test - public void testVisitorCityTE() { - try { - request.setVisitorCity("city"); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("AuthToken must be set before this value can be set.", - e.getLocalizedMessage()); - } + assertThat(request.getVisitorCity()).isEqualTo("city"); } @Test - public void testVisitorCityF() { + void testVisitorCityF() { request.setVisitorCity(null); - assertNull(request.getVisitorCity()); + assertThat(request.getVisitorCity()).isNull(); } /** * Test of getVisitorCountry method, of class PiwikRequest. */ @Test - public void testVisitorCountryT() { + void testVisitorCountryT() { PiwikLocale country = new PiwikLocale(Locale.US); request.setAuthToken("12345678901234567890123456789012"); request.setVisitorCountry(country); - - assertEquals(country, request.getVisitorCountry()); + assertThat(request.getVisitorCountry()).isEqualTo(country); } @Test - public void testVisitorCountryTE() { - try { - PiwikLocale country = new PiwikLocale(Locale.US); - request.setVisitorCountry(country); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("AuthToken must be set before this value can be set.", - e.getLocalizedMessage()); - } - } - - @Test - public void testVisitorCountryF() { + void testVisitorCountryF() { request.setVisitorCountry(null); - - assertNull(request.getVisitorCountry()); - } - - /** - * Test of getVisitorCustomId method, of class PiwikRequest. - */ - @Test - public void testVisitorCustomTT() { - try { - request.setVisitorCustomId("1"); - fail("Exception should have been thrown."); - } catch (IllegalArgumentException e) { - assertEquals("1 is not 16 characters long.", - e.getLocalizedMessage()); - } - } - - @Test - public void testVisitorCustomTFT() { - try { - request.setVisitorCustomId("1234567890abcdeg"); - fail("Exception should have been thrown."); - } catch (IllegalArgumentException e) { - assertEquals("1234567890abcdeg is not a hexadecimal string.", - e.getLocalizedMessage()); - } + assertThat(request.getVisitorCountry()).isNull(); } @Test - public void testVisitorCustomIdTFF() { - request.setVisitorCustomId("1234567890abcdef"); - assertEquals("1234567890abcdef", request.getVisitorCustomId()); + void testVisitorCustomTF() { + request.setVisitorCustomId(VisitorId.fromHex("1234567890abcdef")); + assertThat(request.getVisitorCustomId()).hasToString("1234567890abcdef"); } @Test - public void testVisitorCustomIdF() { - request.setVisitorCustomId("1234567890abcdef"); + void testVisitorCustomIdF() { + request.setVisitorCustomId(VisitorId.fromHex("1234567890abcdef")); request.setVisitorCustomId(null); - assertNull(request.getVisitorCustomId()); + assertThat(request.getVisitorCustomId()).isNull(); } /** * Test of getVisitorFirstVisitTimestamp method, of class PiwikRequest. */ @Test - public void testVisitorFirstVisitTimestamp() { - request.setVisitorFirstVisitTimestamp(1000L); - assertEquals(Long.valueOf(1000L), request.getVisitorFirstVisitTimestamp()); - } - - /** - * Test of getVisitorId method, of class PiwikRequest. - */ - @Test - public void testVisitorIdTT() { - try { - request.setVisitorId("1"); - fail("Exception should have been thrown."); - } catch (IllegalArgumentException e) { - assertEquals("1 is not 16 characters long.", - e.getLocalizedMessage()); - } + void testVisitorFirstVisitTimestamp() { + request.setVisitorFirstVisitTimestamp(Instant.parse("2021-03-10T10:22:22.123Z")); + assertThat(request.getVisitorFirstVisitTimestamp()).isEqualTo("2021-03-10T10:22:22.123Z"); } @Test - public void testVisitorIdTFT() { + void testVisitorIdTFT() { try { - request.setVisitorId("1234567890abcdeg"); + request.setVisitorId(VisitorId.fromHex("1234567890abcdeg")); fail("Exception should have been thrown."); } catch (IllegalArgumentException e) { - assertEquals("1234567890abcdeg is not a hexadecimal string.", - e.getLocalizedMessage()); + assertThat(e.getLocalizedMessage()).isEqualTo("Input must be a valid hex string"); } } @Test - public void testVisitorIdTFF() { - request.setVisitorId("1234567890abcdef"); - assertEquals("1234567890abcdef", request.getVisitorId()); + void testVisitorIdTFF() { + request.setVisitorId(VisitorId.fromHex("1234567890abcdef")); + assertThat(request.getVisitorId()).hasToString("1234567890abcdef"); } @Test - public void testVisitorIdF() { - request.setVisitorId("1234567890abcdef"); + void testVisitorIdF() { + request.setVisitorId(VisitorId.fromHex("1234567890abcdef")); request.setVisitorId(null); - assertNull(request.getVisitorId()); + assertThat(request.getVisitorId()).isNull(); } /** * Test of getVisitorIp method, of class PiwikRequest. */ @Test - public void testVisitorIpT() { + void testVisitorIpT() { request.setAuthToken("12345678901234567890123456789012"); request.setVisitorIp("ip"); - assertEquals("ip", request.getVisitorIp()); - } - - @Test - public void testVisitorIpTE() { - try { - request.setVisitorIp("ip"); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("AuthToken must be set before this value can be set.", - e.getLocalizedMessage()); - } + assertThat(request.getVisitorIp()).isEqualTo("ip"); } @Test - public void testVisitorIpF() { + void testVisitorIpF() { request.setVisitorIp(null); - assertNull(request.getVisitorIp()); + assertThat(request.getVisitorIp()).isNull(); } /** * Test of getVisitorLatitude method, of class PiwikRequest. */ @Test - public void testVisitorLatitudeT() { + void testVisitorLatitudeT() { request.setAuthToken("12345678901234567890123456789012"); request.setVisitorLatitude(10.5); - assertEquals(Double.valueOf(10.5), request.getVisitorLatitude()); - } - - @Test - public void testVisitorLatitudeTE() { - try { - request.setVisitorLatitude(10.5); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("AuthToken must be set before this value can be set.", - e.getLocalizedMessage()); - } + assertThat(request.getVisitorLatitude()).isEqualTo(Double.valueOf(10.5)); } @Test - public void testVisitorLatitudeF() { + void testVisitorLatitudeF() { request.setVisitorLatitude(null); - assertNull(request.getVisitorLatitude()); + assertThat(request.getVisitorLatitude()).isNull(); } /** * Test of getVisitorLongitude method, of class PiwikRequest. */ @Test - public void testVisitorLongitudeT() { + void testVisitorLongitudeT() { request.setAuthToken("12345678901234567890123456789012"); request.setVisitorLongitude(20.5); - assertEquals(Double.valueOf(20.5), request.getVisitorLongitude()); - } - - @Test - public void testVisitorLongitudeTE() { - try { - request.setVisitorLongitude(20.5); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("AuthToken must be set before this value can be set.", - e.getLocalizedMessage()); - } + assertThat(request.getVisitorLongitude()).isEqualTo(Double.valueOf(20.5)); } @Test - public void testVisitorLongitudeF() { + void testVisitorLongitudeF() { request.setVisitorLongitude(null); - assertNull(request.getVisitorLongitude()); + assertThat(request.getVisitorLongitude()).isNull(); } /** * Test of getVisitorPreviousVisitTimestamp method, of class PiwikRequest. */ @Test - public void testVisitorPreviousVisitTimestamp() { - request.setVisitorPreviousVisitTimestamp(1000L); - assertEquals(Long.valueOf(1000L), request.getVisitorPreviousVisitTimestamp()); + void testVisitorPreviousVisitTimestamp() { + request.setVisitorPreviousVisitTimestamp(Instant.ofEpochSecond(1000L)); + assertThat(request.getVisitorPreviousVisitTimestamp()).isEqualTo("1970-01-01T00:16:40Z"); } /** * Test of getVisitorRegion method, of class PiwikRequest. */ @Test - public void testVisitorRegionT() { + void testVisitorRegionT() { request.setAuthToken("12345678901234567890123456789012"); request.setVisitorRegion("region"); - - assertEquals("region", request.getVisitorRegion()); - } - - @Test - public void testGetVisitorRegionTE() { - try { - request.setVisitorRegion("region"); - fail("Exception should have been thrown."); - } catch (IllegalStateException e) { - assertEquals("AuthToken must be set before this value can be set.", - e.getLocalizedMessage()); - } + assertThat(request.getVisitorRegion()).isEqualTo("region"); } @Test - public void testVisitorRegionF() { + void testVisitorRegionF() { request.setVisitorRegion(null); - - assertNull(request.getVisitorRegion()); + assertThat(request.getVisitorRegion()).isNull(); } /** * Test of getVisitorVisitCount method, of class PiwikRequest. */ @Test - public void testVisitorVisitCount() { + void testVisitorVisitCount() { request.setVisitorVisitCount(100); - assertEquals(Integer.valueOf(100), request.getVisitorVisitCount()); - } - - /** - * Test of getQueryString method, of class PiwikRequest. - */ - @Test - public void testGetQueryString() { - request.setRandomValue("random"); - request.setVisitorId("1234567890123456"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456", request.getQueryString()); - request.setPageCustomVariable("key", "val"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&cvar={\"1\":[\"key\",\"val\"]}", - request.getQueryString()); - request.setPageCustomVariable("key", null); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456", request.getQueryString()); - request.addCustomTrackingParameter("key", "test"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&key=test", request.getQueryString()); - request.addCustomTrackingParameter("key", "test2"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&key=test&key=test2", request.getQueryString()); - request.setCustomTrackingParameter("key2", "test3"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&key=test&key=test2&key2=test3", request.getQueryString()); - request.setCustomTrackingParameter("key", "test4"); - assertEquals("idsite=3&rec=1&url=http://test.com&apiv=1&send_image=0&rand=random&_id=1234567890123456&key2=test3&key=test4", request.getQueryString()); - request.setRandomValue(null); - request.setSiteId(null); - request.setRequired(null); - request.setApiVersion(null); - request.setResponseAsImage(null); - request.setVisitorId(null); - request.setActionUrl((String) null); - assertEquals("key2=test3&key=test4", request.getQueryString()); - request.clearCustomTrackingParameter(); - assertEquals("", request.getQueryString()); - } - - @Test - public void testGetQueryString2() { - request.setActionUrlWithString("http://test.com"); - request.setRandomValue("random"); - request.setVisitorId("1234567890123456"); - assertEquals("idsite=3&rec=1&apiv=1&send_image=0&url=http://test.com&rand=random&_id=1234567890123456", request.getQueryString()); - } - - /** - * Test of getUrlEncodedQueryString method, of class PiwikRequest. - */ - @Test - public void testGetUrlEncodedQueryString() { - request.setRandomValue("random"); - request.setVisitorId("1234567890123456"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.addCustomTrackingParameter("ke/y", "te:st"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&ke%2Fy=te%3Ast&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.addCustomTrackingParameter("ke/y", "te:st2"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&ke%2Fy=te%3Ast&ke%2Fy=te%3Ast2&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.setCustomTrackingParameter("ke/y2", "te:st3"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&ke%2Fy=te%3Ast&ke%2Fy=te%3Ast2&ke%2Fy2=te%3Ast3&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.setCustomTrackingParameter("ke/y", "te:st4"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&ke%2Fy=te%3Ast4&ke%2Fy2=te%3Ast3&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); - request.setRandomValue(null); - request.setSiteId(null); - request.setRequired(null); - request.setApiVersion(null); - request.setResponseAsImage(null); - request.setVisitorId(null); - request.setActionUrl((String) null); - assertEquals("ke%2Fy=te%3Ast4&ke%2Fy2=te%3Ast3", request.getUrlEncodedQueryString()); - request.clearCustomTrackingParameter(); - assertEquals("", request.getUrlEncodedQueryString()); + assertThat(request.getVisitorVisitCount()).isEqualTo(Integer.valueOf(100)); } @Test - public void testGetUrlEncodedQueryString2() { - request.setActionUrlWithString("http://test.com"); - request.setRandomValue("random"); - request.setVisitorId("1234567890123456"); - assertEquals("_id=1234567890123456&apiv=1&idsite=3&rand=random&rec=1&send_image=0&url=http%3A%2F%2Ftest.com", request.getUrlEncodedQueryString()); + void failsIfActionUrlIsNull() { + assertThatThrownBy(() -> new PiwikRequest(3, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("Action URL must not be null"); } - /** - * Test of getRandomHexString method, of class PiwikRequest. - */ - @Test - public void testGetRandomHexString() { - String s = PiwikRequest.getRandomHexString(10); - - assertEquals(10, s.length()); - Long.parseLong(s, 16); - } } diff --git a/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java b/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java new file mode 100644 index 00000000..17365eb2 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/PiwikTrackerIT.java @@ -0,0 +1,253 @@ +package org.matomo.java.tracking; + +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.status; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.ConnectException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.VisitorId; +import org.piwik.java.tracking.PiwikRequest; +import org.piwik.java.tracking.PiwikTracker; + +class PiwikTrackerIT { + + private static final WireMockServer wireMockServer = new WireMockServer( + WireMockConfiguration.options().dynamicPort()); + + + private static final int SITE_ID = 42; + + private PiwikTracker piwikTracker; + + private PiwikRequest request; + + @BeforeAll + static void beforeAll() { + wireMockServer.start(); + } + + @BeforeEach + void setUp() throws MalformedURLException { + piwikTracker = new PiwikTracker( + String.format("http://localhost:%d/matomo.php", wireMockServer.port()), -1); + wireMockServer.resetRequests(); + wireMockServer.stubFor(post(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + wireMockServer.stubFor(get(urlPathEqualTo("/matomo.php")).willReturn(status(204))); + request = new PiwikRequest(SITE_ID, new URL("https://test.local/test/path?id=123")); + request.setRandomValue(RandomValue.fromString("rand")); + request.setVisitorId(VisitorId.fromHash(999999999999999999L)); + } + + /** + * Test of sendRequest method, of class PiwikTracker. + */ + @Test + void testSendRequest() { + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendRequest(request); + + wireMockServer.verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue")) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))) + ; + } + + /** + * Test of sendRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendRequestAsync() throws Exception { + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = piwikTracker.sendRequestAsync(request); + future.get(); + + assertThat(future).isNotCompletedExceptionally(); + wireMockServer.verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue")) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))); + } + + + /** + * Test of sendBulkRequest method, of class PiwikTracker. + */ + @Test + void testSendBulkRequest_Iterable() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests); + + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{ \"requests\" : [ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ]}"))); + + } + + /** + * Test of sendBulkRequest method, of class PiwikTracker. + */ + @Test + void testSendBulkRequest_Iterable_StringTT() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + assertThatThrownBy(() -> piwikTracker.sendBulkRequest(requests, "1")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + @Test + void testSendBulkRequest_Iterable_StringFF() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests, null); + + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[\"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\"]}"))); + + } + + @Test + void testSendBulkRequest_Iterable_StringFT() { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + piwikTracker.sendBulkRequest(requests, "12345678901234567890123456789012"); + + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("215")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[\"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ],\"token_auth\":\"12345678901234567890123456789012\"}"))); + + } + + /** + * Test of sendBulkRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendBulkRequestAsync_Iterable() throws Exception { + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = piwikTracker.sendBulkRequestAsync(requests); + future.get(); + + assertThat(future).isNotCompletedExceptionally(); + + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("167")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\" : [ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\" ]}"))); + + } + + /** + * Test of sendBulkRequestAsync method, of class PiwikTracker. + */ + @Test + void testSendBulkRequestAsync_Iterable_StringTT() { + + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + assertThatThrownBy(() -> piwikTracker.sendBulkRequestAsync(requests, "1").get()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + + } + + + @Test + void testSendBulkRequestAsync_Iterable_String() throws Exception { + + List requests = Collections.singletonList(request); + request.setCustomTrackingParameter("parameterName", "parameterValue"); + + CompletableFuture future = piwikTracker.sendBulkRequestAsync( + requests, "12345678901234567890123456789012"); + future.get(); + + assertThat(future).isNotCompletedExceptionally(); + + wireMockServer.verify(postRequestedFor(urlEqualTo("/matomo.php")) + .withHeader("Content-Length", equalTo("215")) + .withHeader("Accept", equalTo("*/*")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo("MatomoJavaClient")) + .withRequestBody(equalToJson( + "{\"requests\":[ \"?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand¶meterName=parameterValue\"],\"token_auth\":\"12345678901234567890123456789012\"}"))); + + } + + @Test + void createsPiwikTrackerWithHostUrl() { + PiwikTracker piwikTracker = new PiwikTracker(String.format("http://localhost:%d/matomo.php", wireMockServer.port())); + + piwikTracker.sendRequest(request); + + wireMockServer.verify(getRequestedFor(urlEqualTo( + "/matomo.php?rec=1&idsite=42&url=https%3A%2F%2Ftest.local%2Ftest%2Fpath%3Fid%3D123&apiv=1&_id=0de0b6b3a763ffff&send_image=0&rand=rand")) + .withHeader("User-Agent", equalTo("MatomoJavaClient"))); + } + + @Test + void createPiwikTrackerWithHostUrlAndProxyHostAndPort() { + PiwikTracker piwikTracker = new PiwikTracker(String.format("http://localhost:%d/matomo.php", wireMockServer.port()), "localhost", 8080); + + assertThatThrownBy(() -> piwikTracker.sendRequest(request)) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not send request via GET") + .hasRootCauseInstanceOf(ConnectException.class) + .hasRootCauseMessage("Connection refused (Connection refused)"); + + } + + @Test + void createPiwikTrackerWithHostUrlAndProxyHostAndPortAndTimeout() { + PiwikTracker piwikTracker = new PiwikTracker(String.format("http://localhost:%d/matomo.php", wireMockServer.port()), "localhost", 8080, 1000); + + assertThatThrownBy(() -> piwikTracker.sendRequest(request)) + .isInstanceOf(MatomoException.class) + .hasMessage("Could not send request via GET") + .hasRootCauseInstanceOf(ConnectException.class) + .hasRootCauseMessage("Connection refused (Connection refused)");} + +} diff --git a/src/test/java/org/matomo/java/tracking/PiwikTrackerTest.java b/src/test/java/org/matomo/java/tracking/PiwikTrackerTest.java deleted file mode 100644 index 6c1dcc12..00000000 --- a/src/test/java/org/matomo/java/tracking/PiwikTrackerTest.java +++ /dev/null @@ -1,433 +0,0 @@ -/* - * Piwik Java Tracker - * - * @link https://github.com/matomo/matomo-java-tracker - * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause - */ -package org.matomo.java.tracking; - -import com.sun.net.httpserver.HttpExchange; -import com.sun.net.httpserver.HttpHandler; -import com.sun.net.httpserver.HttpServer; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.concurrent.FutureCallback; -import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; -import org.apache.http.util.EntityUtils; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.mockito.ArgumentMatcher; -import org.piwik.java.tracking.PiwikRequest; -import org.piwik.java.tracking.PiwikTracker; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetSocketAddress; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; - -/** - * @author brettcsorba - */ -public class PiwikTrackerTest { - private static final Map> PARAMETERS = Collections.singletonMap("parameterName", Collections.singleton("parameterValue")); - - // https://stackoverflow.com/a/3732328 - static class Handler implements HttpHandler { - @Override - public void handle(HttpExchange exchange) throws IOException { - String response = "OK"; - exchange.sendResponseHeaders(200, response.length()); - OutputStream os = exchange.getResponseBody(); - os.write(response.getBytes()); - os.close(); - } - } - - PiwikTracker piwikTracker; - PiwikTracker localTracker; - HttpServer server; - - public PiwikTrackerTest() { - } - - @BeforeClass - public static void setUpClass() { - } - - @AfterClass - public static void tearDownClass() { - } - - @Before - public void setUp() { - // test with mocks - piwikTracker = spy(new PiwikTracker("http://test.com")); - - // test with local server - localTracker = new PiwikTracker("http://localhost:8001/test"); - try { - server = HttpServer.create(new InetSocketAddress(8001), 0); - server.createContext("/test", new Handler()); - server.setExecutor(null); // creates a default executor - server.start(); - } catch (IOException ex) { - } - } - - @After - public void tearDown() { - server.stop(0); - } - - /** - * Test of addParameter method, of class PiwikTracker. - */ - @Test - public void testAddParameter() { - } - - /** - * Test of sendRequest method, of class PiwikTracker. - */ - @Test - public void testSendRequest() throws Exception { - PiwikRequest request = mock(PiwikRequest.class); - HttpClient client = mock(HttpClient.class); - HttpResponse response = mock(HttpResponse.class); - - doReturn(client).when(piwikTracker).getHttpClient(); - doReturn(PARAMETERS).when(request).getParameters(); - doReturn(response).when(client) - .execute(argThat(new CorrectGetRequest("http://test.com?parameterName=parameterValue"))); - - assertEquals(response, piwikTracker.sendRequest(request)); - } - - /** - * Test of sendRequestAsync method, of class PiwikTracker. - */ - @Test - public void testSendRequestAsync() throws Exception { - PiwikRequest request = mock(PiwikRequest.class); - CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class); - HttpResponse response = mock(HttpResponse.class); - Future future = mock(Future.class); - - doReturn(client).when(piwikTracker).getHttpAsyncClient(); - doReturn(PARAMETERS).when(request).getParameters(); - doReturn(response).when(future).get(); - doReturn(future).when(client) - .execute(argThat(new CorrectGetRequest("http://test.com?parameterName=parameterValue")), any()); - - assertEquals(response, piwikTracker.sendRequestAsync(request).get()); - } - - /** - * Test sync API with local server - */ - @Test - public void testWithLocalServer() throws Exception { - // one - PiwikRequest request = new PiwikRequest(3, new URL("http://test.com")); - HttpResponse response = localTracker.sendRequest(request); - String msg = EntityUtils.toString(response.getEntity()); - assertEquals("OK", msg); - - // bulk - List requests = Collections.singletonList(request); - HttpResponse responseBulk = localTracker.sendBulkRequest(requests); - String msgBulk = EntityUtils.toString(responseBulk.getEntity()); - assertEquals("OK", msgBulk); - } - - /** - * Test async API with local server - */ - @Test - public void testWithLocalServerAsync() throws Exception { - // one - PiwikRequest request = new PiwikRequest(3, new URL("http://test.com")); - HttpResponse response = localTracker.sendRequestAsync(request).get(); - String msg = EntityUtils.toString(response.getEntity()); - assertEquals("OK", msg); - - // bulk - List requests = Collections.singletonList(request); - HttpResponse responseBulk = localTracker.sendBulkRequestAsync(requests).get(); - String msgBulk = EntityUtils.toString(responseBulk.getEntity()); - assertEquals("OK", msgBulk); - } - - /** - * Test async API with local server - */ - @Test - public void testWithLocalServerAsyncCallback() throws Exception { - CountDownLatch latch = new CountDownLatch(2); - BlockingQueue responses = new LinkedBlockingQueue<>(); - BlockingQueue exceptions = new LinkedBlockingQueue<>(); - AtomicInteger cancelled = new AtomicInteger(); - - FutureCallback cb = new FutureCallback() { - - @Override - public void completed(HttpResponse httpResponse) { - responses.add(httpResponse); - latch.countDown(); - } - - @Override - public void failed(Exception e) { - exceptions.add(e); - latch.countDown(); - } - - @Override - public void cancelled() { - cancelled.incrementAndGet(); - latch.countDown(); - - } - }; - - // one - PiwikRequest request = new PiwikRequest(3, new URL("http://test.com")); - Future respFuture = localTracker.sendRequestAsync(request, cb); - // bulk - List requests = Collections.singletonList(request); - Future bulkFuture = localTracker.sendBulkRequestAsync(requests, cb); - - assertTrue("Responses not received", latch.await(100, TimeUnit.MILLISECONDS)); - assertEquals("Not expecting cancelled responses", 0, cancelled.get()); - assertEquals("Not expecting exceptions", exceptions.size(), 0); - assertTrue("Single response future not done", respFuture.isDone()); - assertTrue("Bulk response future not done", bulkFuture.isDone()); - HttpResponse response = responses.poll(1, TimeUnit.MILLISECONDS); - assertEquals("OK", EntityUtils.toString(response.getEntity())); - - HttpResponse bulkResponse = responses.poll(1, TimeUnit.MILLISECONDS); - assertEquals("OK", EntityUtils.toString(bulkResponse.getEntity())); - } - - static class CorrectGetRequest implements ArgumentMatcher { - String url; - - public CorrectGetRequest(String url) { - this.url = url; - } - - @Override - public boolean matches(HttpGet get) { - return url.equals(get.getURI().toString()); - } - } - - /** - * Test of sendBulkRequest method, of class PiwikTracker. - */ - @Test - public void testSendBulkRequest_Iterable() { - List requests = new ArrayList<>(); - HttpResponse response = mock(HttpResponse.class); - - doReturn(response).when(piwikTracker).sendBulkRequest(requests, null); - - assertEquals(response, piwikTracker.sendBulkRequest(requests)); - } - - /** - * Test of sendBulkRequest method, of class PiwikTracker. - */ - @Test - public void testSendBulkRequest_Iterable_StringTT() { - try { - List requests = new ArrayList<>(); - HttpClient client = mock(HttpClient.class); - PiwikRequest request = mock(PiwikRequest.class); - - requests.add(request); - - piwikTracker.sendBulkRequest(requests, "1"); - fail("Exception should have been thrown."); - } catch (IllegalArgumentException e) { - assertEquals("1 is not 32 characters long.", e.getLocalizedMessage()); - } - } - - @Test - public void testSendBulkRequest_Iterable_StringFF() throws Exception { - List requests = new ArrayList<>(); - HttpClient client = mock(HttpClient.class); - PiwikRequest request = mock(PiwikRequest.class); - HttpResponse response = mock(HttpResponse.class); - - doReturn(PARAMETERS).when(request).getParameters(); - requests.add(request); - doReturn(client).when(piwikTracker).getHttpClient(); - doReturn(response).when(client).execute(argThat(new CorrectPostRequest("{\"requests\":[\"?parameterName=parameterValue\"]}"))); - - assertEquals(response, piwikTracker.sendBulkRequest(requests, null)); - } - - @Test - public void testSendBulkRequest_Iterable_StringFT() throws Exception { - List requests = new ArrayList<>(); - HttpClient client = mock(HttpClient.class); - PiwikRequest request = mock(PiwikRequest.class); - HttpResponse response = mock(HttpResponse.class); - - doReturn(PARAMETERS).when(request).getParameters(); - requests.add(request); - doReturn(client).when(piwikTracker).getHttpClient(); - doReturn(response).when(client) - .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?parameterName=parameterValue\"],\"token_auth\":\"12345678901234567890123456789012\"}"))); - - assertEquals(response, piwikTracker.sendBulkRequest(requests, "12345678901234567890123456789012")); - } - - /** - * Test of sendBulkRequestAsync method, of class PiwikTracker. - */ - @Test - public void testSendBulkRequestAsync_Iterable() throws Exception { - List requests = new ArrayList<>(); - HttpResponse response = mock(HttpResponse.class); - Future future = mock(Future.class); - doReturn(response).when(future).get(); - - doReturn(future).when(piwikTracker).sendBulkRequestAsync(requests); - - assertEquals(response, piwikTracker.sendBulkRequestAsync(requests).get()); - } - - /** - * Test of sendBulkRequestAsync method, of class PiwikTracker. - */ - @Test - public void testSendBulkRequestAsync_Iterable_StringTT() { - try { - List requests = new ArrayList<>(); - CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class); - PiwikRequest request = mock(PiwikRequest.class); - - requests.add(request); - - piwikTracker.sendBulkRequestAsync(requests, "1"); - fail("Exception should have been thrown."); - } catch (IllegalArgumentException e) { - assertEquals("1 is not 32 characters long.", e.getLocalizedMessage()); - } - } - - @Test - public void testSendBulkRequestAsync_Iterable_StringFF() throws Exception { - List requests = new ArrayList<>(); - CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class); - PiwikRequest request = mock(PiwikRequest.class); - HttpResponse response = mock(HttpResponse.class); - Future future = mock(Future.class); - doReturn(response).when(future).get(); - - doReturn(PARAMETERS).when(request).getParameters(); - requests.add(request); - doReturn(client).when(piwikTracker).getHttpAsyncClient(); - doReturn(future).when(client) - .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?parameterName=parameterValue\"]}")), any()); - - assertEquals(response, piwikTracker.sendBulkRequestAsync(requests).get()); - } - - @Test - public void testSendBulkRequestAsync_Iterable_StringFT() throws Exception { - List requests = new ArrayList<>(); - CloseableHttpAsyncClient client = mock(CloseableHttpAsyncClient.class); - PiwikRequest request = mock(PiwikRequest.class); - HttpResponse response = mock(HttpResponse.class); - Future future = mock(Future.class); - doReturn(response).when(future).get(); - - doReturn(PARAMETERS).when(request).getParameters(); - requests.add(request); - doReturn(client).when(piwikTracker).getHttpAsyncClient(); - doReturn(future).when(client) - .execute(argThat(new CorrectPostRequest("{\"requests\":[\"?parameterName=parameterValue\"],\"token_auth\":\"12345678901234567890123456789012\"}")), any()); - - assertEquals(response, piwikTracker.sendBulkRequestAsync(requests, "12345678901234567890123456789012").get()); - } - - static class CorrectPostRequest implements ArgumentMatcher { - String body; - - public CorrectPostRequest(String body) { - this.body = body; - } - - @Override - public boolean matches(HttpPost post) { - try { - InputStream bais = post.getEntity().getContent(); - byte[] bytes = new byte[bais.available()]; - bais.read(bytes); - String str = new String(bytes); - return body.equals(str); - } catch (IOException e) { - fail("Exception should not have been throw."); - } - return false; - } - } - - /** - * Test of getHttpClient method, of class PiwikTracker. - */ - @Test - public void testGetHttpClient() { - assertNotNull(piwikTracker.getHttpClient()); - } - - /** - * Test of getHttpAsyncClient method, of class PiwikTracker. - */ - @Test - public void testGetHttpAsyncClient() { - assertNotNull(piwikTracker.getHttpAsyncClient()); - } - - /** - * Test of getHttpClient method, of class PiwikTracker, with proxy. - */ - @Test - public void testGetHttpClientWithProxy() { - piwikTracker = new PiwikTracker("http://test.com", "http://proxy", 8080); - HttpClient httpClient = piwikTracker.getHttpClient(); - - assertNotNull(httpClient); - } -} diff --git a/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java b/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java new file mode 100644 index 00000000..f4199853 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java @@ -0,0 +1,52 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.Authenticator; +import java.net.Authenticator.RequestorType; +import java.net.InetAddress; +import java.net.PasswordAuthentication; +import java.net.URL; +import org.junit.jupiter.api.Test; + +class ProxyAuthenticatorTest { + + private PasswordAuthentication passwordAuthentication; + + @Test + void createsPasswordAuthentication() throws Exception { + + ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator("user", "password"); + Authenticator.setDefault(proxyAuthenticator); + givenPasswordAuthentication(RequestorType.PROXY); + + assertThat(passwordAuthentication.getUserName()).isEqualTo("user"); + assertThat(passwordAuthentication.getPassword()).contains('p', 'a', 's', 's', 'w', 'o', 'r', 'd'); + + } + @Test + void returnsNullIfNoPasswordAuthentication() throws Exception { + + ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator("user", "password"); + Authenticator.setDefault(proxyAuthenticator); + givenPasswordAuthentication(RequestorType.SERVER); + + assertThat(passwordAuthentication).isNull(); + + } + + private void givenPasswordAuthentication(RequestorType proxy) throws Exception { + passwordAuthentication = Authenticator.requestPasswordAuthentication( + "host", + InetAddress.getLocalHost(), + 8080, + "http", + "prompt", + "https", + new URL("https://www.daniel-heid.de"), + proxy + ); + } + + +} diff --git a/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java b/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java new file mode 100644 index 00000000..0909c016 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/QueryCreatorTest.java @@ -0,0 +1,363 @@ +package org.matomo.java.tracking; + +import static java.util.Arrays.asList; +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 java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale.LanguageRange; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.parameters.AcceptLanguage; +import org.matomo.java.tracking.parameters.Country; +import org.matomo.java.tracking.parameters.CustomVariable; +import org.matomo.java.tracking.parameters.CustomVariables; +import org.matomo.java.tracking.parameters.DeviceResolution; +import org.matomo.java.tracking.parameters.EcommerceItem; +import org.matomo.java.tracking.parameters.EcommerceItems; +import org.matomo.java.tracking.parameters.RandomValue; +import org.matomo.java.tracking.parameters.UniqueId; +import org.matomo.java.tracking.parameters.VisitorId; + +class QueryCreatorTest { + + private final MatomoRequest.MatomoRequestBuilder matomoRequestBuilder = + MatomoRequest.builder().visitorId(VisitorId.fromHash(1234567890123456789L)).randomValue( + RandomValue.fromString("random-value")); + + private String defaultAuthToken = "876de1876fb2cda2816c362a61bfc712"; + + private String query; + + private MatomoRequest request; + + @Test + void usesDefaultSiteId() { + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value"); + + } + + private void whenCreatesQuery() { + request = matomoRequestBuilder.build(); + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create("http://localhost")).defaultSiteId(42).defaultAuthToken(defaultAuthToken).build(); + String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); + query = new QueryCreator(trackerConfiguration).createQuery(request, authToken); + } + + @Test + void overridesDefaultSiteId() { + + matomoRequestBuilder.siteId(123); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&idsite=123&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value"); + + } + + @Test + void usesDefaultTokenAuth() { + + defaultAuthToken = "f123bfc9a46de0bb5453afdab6f93200"; + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=f123bfc9a46de0bb5453afdab6f93200&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value"); + + } + + @Test + void overridesDefaultTokenAuth() { + + defaultAuthToken = "f123bfc9a46de0bb5453afdab6f93200"; + matomoRequestBuilder.authToken("e456bfc9a46de0bb5453afdab6f93200"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=e456bfc9a46de0bb5453afdab6f93200&rec=1&apiv=1&_id=112210f47de98115&token_auth=e456bfc9a46de0bb5453afdab6f93200&send_image=0&rand=random-value"); + + } + + @Test + void validatesTokenAuth() { + + matomoRequestBuilder.authToken("invalid-token-auth"); + + assertThatThrownBy(this::whenCreatesQuery).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + + } + + @Test + void convertsTrueBooleanTo1() { + + matomoRequestBuilder.pluginFlash(true); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&fla=1&send_image=0&rand=random-value"); + + } + + @Test + void convertsFalseBooleanTo0() { + + matomoRequestBuilder.pluginJava(false); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&java=0&send_image=0&rand=random-value"); + + } + + @Test + void encodesUrl() { + + matomoRequestBuilder.actionUrl("https://www.daniel-heid.de/some/page?foo=bar"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&url=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fpage%3Ffoo%3Dbar&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value"); + + } + + @Test + void encodesReferrerUrl() { + + matomoRequestBuilder.referrerUrl("https://www.daniel-heid.de/some/referrer?foo2=bar2"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Freferrer%3Ffoo2%3Dbar2&send_image=0&rand=random-value"); + + } + + @Test + void encodesLink() { + + matomoRequestBuilder.outlinkUrl("https://www.daniel-heid.de/some/external/link#"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&link=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fexternal%2Flink%23&send_image=0&rand=random-value"); + + } + + @Test + void encodesDownloadUrl() { + + matomoRequestBuilder.downloadUrl("https://www.daniel-heid.de/some/download.pdf"); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&download=https%3A%2F%2Fwww.daniel-heid.de%2Fsome%2Fdownload.pdf&send_image=0&rand=random-value"); + + } + + @Test + void tracksMinimalRequest() { + + matomoRequestBuilder.actionName("Help / Feedback").actionUrl( + "https://www.daniel-heid.de/portfolio").visitorId(VisitorId.fromHash(3434343434343434343L)) + .referrerUrl("https://www.daniel-heid.de/referrer").visitCustomVariables( + new CustomVariables().add(new CustomVariable("customVariable1Key", "customVariable1Value"), 5) + .add(new CustomVariable("customVariable2Key", "customVariable2Value"), 6)) + .visitorVisitCount(2).visitorPreviousVisitTimestamp(Instant.parse("2022-08-09T18:34:12Z")) + .deviceResolution(DeviceResolution.builder().width(1024).height(768).build()) + .headerAcceptLanguage(AcceptLanguage.builder().languageRange(new LanguageRange("de")) + .languageRange(new LanguageRange("de-DE", 0.9)).languageRange(new LanguageRange("en", 0.8)) + .build()).pageViewId(UniqueId.fromValue(999999999999999999L)).goalId(0).ecommerceRevenue( + 12.34).ecommerceItems(EcommerceItems.builder().item( + EcommerceItem.builder().sku("SKU").build()).item(EcommerceItem.builder().sku("SKU").name( + "NAME").category("CATEGORY").price(123.4).build()).build()).authToken( + "fdf6e8461ea9de33176b222519627f78").visitorCountry( + Country.fromLanguageRanges("en-GB;q=0.7,de,de-DE;q=0.9,en;q=0.8,en-US;q=0.6")); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=fdf6e8461ea9de33176b222519627f78&rec=1&action_name=Help+%2F+Feedback&url=https%3A%2F%2Fwww.daniel-heid.de%2Fportfolio&apiv=1&_id=2fa93d2858bc4867&urlref=https%3A%2F%2Fwww.daniel-heid.de%2Freferrer&_cvar=%7B%225%22%3A%5B%22customVariable1Key%22%2C%22customVariable1Value%22%5D%2C%226%22%3A%5B%22customVariable2Key%22%2C%22customVariable2Value%22%5D%7D&_idvc=2&_viewts=1660070052&res=1024x768&lang=de%2Cde-de%3Bq%3D0.9%2Cen%3Bq%3D0.8&pv_id=lbBbxG&idgoal=0&revenue=12.34&ec_items=%5B%5B%22SKU%22%2C%22%22%2C%22%22%2C0.0%2C0%5D%2C%5B%22SKU%22%2C%22NAME%22%2C%22CATEGORY%22%2C123.4%2C0%5D%5D&token_auth=fdf6e8461ea9de33176b222519627f78&country=de&send_image=0&rand=random-value"); + + } + + @Test + void testGetQueryString() { + matomoRequestBuilder.siteId(3).actionUrl("http://test.com").randomValue( + RandomValue.fromString("random")).visitorId(VisitorId.fromHex("1234567890123456")); + defaultAuthToken = null; + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random"); + matomoRequestBuilder.pageCustomVariables( + new CustomVariables().add(new CustomVariable("key", "val"), 7)); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random"); + matomoRequestBuilder.customTrackingParameters(singletonMap("key", singleton("test"))); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key=test"); + matomoRequestBuilder.customTrackingParameters(singletonMap("key", asList("test", "test2"))); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key=test&key=test2"); + Map> customTrackingParameters = new HashMap<>(); + customTrackingParameters.put("key", asList("test", "test2")); + customTrackingParameters.put("key2", Collections.singletonList("test3")); + matomoRequestBuilder.customTrackingParameters(customTrackingParameters); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key2=test3&key=test&key=test2"); + customTrackingParameters.put("key", Collections.singletonList("test4")); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&send_image=0&rand=random&key2=test3&key=test4"); + matomoRequestBuilder.randomValue(null); + matomoRequestBuilder.siteId(null); + matomoRequestBuilder.required(null); + matomoRequestBuilder.apiVersion(null); + matomoRequestBuilder.responseAsImage(null); + matomoRequestBuilder.visitorId(null); + matomoRequestBuilder.actionUrl(null); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "idsite=42&cvar=%7B%227%22%3A%5B%22key%22%2C%22val%22%5D%7D&key2=test3&key=test4"); + } + + @Test + void testGetQueryString2() { + matomoRequestBuilder.actionUrl("http://test.com").randomValue(RandomValue.fromString("random")) + .visitorId(VisitorId.fromHex("1234567890123456")).siteId(3); + defaultAuthToken = null; + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random"); + } + + @Test + void testGetUrlEncodedQueryString() { + defaultAuthToken = null; + matomoRequestBuilder.actionUrl("http://test.com").randomValue(RandomValue.fromString("random")) + .visitorId(VisitorId.fromHex("1234567890123456")).siteId(3); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random"); + Map> customTrackingParameters = new HashMap<>(); + customTrackingParameters.put("ke/y", Collections.singletonList("te:st")); + matomoRequestBuilder.customTrackingParameters(customTrackingParameters); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast"); + customTrackingParameters.put("ke/y", asList("te:st", "te:st2")); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast&ke%2Fy=te%3Ast2"); + customTrackingParameters.put("ke/y2", Collections.singletonList("te:st3")); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast&ke%2Fy=te%3Ast2&ke%2Fy2=te%3Ast3"); + customTrackingParameters.put("ke/y", asList("te:st3", "te:st4")); + whenCreatesQuery(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random&ke%2Fy=te%3Ast3&ke%2Fy=te%3Ast4&ke%2Fy2=te%3Ast3"); + matomoRequestBuilder.randomValue(null).siteId(null).required(null).apiVersion(null) + .responseAsImage(null).visitorId(null).actionUrl(null); + whenCreatesQuery(); + assertThat(query).isEqualTo("idsite=42&ke%2Fy=te%3Ast3&ke%2Fy=te%3Ast4&ke%2Fy2=te%3Ast3"); + + } + + @Test + void testGetUrlEncodedQueryString2() { + matomoRequestBuilder.actionUrl("http://test.com").randomValue(RandomValue.fromString("random")) + .visitorId(VisitorId.fromHex("1234567890123456")); + defaultAuthToken = null; + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42&rec=1&url=http%3A%2F%2Ftest.com&apiv=1&_id=1234567890123456&send_image=0&rand=random"); + + } + + @Test + void testVisitCustomVariableCustomVariable() { + matomoRequestBuilder.randomValue(RandomValue.fromString("random")).visitorId( + VisitorId.fromHex("1234567890123456")).siteId(3); + org.matomo.java.tracking.CustomVariable cv = new org.matomo.java.tracking.CustomVariable( + "visitKey", "visitVal"); + matomoRequestBuilder.visitCustomVariables(new CustomVariables().add(cv, 8)); + defaultAuthToken = null; + + whenCreatesQuery(); + + assertThat(request.getVisitCustomVariable(1)).isNull(); + assertThat(query).isEqualTo( + "rec=1&idsite=3&apiv=1&_id=1234567890123456&_cvar=%7B%228%22%3A%5B%22visitKey%22%2C%22visitVal%22%5D%7D&send_image=0&rand=random"); + } + + @Test + void doesNotAppendEmptyString() { + + matomoRequestBuilder.eventAction(""); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&e_a=&send_image=0&rand=random-value"); + + } + + @Test + void testAuthTokenTT() { + + matomoRequestBuilder.authToken("1234"); + + assertThatThrownBy(this::whenCreatesQuery).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + @Test + void createsQueryWithDimensions() { + matomoRequestBuilder.dimensions(asList("firstDimension", "secondDimension")); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&send_image=0&rand=random-value&dimension1=firstDimension&dimension2=secondDimension"); + } + + @Test + void appendsCharsetParameters() { + matomoRequestBuilder.characterSet(StandardCharsets.ISO_8859_1); + + whenCreatesQuery(); + + assertThat(query).isEqualTo( + "idsite=42token_auth=876de1876fb2cda2816c362a61bfc712&rec=1&apiv=1&_id=112210f47de98115&cs=ISO-8859-1&send_image=0&rand=random-value"); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java b/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java new file mode 100644 index 00000000..2cf0653e --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java @@ -0,0 +1,162 @@ +package org.matomo.java.tracking; + +import org.junit.jupiter.api.Test; +import org.piwik.java.tracking.PiwikDate; +import org.piwik.java.tracking.PiwikLocale; + +import java.time.Instant; +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RequestValidatorTest { + + private final MatomoRequest request = new MatomoRequest(); + + @Test + void testEcommerceRevenue() { + + request.setEcommerceRevenue(20.0); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Goal ID must be set if ecommerce parameters are used"); + + } + + @Test + void testEcommerceDiscount() { + request.setEcommerceDiscount(1.0); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Goal ID must be set if ecommerce parameters are used"); + + } + + @Test + void testEcommerceId() { + request.setEcommerceId("1"); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Goal ID must be set if ecommerce parameters are used"); + } + + @Test + void testEcommerceSubtotal() { + request.setEcommerceSubtotal(20.0); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Goal ID must be set if ecommerce parameters are used"); + } + + @Test + void testEcommerceShippingCost() { + request.setEcommerceShippingCost(20.0); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Goal ID must be set if ecommerce parameters are used"); + } + + @Test + void testEcommerceLastOrderTimestamp() { + request.setEcommerceLastOrderTimestamp(Instant.ofEpochSecond(1000L)); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Goal ID must be set if ecommerce parameters are used"); + } + + @Test + void testEcommerceTax() { + request.setEcommerceTax(20.0); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Goal ID must be set if ecommerce parameters are used"); + } + + @Test + void testEcommerceItemE() { + + request.addEcommerceItem(new EcommerceItem("sku", "name", "category", 1.0, 2)); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Goal ID must be set if ecommerce parameters are used"); + } + + @Test + void testSearchResultsCount() { + + request.setSearchResultsCount(100L); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Search query must be set if search results count is set"); + + } + + @Test + void testVisitorLongitude() { + request.setVisitorLongitude(20.5); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Auth token must be present if longitude, latitude, region, city or country are set"); + } + + @Test + void testVisitorLatitude() { + request.setVisitorLatitude(10.5); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Auth token must be present if longitude, latitude, region, city or country are set"); + } + + @Test + void testVisitorCity() { + request.setVisitorCity("city"); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Auth token must be present if longitude, latitude, region, city or country are set"); + } + + @Test + void testVisitorRegion() { + request.setVisitorRegion("region"); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Auth token must be present if longitude, latitude, region, city or country are set"); + } + + @Test + void testVisitorCountryTE() { + PiwikLocale country = new PiwikLocale(Locale.US); + request.setVisitorCountry(country); + + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Auth token must be present if longitude, latitude, region, city or country are set"); + } + + @Test + void testRequestDatetime() { + + PiwikDate date = new PiwikDate(1000L); + request.setRequestDatetime(date); + + assertThatThrownBy(() -> RequestValidator.validate(request, null)) + .isInstanceOf(MatomoException.class) + .hasMessage("Auth token must be present if request timestamp is more than four hours ago"); + + } + +} diff --git a/src/test/java/org/matomo/java/tracking/SenderIT.java b/src/test/java/org/matomo/java/tracking/SenderIT.java new file mode 100644 index 00000000..32763878 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/SenderIT.java @@ -0,0 +1,96 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import java.net.MalformedURLException; +import java.net.URI; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class SenderIT { + + private static final WireMockServer wireMockServer = new WireMockServer( + WireMockConfiguration.options().dynamicPort()); + + @BeforeAll + static void beforeAll() { + wireMockServer.start(); + } + + @Test + void sendSingleFailsIfQueryIsMalformed() { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create("telnet://localhost")).build(); + Sender sender = new Sender( + trackerConfiguration, new QueryCreator(trackerConfiguration), Runnable::run); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())).isInstanceOf( + InvalidUrlException.class).hasRootCause( + new MalformedURLException("unknown protocol: telnet")); + } + + @Test + void failsIfEndpointReturnsNotFound() { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create(wireMockServer.baseUrl())).build(); + + Sender sender = new Sender( + trackerConfiguration, new QueryCreator(trackerConfiguration), Runnable::run); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())).isInstanceOf( + MatomoException.class).hasMessage("Tracking endpoint responded with code 404"); + } + + @Test + void failsIfCouldNotConnectToEndpoint() { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create("http://localhost:1234")).build(); + + Sender sender = new Sender( + trackerConfiguration, new QueryCreator(trackerConfiguration), Runnable::run); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())).isInstanceOf( + MatomoException.class).hasMessage("Could not send request via GET"); + } + + @Test + void connectsViaProxy() throws Exception { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create(wireMockServer.baseUrl())).proxyHost("localhost").proxyPort(wireMockServer.port()) + .build(); + + Sender sender = new Sender( + trackerConfiguration, new QueryCreator(trackerConfiguration), Runnable::run); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())).isInstanceOf( + MatomoException.class).hasMessage("Tracking endpoint responded with code 400"); + } + + @Test + void connectsViaProxyWithProxyUserNameAndPassword() throws Exception { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create(wireMockServer.baseUrl())).proxyHost("localhost").proxyPort(wireMockServer.port()) + .proxyUserName("user").proxyPassword("password").build(); + + Sender sender = new Sender( + trackerConfiguration, new QueryCreator(trackerConfiguration), Runnable::run); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())).isInstanceOf( + MatomoException.class).hasMessage("Tracking endpoint responded with code 400"); + } + + @Test + void logsFailedTracking() throws Exception { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create(wireMockServer.baseUrl())).logFailedTracking(true).build(); + + Sender sender = new Sender( + trackerConfiguration, new QueryCreator(trackerConfiguration), Runnable::run); + + assertThatThrownBy(() -> sender.sendSingle(new MatomoRequest())).isInstanceOf( + MatomoException.class).hasMessage("Tracking endpoint responded with code 404"); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/TrackerConfigurationTest.java b/src/test/java/org/matomo/java/tracking/TrackerConfigurationTest.java new file mode 100644 index 00000000..be5ebc90 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/TrackerConfigurationTest.java @@ -0,0 +1,61 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import org.junit.jupiter.api.Test; + +class TrackerConfigurationTest { + + @Test + void validateDoesNotFailIfDefaultAuthTokenIsNull() throws Exception { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create("https://matomo.example/matomo.php")).defaultSiteId(1).defaultAuthToken(null) + .build(); + trackerConfiguration.validate(); + } + + @Test + void validateFailsIfDefaultAuthTokenIsEmpty() throws Exception { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create("https://matomo.example/matomo.php")).defaultSiteId(1).defaultAuthToken("") + .build(); + + assertThatThrownBy(trackerConfiguration::validate).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + @Test + void validateFailsIfDefaultAuthTokenIsTooLong() throws Exception { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create("https://matomo.example/matomo.php")).defaultSiteId(1).defaultAuthToken( + "123456789012345678901234567890123") + .build(); + + assertThatThrownBy(trackerConfiguration::validate).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + @Test + void validateFailsIfDefaultAuthTokenIsTooShort() throws Exception { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create("https://matomo.example/matomo.php")).defaultSiteId(1).defaultAuthToken( + "1234567890123456789012345678901") + .build(); + + assertThatThrownBy(trackerConfiguration::validate).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must be exactly 32 characters long"); + } + + @Test + void validateFailsIfDefaultAuthTokenContainsInvalidCharacters() throws Exception { + TrackerConfiguration trackerConfiguration = TrackerConfiguration.builder().apiEndpoint( + URI.create("https://matomo.example/matomo.php")).defaultSiteId(1).defaultAuthToken( + "1234567890123456789012345678901!") + .build(); + + assertThatThrownBy(trackerConfiguration::validate).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Auth token must contain only lowercase letters and numbers"); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/TrackingParameterMethodTest.java b/src/test/java/org/matomo/java/tracking/TrackingParameterMethodTest.java new file mode 100644 index 00000000..a637e20f --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/TrackingParameterMethodTest.java @@ -0,0 +1,50 @@ +package org.matomo.java.tracking; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +class TrackingParameterMethodTest { + + @Test + void validateParameterValueFailsIfPatternDoesNotMatch() { + TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod.builder() + .parameterName("foo") + .pattern(Pattern.compile("bar")) + .build(); + + assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue("baz")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid value for foo. Must match regex bar"); + } + + @Test + void doNothingIfPatternIsNull() { + TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod.builder() + .parameterName("foo") + .build(); + + trackingParameterMethod.validateParameterValue("baz"); + } + + @Test + void doNothingIfParameterValueIsNotCharSequence() { + TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod.builder() + .parameterName("foo") + .pattern(Pattern.compile("bar")) + .build(); + + trackingParameterMethod.validateParameterValue(1); + } + + @Test + void doNothingIfParameterValueIsNull() { + TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod.builder() + .parameterName("foo") + .pattern(Pattern.compile("bar")) + .build(); + + trackingParameterMethod.validateParameterValue(null);} + +} diff --git a/src/test/java/org/matomo/java/tracking/parameters/AcceptLanguageTest.java b/src/test/java/org/matomo/java/tracking/parameters/AcceptLanguageTest.java new file mode 100644 index 00000000..6f289697 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/parameters/AcceptLanguageTest.java @@ -0,0 +1,46 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class AcceptLanguageTest { + + @Test + void fromHeader() { + + AcceptLanguage acceptLanguage = AcceptLanguage.fromHeader( + "de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"); + + assertThat(acceptLanguage).hasToString( + "de,de-de;q=0.9,de-dd;q=0.9,en;q=0.8,en-gb;q=0.7,en-us;q=0.6"); + + } + + @ParameterizedTest + @NullAndEmptySource + void fromHeaderToleratesNull(String header) { + + AcceptLanguage acceptLanguage = AcceptLanguage.fromHeader(header); + + assertThat(acceptLanguage).isNull(); + + } + + @Test + void failsOnNullLanguageRange() { + assertThat(AcceptLanguage.builder().languageRanges(singletonList(null)).build()).hasToString( + ""); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/parameters/CountryTest.java b/src/test/java/org/matomo/java/tracking/parameters/CountryTest.java new file mode 100644 index 00000000..f0ae93ee --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/parameters/CountryTest.java @@ -0,0 +1,165 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Locale; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class CountryTest { + + @Test + void createsCountryFromCode() { + + Country country = Country.fromCode("DE"); + + assertThat(country).hasToString("de"); + + } + + @Test + void createsCountryFromAcceptLanguageHeader() { + + Country country = Country.fromLanguageRanges("en-GB;q=0.7,de,de-DE;q=0.9,en;q=0.8,en-US;q=0.6"); + + assertThat(country).hasToString("de"); + + } + + @ParameterizedTest + @NullAndEmptySource + void returnsNullOnEmptyRanges(String ranges) { + + Country country = Country.fromLanguageRanges(ranges); + + assertThat(country).isNull(); + + } + + @Test + void failsOnInvalidCountryCode() { + + assertThatThrownBy(() -> Country.fromCode("invalid")).isInstanceOf( + IllegalArgumentException.class).hasMessage( + "Invalid country code"); + + } + + @Test + void failsOnInvalidCountryCodeLength() { + + assertThatThrownBy(() -> Country.fromCode("invalid")).isInstanceOf( + IllegalArgumentException.class).hasMessage( + "Invalid country code"); + + } + + @Test + void returnsNullOnNullCode() { + + Country country = Country.fromCode(null); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnEmptyCode() { + + Country country = Country.fromCode(""); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnBlankCode() { + + Country country = Country.fromCode(" "); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnNullRanges() { + + Country country = Country.fromLanguageRanges(null); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnEmptyRanges() { + + Country country = Country.fromLanguageRanges(""); + + assertThat(country).isNull(); + + } + + @Test + void returnsNullOnBlankRanges() { + + Country country = Country.fromLanguageRanges(" "); + + assertThat(country).isNull(); + + } + + @Test + void failsOnInvalidRanges() { + + assertThatThrownBy(() -> Country.fromLanguageRanges("invalid")).isInstanceOf( + IllegalArgumentException.class).hasMessage( + "Invalid country code"); + + } + + @Test + void failsOnLocaleWithoutCountryCode() { + + assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de"))).isInstanceOf( + IllegalArgumentException.class).hasMessage( + "Invalid locale"); + + } + + @Test + void setLocaleFailsOnNullLocale() { + + assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(null)).isInstanceOf( + IllegalArgumentException.class).hasMessage( + "Invalid locale"); + + } + + @Test + void setLocaleFailsOnNullCountryCode() { + + assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(Locale.forLanguageTag("de"))).isInstanceOf( + IllegalArgumentException.class).hasMessage( + "Invalid locale"); + + } + + @Test + void setLocaleFailsOnEmptyCountryCode() { + + assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(Locale.forLanguageTag("de"))).isInstanceOf( + IllegalArgumentException.class).hasMessage( + "Invalid locale"); + + } + +} diff --git a/src/test/java/org/matomo/java/tracking/parameters/CustomVariableTest.java b/src/test/java/org/matomo/java/tracking/parameters/CustomVariableTest.java new file mode 100644 index 00000000..f072d09e --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/parameters/CustomVariableTest.java @@ -0,0 +1,69 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CustomVariableTest { + + private CustomVariable customVariable; + + @BeforeEach + void setUp() { + customVariable = new CustomVariable("key", "value"); + } + + @Test + void testConstructorNullKey() { + try { + new CustomVariable(null, null); + fail("Exception should have been throw."); + } catch (NullPointerException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); + } + } + + @Test + void testConstructorNullValue() { + try { + new CustomVariable("key", null); + fail("Exception should have been throw."); + } catch (NullPointerException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("value is marked non-null but is null"); + } + } + + @Test + void testGetKey() { + assertThat(customVariable.getKey()).isEqualTo("key"); + } + + @Test + void testGetValue() { + assertThat(customVariable.getValue()).isEqualTo("value"); + } + + @Test + void equalsCustomVariable() { + CustomVariable variableA = new CustomVariable("a", "b"); + CustomVariable variableB = new CustomVariable("a", "b"); + assertThat(variableA).isEqualTo(variableB); + assertThat(variableA.hashCode()).isEqualTo(variableB.hashCode()); + CustomVariable c = new CustomVariable("a", "c"); + assertThat(variableA).isNotEqualTo(c); + assertThat(variableA.hashCode()).isNotEqualTo(c.hashCode()); + CustomVariable d = new CustomVariable("d", "b"); + assertThat(variableA).isNotEqualTo(d); + assertThat(variableA.hashCode()).isNotEqualTo(d.hashCode()); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/parameters/CustomVariablesTest.java b/src/test/java/org/matomo/java/tracking/parameters/CustomVariablesTest.java new file mode 100644 index 00000000..cfb5087c --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/parameters/CustomVariablesTest.java @@ -0,0 +1,170 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; + +import org.junit.jupiter.api.Test; + +class CustomVariablesTest { + + private final CustomVariables customVariables = new CustomVariables(); + + @Test + void testAdd_CustomVariable() { + CustomVariable a = new CustomVariable("a", "b"); + assertThat(customVariables.isEmpty()).isTrue(); + customVariables.add(a); + assertThat(customVariables.isEmpty()).isFalse(); + assertThat(customVariables.get("a")).isEqualTo("b"); + assertThat(customVariables.get(1)).isEqualTo(a); + assertThat(customVariables).hasToString("{\"1\":[\"a\",\"b\"]}"); + CustomVariable b = new CustomVariable("c", "d"); + customVariables.add(b); + assertThat(customVariables.get("c")).isEqualTo("d"); + assertThat(customVariables.get(2)).isEqualTo(b); + assertThat(customVariables).hasToString("{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"]}"); + CustomVariable c = new CustomVariable("a", "e"); + customVariables.add(c, 5); + assertThat(customVariables.get("a")).isEqualTo("b"); + assertThat(customVariables.get(5)).isEqualTo(c); + assertThat(customVariables.get(3)).isNull(); + assertThat(customVariables).hasToString( + "{\"1\":[\"a\",\"b\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"e\"]}"); + CustomVariable d = new CustomVariable("a", "f"); + customVariables.add(d); + assertThat(customVariables.get("a")).isEqualTo("f"); + assertThat(customVariables.get(1)).isEqualTo(d); + assertThat(customVariables.get(5)).isEqualTo(d); + assertThat(customVariables).hasToString( + "{\"1\":[\"a\",\"f\"],\"2\":[\"c\",\"d\"],\"5\":[\"a\",\"f\"]}"); + customVariables.remove("a"); + assertThat(customVariables.get("a")).isNull(); + assertThat(customVariables.get(1)).isNull(); + assertThat(customVariables.get(5)).isNull(); + assertThat(customVariables).hasToString("{\"2\":[\"c\",\"d\"]}"); + customVariables.remove(2); + assertThat(customVariables.get("c")).isNull(); + assertThat(customVariables.get(2)).isNull(); + assertThat(customVariables.isEmpty()).isTrue(); + assertThat(customVariables).hasToString("{}"); + } + + @Test + void testAddCustomVariableIndexLessThan1() { + try { + customVariables.add(new CustomVariable("a", "b"), 0); + fail("Exception should have been throw."); + } catch (IllegalArgumentException e) { + assertThat(e.getLocalizedMessage()).isEqualTo("Index must be greater than 0"); + } + } + + @Test + void equalCustomVariables() { + CustomVariables customVariables = new CustomVariables(); + customVariables.add(new CustomVariable("a", "b")); + customVariables.add(new CustomVariable("c", "d")); + customVariables.add(new CustomVariable("a", "e")); + customVariables.add(new CustomVariable("a", "f")); + assertThat(customVariables).isEqualTo(customVariables); + assertThat(customVariables).hasSameHashCodeAs(customVariables); + } + + @Test + void notEqualCustomVariables() { + CustomVariables customVariablesA = new CustomVariables(); + customVariablesA.add(new CustomVariable("a", "b")); + customVariablesA.add(new CustomVariable("c", "d")); + customVariablesA.add(new CustomVariable("a", "e")); + customVariablesA.add(new CustomVariable("a", "f")); + CustomVariables customVariablesB = new CustomVariables(); + customVariablesB.add(new CustomVariable("a", "b")); + customVariablesB.add(new CustomVariable("c", "d")); + customVariablesB.add(new CustomVariable("a", "e")); + assertThat(customVariablesA).isNotEqualTo(customVariablesB); + assertThat(customVariablesA).doesNotHaveSameHashCodeAs(customVariablesB); + } + + @Test + void testAddCustomVariableNull() { + assertThatThrownBy(() -> customVariables.add(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("variable" + + " is marked non-null but is null") + .hasNoCause(); + } + + @Test + void testAddCustomVariableKeyEmpty() { + assertThatThrownBy(() -> customVariables.add(new CustomVariable("", "b"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Custom variable key must not be null or empty") + .hasNoCause(); + } + + @Test + void testAddCustomVariableValueEmpty() { + assertThatThrownBy(() -> customVariables.add(new CustomVariable("a", ""))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Custom variable value must not be null or empty") + .hasNoCause(); + } + + @Test + void testAddCustomVariableNullIndex() { + assertThatThrownBy(() -> customVariables.add(new CustomVariable("a", "b"), 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Index must be greater than 0") + .hasNoCause(); + } + + + @Test + void testAddNullCustomVariableIndex() { + assertThatThrownBy(() -> customVariables.add(null, 1)) + .isInstanceOf(NullPointerException.class) + .hasMessage("cv is marked non-null but is null") + .hasNoCause(); + } + + @Test + void testGetCustomVariableIntegerNull() { + assertThatThrownBy(() -> customVariables.get(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Index must be greater than 0") + .hasNoCause(); + } + + @Test + void testGetCustomVariableKeyNull() { + assertThatThrownBy(() -> customVariables.get(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("key is marked non-null but is null") + .hasNoCause(); + } + + @Test + void testGetCustomVariableKeyEmpty() { + assertThatThrownBy(() -> customVariables.get("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("key must not be null or empty") + .hasNoCause(); + } + + @Test + void testRemoveCustomVariableKeyNull() { + assertThatThrownBy(() -> customVariables.remove(null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("key is marked non-null but is null") + .hasNoCause(); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/parameters/DeviceResolutionTest.java b/src/test/java/org/matomo/java/tracking/parameters/DeviceResolutionTest.java new file mode 100644 index 00000000..c87bb778 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/parameters/DeviceResolutionTest.java @@ -0,0 +1,42 @@ +/* + * Matomo Java Tracker + * + * @link https://github.com/matomo/matomo-java-tracker + * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause + */ + +package org.matomo.java.tracking.parameters; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class DeviceResolutionTest { + + @Test + void formatsDeviceResolution() { + + DeviceResolution deviceResolution = DeviceResolution.builder().width(1280).height(1080).build(); + + assertThat(deviceResolution).hasToString("1280x1080"); + + } + + @Test + void returnsNullOnNull() { + + DeviceResolution deviceResolution = DeviceResolution.fromString(null); + + assertThat(deviceResolution).isNull(); + + } + + @Test + void failsOnWrongDimensionSize() { + assertThatThrownBy(() -> DeviceResolution.fromString("1920x1080x720")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Wrong dimension size"); + } + +} diff --git a/src/test/java/org/matomo/java/tracking/parameters/EcommerceItemsTest.java b/src/test/java/org/matomo/java/tracking/parameters/EcommerceItemsTest.java new file mode 100644 index 00000000..76f67d25 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/parameters/EcommerceItemsTest.java @@ -0,0 +1,17 @@ +package org.matomo.java.tracking.parameters; + +import org.junit.jupiter.api.Test; +import org.matomo.java.tracking.EcommerceItem; + +import static org.assertj.core.api.Assertions.assertThat; + + +class EcommerceItemsTest { + + @Test + void formatsJson() { + EcommerceItems ecommerceItems = new EcommerceItems(); + ecommerceItems.add(new EcommerceItem("sku", "name", "category", 1.0, 1)); + assertThat(ecommerceItems).hasToString("[[\"sku\",\"name\",\"category\",1.0,1]]"); + } +} diff --git a/src/test/java/org/matomo/java/tracking/parameters/UniqueIdTest.java b/src/test/java/org/matomo/java/tracking/parameters/UniqueIdTest.java new file mode 100644 index 00000000..0bd59140 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/parameters/UniqueIdTest.java @@ -0,0 +1,28 @@ +package org.matomo.java.tracking.parameters; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class UniqueIdTest { + + @Test + void createsRandomUniqueId() { + + UniqueId uniqueId = UniqueId.random(); + + assertThat(uniqueId.toString()).matches("[0-9a-zA-Z]{6}"); + + } + + @Test + void createsSameUniqueIds() { + + UniqueId uniqueId1 = UniqueId.fromValue(868686868L); + UniqueId uniqueId2 = UniqueId.fromValue(868686868); + + assertThat(uniqueId1).hasToString(uniqueId2.toString()); + + } + +} diff --git a/src/test/java/org/matomo/java/tracking/parameters/VisitorIdTest.java b/src/test/java/org/matomo/java/tracking/parameters/VisitorIdTest.java new file mode 100644 index 00000000..11dd23e5 --- /dev/null +++ b/src/test/java/org/matomo/java/tracking/parameters/VisitorIdTest.java @@ -0,0 +1,164 @@ +package org.matomo.java.tracking.parameters; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class VisitorIdTest { + + private static Stream validHexStrings() { + return Stream.of( + Arguments.of("0", "0000000000000000"), + Arguments.of("0000", "0000000000000000"), + Arguments.of("1", "0000000000000001"), + Arguments.of("a", "000000000000000a"), + Arguments.of("1a", "000000000000001a"), + Arguments.of("01a", "000000000000001a"), + Arguments.of("1a2b", "0000000000001a2b"), + Arguments.of("1a2b3c", "00000000001a2b3c"), + Arguments.of("1a2b3c4d", "000000001a2b3c4d"), + Arguments.of("1a2b3c4d5e", "0000001a2b3c4d5e"), + Arguments.of("1A2B3C4D5E", "0000001a2b3c4d5e"), + Arguments.of("1a2b3c4d5e6f", "00001a2b3c4d5e6f"), + Arguments.of("1a2b3c4d5e6f7a", "001a2b3c4d5e6f7a") + ); + } + + @Test + void hasCorrectFormat() { + + VisitorId visitorId = VisitorId.random(); + + assertThat(visitorId.toString()).matches("^[a-z0-9]{16}$"); + + } + + @Test + void createsRandomVisitorId() { + + VisitorId first = VisitorId.random(); + VisitorId second = VisitorId.random(); + + assertThat(first).doesNotHaveToString(second.toString()); + + } + + @Test + void fixedVisitorIdForLongHash() { + + VisitorId visitorId = VisitorId.fromHash(987654321098765432L); + + assertThat(visitorId).hasToString("0db4da5f49f8b478"); + + } + + @Test + void fixedVisitorIdForIntHash() { + + VisitorId visitorId = VisitorId.fromHash(777777777); + + assertThat(visitorId).hasToString("000000002e5bf271"); + + } + + @Test + void sameVisitorIdForSameHash() { + + VisitorId first = VisitorId.fromHash(1234567890L); + VisitorId second = VisitorId.fromHash(1234567890); + + assertThat(first).hasToString(second.toString()); + + } + + @Test + void alwaysTheSameToString() { + + VisitorId visitorId = VisitorId.random(); + + assertThat(visitorId).hasToString(visitorId.toString()); + + } + + @Test + void createsVisitorIdFrom16CharacterHex() { + + VisitorId visitorId = VisitorId.fromHex("1234567890abcdef"); + + assertThat(visitorId).hasToString("1234567890abcdef"); + + } + + @Test + void createsVisitorIdFrom1CharacterHex() { + + VisitorId visitorId = VisitorId.fromHex("a"); + + assertThat(visitorId).hasToString("000000000000000a"); + + } + + @Test + void createsVisitorIdFrom2CharacterHex() { + + VisitorId visitorId = VisitorId.fromHex("12"); + + assertThat(visitorId).hasToString("0000000000000012"); + + } + + @Test + void failsOnInvalidHexString() { + + assertThatThrownBy(() -> VisitorId.fromHex("invalid123456789")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Input must be a valid hex string") + ; + + } + + @ParameterizedTest + @ValueSource(strings = {"g", "gh", "ghi", "ghij", "ghijk", "ghijkl", "ghijklm", "ghijklmn", "ghijklmn", "-1"}) + void failsOnInvalidHexString(String hex) { + assertThatThrownBy(() -> VisitorId.fromHex(hex)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Input must be a valid hex string") + ; + } + + @ParameterizedTest + @MethodSource("validHexStrings") + void createsVisitorIdFromHex(String hex, String expected) { + + VisitorId visitorId = VisitorId.fromHex(hex); + + assertThat(visitorId).hasToString(expected); + + } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void failsOnEmptyStrings(String hex) { + assertThatThrownBy(() -> VisitorId.fromHex(hex)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Hex string must not be null or empty") + ; + } + + @ParameterizedTest + @ValueSource(strings = {"1234567890abcdefg", "1234567890abcdeff"}) + void failsOnInvalidHexStringLength(String hex) { + assertThatThrownBy(() -> VisitorId.fromHex(hex)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Hex string must not be longer than 16 characters") + ; + } + +} diff --git a/src/test/java/org/piwik/java/tracking/CustomVariableTest.java b/src/test/java/org/piwik/java/tracking/CustomVariableTest.java new file mode 100644 index 00000000..4492d0a2 --- /dev/null +++ b/src/test/java/org/piwik/java/tracking/CustomVariableTest.java @@ -0,0 +1,17 @@ +package org.piwik.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class CustomVariableTest { + + @Test + void createsCustomVariable() { + CustomVariable customVariable = new CustomVariable("key", "value"); + + assertThat(customVariable.getKey()).isEqualTo("key"); + assertThat(customVariable.getValue()).isEqualTo("value"); + } + +} diff --git a/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java b/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java new file mode 100644 index 00000000..78a311f5 --- /dev/null +++ b/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java @@ -0,0 +1,19 @@ +package org.piwik.java.tracking; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class EcommerceItemTest { + + @Test + void createsEcItem() { + EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 1); + + assertThat(item.getSku()).isEqualTo("sku"); + assertThat(item.getName()).isEqualTo("name"); + assertThat(item.getCategory()).isEqualTo("category"); + assertThat(item.getPrice()).isEqualTo(1.0); + assertThat(item.getQuantity()).isEqualTo(1); + } +}