├── core ├── src │ ├── main │ │ └── java │ │ │ ├── lombok.config │ │ │ └── org │ │ │ ├── piwik │ │ │ └── java │ │ │ │ └── tracking │ │ │ │ ├── package-info.java │ │ │ │ ├── PiwikLocale.java │ │ │ │ ├── CustomVariable.java │ │ │ │ ├── PiwikRequest.java │ │ │ │ ├── EcommerceItem.java │ │ │ │ ├── PiwikDate.java │ │ │ │ └── PiwikTracker.java │ │ │ └── matomo │ │ │ └── java │ │ │ └── tracking │ │ │ ├── SenderProvider.java │ │ │ ├── SenderFactory.java │ │ │ ├── servlet │ │ │ ├── CookieWrapper.java │ │ │ └── HttpServletRequestWrapper.java │ │ │ ├── parameters │ │ │ ├── package-info.java │ │ │ ├── Hex.java │ │ │ ├── EcommerceItem.java │ │ │ ├── EcommerceItems.java │ │ │ ├── RandomValue.java │ │ │ ├── UniqueId.java │ │ │ ├── DeviceResolution.java │ │ │ ├── CustomVariable.java │ │ │ ├── AcceptLanguage.java │ │ │ ├── Country.java │ │ │ └── VisitorId.java │ │ │ ├── InvalidUrlException.java │ │ │ ├── DaemonThreadFactory.java │ │ │ ├── MatomoException.java │ │ │ ├── package-info.java │ │ │ ├── TrustingX509TrustManager.java │ │ │ ├── TrackingParameter.java │ │ │ ├── Sender.java │ │ │ ├── CustomVariable.java │ │ │ ├── ProxyAuthenticator.java │ │ │ ├── ActionType.java │ │ │ ├── EcommerceItem.java │ │ │ ├── MatomoLocale.java │ │ │ ├── BulkRequest.java │ │ │ ├── ServiceLoaderSenderFactory.java │ │ │ ├── ExecutorServiceCloser.java │ │ │ ├── AuthToken.java │ │ │ ├── RequestValidator.java │ │ │ ├── MatomoRequestBuilder.java │ │ │ ├── TrackingParameterMethod.java │ │ │ └── MatomoDate.java │ └── test │ │ └── java │ │ └── org │ │ ├── matomo │ │ └── java │ │ │ └── tracking │ │ │ ├── TestThrowable.java │ │ │ ├── TestSenderFactory.java │ │ │ ├── InvalidUrlExceptionTest.java │ │ │ ├── parameters │ │ │ ├── UniqueIdTest.java │ │ │ ├── EcommerceItemsTest.java │ │ │ ├── HexTest.java │ │ │ ├── AcceptLanguageTest.java │ │ │ ├── DeviceResolutionTest.java │ │ │ ├── CustomVariableTest.java │ │ │ ├── CountryTest.java │ │ │ └── VisitorIdTest.java │ │ │ ├── MatomoLocaleTest.java │ │ │ ├── MatomoExceptionTest.java │ │ │ ├── PiwikLocaleTest.java │ │ │ ├── DaemonThreadFactoryTest.java │ │ │ ├── ServiceLoaderSenderFactoryTest.java │ │ │ ├── TrustingX509TrustManagerTest.java │ │ │ ├── BulkRequestTest.java │ │ │ ├── CustomVariableTest.java │ │ │ ├── PiwikDateTest.java │ │ │ ├── ExecutorServiceCloserTest.java │ │ │ ├── ProxyAuthenticatorTest.java │ │ │ ├── EcommerceItemTest.java │ │ │ ├── AuthTokenTest.java │ │ │ ├── TestSender.java │ │ │ ├── TrackingParameterMethodTest.java │ │ │ ├── RequestValidatorTest.java │ │ │ ├── MatomoRequestBuilderTest.java │ │ │ └── MatomoRequestTest.java │ │ └── piwik │ │ └── java │ │ └── tracking │ │ ├── CustomVariableTest.java │ │ ├── EcommerceItemTest.java │ │ └── PiwikTrackerIT.java └── pom.xml ├── java11 ├── src │ └── main │ │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── org.matomo.java.tracking.SenderProvider │ │ └── java │ │ └── org │ │ └── matomo │ │ └── java │ │ └── tracking │ │ └── Java11SenderProvider.java └── pom.xml ├── java8 ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── services │ │ │ │ └── org.matomo.java.tracking.SenderProvider │ │ └── java │ │ │ └── org │ │ │ └── matomo │ │ │ └── java │ │ │ └── tracking │ │ │ ├── TrustingHostnameVerifier.java │ │ │ └── Java8SenderProvider.java │ └── test │ │ └── java │ │ └── org │ │ └── matomo │ │ └── java │ │ └── tracking │ │ └── TrustingHostnameVerifierTest.java └── pom.xml ├── test ├── src │ └── main │ │ ├── resources │ │ ├── simplelogger.properties │ │ └── web │ │ │ └── track.html │ │ └── java │ │ └── org │ │ └── matomo │ │ └── java │ │ └── tracking │ │ └── test │ │ ├── SendExample.java │ │ ├── ServletMatomoRequestExample.java │ │ ├── BulkExample.java │ │ ├── MatomoServletTester.java │ │ ├── ConsumerExample.java │ │ └── EcommerceExample.java └── pom.xml ├── spring ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── spring │ │ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ │ └── java │ │ │ └── org │ │ │ └── matomo │ │ │ └── java │ │ │ └── tracking │ │ │ └── spring │ │ │ ├── package-info.java │ │ │ ├── TrackerConfigurationBuilderCustomizer.java │ │ │ ├── StandardTrackerConfigurationBuilderCustomizer.java │ │ │ ├── MatomoTrackerProperties.java │ │ │ └── MatomoTrackerAutoConfiguration.java │ └── test │ │ └── java │ │ └── org │ │ └── matomo │ │ └── java │ │ └── tracking │ │ └── spring │ │ ├── MatomoTrackerAutoConfigurationIT.java │ │ └── StandardTrackerConfigurationBuilderCustomizerIT.java └── pom.xml ├── .gitignore ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md ├── workflows │ ├── gh-pages.yml │ ├── build.yml │ ├── codeql-analysis.yml │ └── release.yml └── release-drafter.yml ├── servlet-jakarta ├── src │ ├── test │ │ └── java │ │ │ └── org │ │ │ └── matomo │ │ │ └── java │ │ │ └── tracking │ │ │ ├── TestSenderFactory.java │ │ │ ├── servlet │ │ │ └── JakartaHttpServletWrapperTest.java │ │ │ ├── TestSender.java │ │ │ └── MatomoTrackerFilterIT.java │ └── main │ │ └── java │ │ └── org │ │ └── matomo │ │ └── java │ │ └── tracking │ │ └── servlet │ │ ├── MatomoTrackerFilter.java │ │ └── JakartaHttpServletWrapper.java └── pom.xml ├── servlet-javax ├── src │ ├── test │ │ └── java │ │ │ └── org │ │ │ └── matomo │ │ │ └── java │ │ │ └── tracking │ │ │ ├── TestSenderFactory.java │ │ │ ├── servlet │ │ │ └── JavaxHttpServletWrapperTest.java │ │ │ ├── TestSender.java │ │ │ └── MatomoTrackerFilterIT.java │ └── main │ │ └── java │ │ └── org │ │ └── matomo │ │ └── java │ │ └── tracking │ │ └── servlet │ │ ├── MatomoTrackerFilter.java │ │ └── JavaxHttpServletWrapper.java └── pom.xml ├── SECURITY.md ├── docker-compose.yml ├── LICENSE ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md /core/src/main/java/lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true 2 | -------------------------------------------------------------------------------- /java11/src/main/resources/META-INF/services/org.matomo.java.tracking.SenderProvider: -------------------------------------------------------------------------------- 1 | org.matomo.java.tracking.Java11SenderProvider -------------------------------------------------------------------------------- /java8/src/main/resources/META-INF/services/org.matomo.java.tracking.SenderProvider: -------------------------------------------------------------------------------- 1 | org.matomo.java.tracking.Java8SenderProvider -------------------------------------------------------------------------------- /test/src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.defaultLogLevel=debug 2 | org.slf4j.simpleLogger.log.org.eclipse.jetty=info -------------------------------------------------------------------------------- /spring/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | org.matomo.java.tracking.spring.MatomoTrackerAutoConfiguration 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *\.DS_Store 3 | *#*# 4 | *.swp 5 | info 6 | **/target/ 7 | hs_err_pid* 8 | *.iml 9 | .idea 10 | .classpath 11 | .project 12 | test/logs/* 13 | !test/logs/.gitkeep -------------------------------------------------------------------------------- /core/src/main/java/org/piwik/java/tracking/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Piwik Java Tracking API. Renamed to {@link org.matomo.java.tracking} in 3.0.0. 3 | */ 4 | 5 | package org.piwik.java.tracking; 6 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/SenderProvider.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | interface SenderProvider { 4 | 5 | Sender provideSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/TestThrowable.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | class TestThrowable extends Throwable { 4 | 5 | TestThrowable() { 6 | super("message", null, false, false); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/SenderFactory.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | /** 4 | * A factory for {@link Sender} instances. 5 | */ 6 | public interface SenderFactory { 7 | 8 | Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/servlet/CookieWrapper.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.servlet; 2 | 3 | import lombok.Value; 4 | 5 | /** 6 | * Wrapper for the cookie name and value. 7 | */ 8 | @Value 9 | public class CookieWrapper { 10 | 11 | String name; 12 | 13 | String value; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | assignees: 8 | - dheid 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: weekly 13 | assignees: 14 | - dheid 15 | -------------------------------------------------------------------------------- /test/src/main/resources/web/track.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Matomo Java Tracker Servlet Test 7 | 8 | 9 | Thank you! Your request was sent to Matomo at http://localhost:8080. 10 | 11 | -------------------------------------------------------------------------------- /java8/src/test/java/org/matomo/java/tracking/TrustingHostnameVerifierTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class TrustingHostnameVerifierTest { 8 | 9 | @Test 10 | void verifyAlwaysReturnsTrue() { 11 | 12 | boolean verified = new TrustingHostnameVerifier().verify(null, null); 13 | 14 | assertThat(verified).isTrue(); 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /spring/src/main/java/org/matomo/java/tracking/spring/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides Spring specific classes to integrate Matomo tracking into Spring applications. 3 | * 4 | *

See {@link org.matomo.java.tracking.spring.MatomoTrackerProperties} for the available configuration properties. 5 | * 6 | *

See {@link org.matomo.java.tracking.spring.MatomoTrackerAutoConfiguration} for the available configuration 7 | * options. 8 | */ 9 | 10 | package org.matomo.java.tracking.spring; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: dheid 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Code snippets** 14 | Code to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - [ ] Ensure that the pull request title represents the desired changelog entry 4 | - [ ] Please describe what you did 5 | - [ ] Link to relevant issues in GitHub 6 | - [ ] Link to relevant pull requests, esp. upstream and downstream changes 7 | - [ ] Ensure you have provided tests - that demonstrates feature works or fixes the issue 8 | 9 | 12 | -------------------------------------------------------------------------------- /java8/src/main/java/org/matomo/java/tracking/TrustingHostnameVerifier.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.Nullable; 4 | import javax.net.ssl.HostnameVerifier; 5 | import javax.net.ssl.SSLSession; 6 | 7 | class TrustingHostnameVerifier implements HostnameVerifier { 8 | 9 | @Override 10 | public boolean verify( 11 | @Nullable 12 | String hostname, 13 | @Nullable 14 | SSLSession session 15 | ) { 16 | return true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains types for Matomo Tracking Parameters according to 3 | * the Matomo Tracking HTTP API. 4 | * 5 | *

The types help you to use the correct format for the tracking parameters. The package was introduced in Matomo 6 | * Java Tracker version 3 to let the tracker be more self-explanatory and better maintainable. 7 | */ 8 | 9 | package org.matomo.java.tracking.parameters; 10 | -------------------------------------------------------------------------------- /core/src/test/java/org/piwik/java/tracking/CustomVariableTest.java: -------------------------------------------------------------------------------- 1 | package org.piwik.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class CustomVariableTest { 8 | 9 | @Test 10 | void createsCustomVariable() { 11 | CustomVariable customVariable = new CustomVariable("key", "value"); 12 | 13 | assertThat(customVariable.getKey()).isEqualTo("key"); 14 | assertThat(customVariable.getValue()).isEqualTo("value"); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/TestSenderFactory.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import lombok.Getter; 4 | 5 | class TestSenderFactory implements SenderFactory { 6 | 7 | @Getter 8 | private TestSender testSender; 9 | 10 | @Override 11 | public Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator) { 12 | TestSender testSender = new TestSender(trackerConfiguration, queryCreator); 13 | this.testSender = testSender; 14 | return testSender; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/InvalidUrlException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | /** 11 | * Thrown when an invalid URL is passed to the tracker. 12 | */ 13 | public class InvalidUrlException extends RuntimeException { 14 | 15 | InvalidUrlException(Throwable cause) { 16 | super(cause); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /servlet-jakarta/src/test/java/org/matomo/java/tracking/TestSenderFactory.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import lombok.Getter; 4 | 5 | class TestSenderFactory implements SenderFactory { 6 | 7 | @Getter 8 | private TestSender testSender; 9 | 10 | @Override 11 | public Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator) { 12 | TestSender testSender = new TestSender(trackerConfiguration, queryCreator); 13 | this.testSender = testSender; 14 | return testSender; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /servlet-javax/src/test/java/org/matomo/java/tracking/TestSenderFactory.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import lombok.Getter; 4 | 5 | class TestSenderFactory implements SenderFactory { 6 | 7 | @Getter 8 | private TestSender testSender; 9 | 10 | @Override 11 | public Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator) { 12 | TestSender testSender = new TestSender(trackerConfiguration, queryCreator); 13 | this.testSender = testSender; 14 | return testSender; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/InvalidUrlExceptionTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class InvalidUrlExceptionTest { 8 | 9 | @Test 10 | void createsInvalidUrlException() { 11 | InvalidUrlException invalidUrlException = new InvalidUrlException(new Throwable()); 12 | 13 | assertThat(invalidUrlException).isNotNull(); 14 | assertThat(invalidUrlException.getCause()).isNotNull(); 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/DaemonThreadFactory.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import java.util.concurrent.ThreadFactory; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | class DaemonThreadFactory implements ThreadFactory { 8 | 9 | private final AtomicInteger count = new AtomicInteger(); 10 | 11 | @Override 12 | public Thread newThread(@NonNull Runnable r) { 13 | Thread thread = new Thread(null, r, String.format("MatomoJavaTracker-%d", count.getAndIncrement())); 14 | thread.setDaemon(true); 15 | return thread; 16 | } 17 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The following versions of this library are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | |---------|------------------------| 10 | | >3 | :white_check_mark: yes | 11 | | <=2 | ✖️ no | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | If you found a security vulerability please don't hesitate to send me a message, 16 | open a new [discussion](https://github.com/matomo-org/matomo-java-tracker/discussions) or 17 | open a new [issue](https://github.com/matomo-org/matomo-java-tracker/issues). 18 | -------------------------------------------------------------------------------- /core/src/test/java/org/piwik/java/tracking/EcommerceItemTest.java: -------------------------------------------------------------------------------- 1 | package org.piwik.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class EcommerceItemTest { 8 | 9 | @Test 10 | void createsEcItem() { 11 | EcommerceItem item = new EcommerceItem("sku", "name", "category", 1.0, 1); 12 | 13 | assertThat(item.getSku()).isEqualTo("sku"); 14 | assertThat(item.getName()).isEqualTo("name"); 15 | assertThat(item.getCategory()).isEqualTo("category"); 16 | assertThat(item.getPrice()).isEqualTo(1.0); 17 | assertThat(item.getQuantity()).isEqualTo(1); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | database: 4 | image: mariadb:10.11.5-jammy 5 | command: --max-allowed-packet=64MB 6 | environment: 7 | - MYSQL_ROOT_PASSWORD=matomo 8 | - MYSQL_PASSWORD=matomo 9 | - MYSQL_DATABASE=matomo 10 | - MYSQL_USER=matomo 11 | matomo: 12 | image: matomo:4.15.1-apache 13 | environment: 14 | - MATOMO_DATABASE_HOST=database 15 | - MATOMO_DATABASE_ADAPTER=mysql 16 | - MATOMO_DATABASE_TABLES_PREFIX=matomo_ 17 | - MATOMO_DATABASE_USERNAME=matomo 18 | - MATOMO_DATABASE_PASSWORD=matomo 19 | - MATOMO_DATABASE_DBNAME=matomo 20 | ports: 21 | - '8080:80' 22 | -------------------------------------------------------------------------------- /java8/src/main/java/org/matomo/java/tracking/Java8SenderProvider.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import java.util.concurrent.Executors; 4 | 5 | /** 6 | * Provides a {@link Sender} implementation based on Java 8. 7 | */ 8 | public class Java8SenderProvider implements SenderProvider { 9 | 10 | @Override 11 | public Sender provideSender( 12 | TrackerConfiguration trackerConfiguration, QueryCreator queryCreator 13 | ) { 14 | return new Java8Sender( 15 | trackerConfiguration, 16 | queryCreator, 17 | Executors.newFixedThreadPool(trackerConfiguration.getThreadPoolSize(), new DaemonThreadFactory()) 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/Hex.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import lombok.NonNull; 11 | 12 | final class Hex { 13 | 14 | private Hex() { 15 | // utility class 16 | } 17 | 18 | static String fromBytes(@NonNull byte[] bytes) { 19 | StringBuilder result = new StringBuilder(bytes.length * 2); 20 | for (byte b : bytes) { 21 | result.append(String.format("%02x", b)); 22 | } 23 | return result.toString(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[REQUEST]" 5 | labels: enhancement 6 | assignees: dheid 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/parameters/UniqueIdTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.parameters; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class UniqueIdTest { 8 | 9 | @Test 10 | void createsRandomUniqueId() { 11 | 12 | UniqueId uniqueId = UniqueId.random(); 13 | 14 | assertThat(uniqueId.toString()).matches("[0-9a-zA-Z]{6}"); 15 | 16 | } 17 | 18 | @Test 19 | void createsSameUniqueIds() { 20 | 21 | UniqueId uniqueId1 = UniqueId.fromValue(868686868L); 22 | UniqueId uniqueId2 = UniqueId.fromValue(868686868); 23 | 24 | assertThat(uniqueId1).hasToString(uniqueId2.toString()); 25 | 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/MatomoLocaleTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import java.util.Locale; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class MatomoLocaleTest { 10 | 11 | @Test 12 | void createsMatomoLocaleFromLocale() { 13 | MatomoLocale locale = new MatomoLocale(Locale.US); 14 | assertThat(locale.toString()).isEqualTo("us"); 15 | } 16 | 17 | @Test 18 | void failsIfLocaleIsNull() { 19 | assertThatThrownBy(() -> new MatomoLocale(null)) 20 | .isInstanceOf(NullPointerException.class) 21 | .hasMessage("Locale must not be null"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/MatomoException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | /** 11 | * Thrown when an error occurs while communicating with the Matomo server or when the request is invalid. 12 | */ 13 | public class MatomoException extends RuntimeException { 14 | 15 | private static final long serialVersionUID = 4592083764365938934L; 16 | 17 | MatomoException(String message) { 18 | super(message); 19 | } 20 | 21 | MatomoException(String message, Throwable cause) { 22 | super(message, cause); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package contains classes that allow you to specify a {@link org.matomo.java.tracking.MatomoTracker} 3 | * with the corresponding {@link org.matomo.java.tracking.TrackerConfiguration}. You can then send a 4 | * {@link org.matomo.java.tracking.MatomoRequest} as a single HTTP GET request or multiple requests as a bulk HTTP POST 5 | * request synchronously or asynchronously. If an exception occurs, {@link org.matomo.java.tracking.MatomoException} 6 | * will be thrown. 7 | * 8 | *

For more information about the Matomo Tracking HTTP API, see the Matomo Tracking HTTP API. 9 | */ 10 | 11 | package org.matomo.java.tracking; 12 | 13 | 14 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/TrustingX509TrustManager.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.Nullable; 4 | import java.security.cert.X509Certificate; 5 | import javax.net.ssl.X509TrustManager; 6 | 7 | class TrustingX509TrustManager implements X509TrustManager { 8 | 9 | @Override 10 | @Nullable 11 | public X509Certificate[] getAcceptedIssuers() { 12 | return null; 13 | } 14 | 15 | @Override 16 | public void checkClientTrusted( 17 | @Nullable X509Certificate[] chain, @Nullable String authType 18 | ) { 19 | // no operation 20 | } 21 | 22 | @Override 23 | public void checkServerTrusted( 24 | @Nullable X509Certificate[] chain, @Nullable String authType 25 | ) { 26 | // no operation 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/MatomoExceptionTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class MatomoExceptionTest { 8 | 9 | @Test 10 | void createsMatomoExceptionWithMessage() { 11 | MatomoException matomoException = new MatomoException("message"); 12 | 13 | assertEquals("message", matomoException.getMessage()); 14 | } 15 | 16 | @Test 17 | void createsMatomoExceptionWithMessageAndCause() { 18 | Throwable cause = new Throwable(); 19 | MatomoException matomoException = new MatomoException("message", cause); 20 | 21 | assertEquals("message", matomoException.getMessage()); 22 | assertEquals(cause, matomoException.getCause()); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/PiwikLocaleTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Locale; 6 | import org.junit.jupiter.api.Test; 7 | import org.piwik.java.tracking.PiwikLocale; 8 | 9 | class PiwikLocaleTest { 10 | 11 | private final PiwikLocale locale = new PiwikLocale(Locale.US); 12 | 13 | /** 14 | * Test of setLocale method, of class PiwikLocale. 15 | */ 16 | @Test 17 | void testLocale() { 18 | locale.setLocale(Locale.GERMANY); 19 | assertThat(locale.getLocale()).isEqualTo(Locale.GERMAN); 20 | } 21 | 22 | /** 23 | * Test of toString method, of class PiwikLocale. 24 | */ 25 | @Test 26 | void testToString() { 27 | assertThat(locale).hasToString("us"); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/DaemonThreadFactoryTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class DaemonThreadFactoryTest { 8 | 9 | private final DaemonThreadFactory daemonThreadFactory = new DaemonThreadFactory(); 10 | 11 | @Test 12 | void createsNewThreadAsDaemonThread() { 13 | Thread thread = daemonThreadFactory.newThread(() -> { 14 | // do nothing 15 | }); 16 | assertThat(thread.isDaemon()).isTrue(); 17 | } 18 | 19 | @Test 20 | void createsNewThreadWithMatomoJavaTrackerName() { 21 | Thread thread = daemonThreadFactory.newThread(() -> { 22 | // do nothing 23 | }); 24 | assertThat(thread.getName()).startsWith("MatomoJavaTracker-"); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/TrackingParameter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | import java.lang.annotation.ElementType; 11 | import java.lang.annotation.Retention; 12 | import java.lang.annotation.RetentionPolicy; 13 | import java.lang.annotation.Target; 14 | 15 | @Target(ElementType.FIELD) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @interface TrackingParameter { 18 | 19 | String name(); 20 | 21 | String regex() default ""; 22 | 23 | double min() default Double.MIN_VALUE; 24 | 25 | double max() default Double.MAX_VALUE; 26 | 27 | int maxLength() default Integer.MAX_VALUE; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/Sender.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import edu.umd.cs.findbugs.annotations.Nullable; 5 | import java.util.Collection; 6 | import java.util.concurrent.CompletableFuture; 7 | 8 | interface Sender extends AutoCloseable { 9 | @NonNull 10 | CompletableFuture sendSingleAsync( 11 | @NonNull MatomoRequest request 12 | ); 13 | 14 | void sendSingle( 15 | @NonNull MatomoRequest request 16 | ); 17 | 18 | void sendBulk( 19 | @NonNull Iterable requests, @Nullable String overrideAuthToken 20 | ); 21 | 22 | @NonNull 23 | CompletableFuture sendBulkAsync( 24 | @NonNull Collection requests, @Nullable String overrideAuthToken 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy GitHub Pages 2 | on: 3 | push: 4 | branches: ["main"] 5 | workflow_dispatch: 6 | permissions: 7 | contents: read 8 | pages: write 9 | id-token: write 10 | concurrency: 11 | group: "pages" 12 | cancel-in-progress: false 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/configure-pages@v5 19 | - uses: actions/jekyll-build-pages@v1 20 | with: 21 | source: ./ 22 | destination: ./_site 23 | - uses: actions/upload-pages-artifact@v3 24 | deploy: 25 | environment: 26 | name: github-pages 27 | url: ${{ steps.deployment.outputs.page_url }} 28 | runs-on: ubuntu-latest 29 | needs: build 30 | steps: 31 | - id: deployment 32 | uses: actions/deploy-pages@v4 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | pull-requests: write 14 | checks: write 15 | contents: read 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-java@v4 19 | with: 20 | java-version: '17' 21 | distribution: 'temurin' 22 | cache: maven 23 | - run: mvn -B verify 24 | - uses: madrapps/jacoco-report@v1.7.1 25 | with: 26 | paths: ${{ github.workspace }}/target/site/jacoco/jacoco.xml 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | min-coverage-overall: 80 29 | min-coverage-changed-files: 80 30 | - uses: scacap/action-surefire-report@v1.9.0 -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/ServiceLoaderSenderFactoryTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 4 | 5 | import java.net.URI; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class ServiceLoaderSenderFactoryTest { 9 | 10 | @Test 11 | void failsIfNoImplementationFound() { 12 | ServiceLoaderSenderFactory serviceLoaderSenderFactory = new ServiceLoaderSenderFactory(); 13 | 14 | TrackerConfiguration trackerConfiguration = 15 | TrackerConfiguration.builder().apiEndpoint(URI.create("http://localhost/matomo.php")).build(); 16 | 17 | assertThatThrownBy(() -> serviceLoaderSenderFactory.createSender(trackerConfiguration, 18 | new QueryCreator(trackerConfiguration) 19 | )) 20 | .isInstanceOf(MatomoException.class) 21 | .hasMessage("No SenderProvider found"); 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/TrustingX509TrustManagerTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.security.cert.X509Certificate; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class TrustingX509TrustManagerTest { 9 | 10 | private final TrustingX509TrustManager trustingX509TrustManager = new TrustingX509TrustManager(); 11 | 12 | @Test 13 | void acceptedIssuersIsAlwaysNull() { 14 | X509Certificate[] acceptedIssuers = trustingX509TrustManager.getAcceptedIssuers(); 15 | assertThat(acceptedIssuers).isNull(); 16 | } 17 | 18 | @Test 19 | void checkClientTrustedDoesNothing() { 20 | trustingX509TrustManager.checkClientTrusted(null, null); 21 | } 22 | 23 | @Test 24 | void checkServerTrustedDoesNothing() { 25 | trustingX509TrustManager.checkServerTrusted(null, null); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '27 11 * * 5' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'java' ] 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/setup-java@v4 28 | with: 29 | java-version: '17' 30 | distribution: 'temurin' 31 | cache: maven 32 | - uses: github/codeql-action/init@v3 33 | with: 34 | languages: ${{ matrix.language }} 35 | - uses: github/codeql-action/autobuild@v3 36 | - uses: github/codeql-action/analyze@v3 37 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/CustomVariable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | import lombok.NonNull; 11 | 12 | /** 13 | * A user defined custom variable. 14 | * 15 | * @author brettcsorba 16 | * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. 17 | */ 18 | @Deprecated 19 | public class CustomVariable extends org.matomo.java.tracking.parameters.CustomVariable { 20 | 21 | /** 22 | * Instantiates a new custom variable. 23 | * 24 | * @param key the key of the custom variable (required) 25 | * @param value the value of the custom variable (required) 26 | */ 27 | public CustomVariable(@NonNull String key, @NonNull String value) { 28 | super(key, value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/java/org/piwik/java/tracking/PiwikLocale.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.piwik.java.tracking; 9 | 10 | import java.util.Locale; 11 | import org.matomo.java.tracking.parameters.Country; 12 | 13 | /** 14 | * A locale object that can be used to send visitor country to Matomo. This class is deprecated and will be removed in 15 | * the future. 16 | * 17 | * @author brettcsorba 18 | * @deprecated Use {@link Country} instead. 19 | */ 20 | @Deprecated 21 | public class PiwikLocale extends Country { 22 | 23 | /** 24 | * Creates a new Piwik locale object with the specified locale. 25 | * 26 | * @param locale the locale to use 27 | * @deprecated Use {@link Country} instead. 28 | */ 29 | @Deprecated 30 | public PiwikLocale(Locale locale) { 31 | super(locale); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/ProxyAuthenticator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | import edu.umd.cs.findbugs.annotations.Nullable; 11 | import java.net.Authenticator; 12 | import java.net.PasswordAuthentication; 13 | import lombok.NonNull; 14 | import lombok.RequiredArgsConstructor; 15 | 16 | @RequiredArgsConstructor 17 | class ProxyAuthenticator extends Authenticator { 18 | 19 | @NonNull 20 | private final String user; 21 | 22 | @NonNull 23 | private final String password; 24 | 25 | @Nullable 26 | @Override 27 | protected PasswordAuthentication getPasswordAuthentication() { 28 | if (getRequestorType() == RequestorType.PROXY) { 29 | return new PasswordAuthentication(user, password.toCharArray()); 30 | } 31 | return null; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import lombok.AllArgsConstructor; 11 | import lombok.Builder; 12 | import lombok.Getter; 13 | import lombok.Setter; 14 | 15 | /** 16 | * Represents an item in an ecommerce order. 17 | */ 18 | @Builder 19 | @AllArgsConstructor 20 | @Getter 21 | @Setter 22 | public class EcommerceItem { 23 | 24 | private String sku; 25 | 26 | @Builder.Default 27 | private String name = ""; 28 | 29 | @Builder.Default 30 | private String category = ""; 31 | 32 | @Builder.Default 33 | private Double price = 0.0; 34 | 35 | @Builder.Default 36 | private Integer quantity = 0; 37 | 38 | public String toString() { 39 | return String.format("[\"%s\",\"%s\",\"%s\",%s,%d]", sku, name, category, price, quantity); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/ActionType.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import java.util.function.BiConsumer; 4 | import lombok.NonNull; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | /** 8 | * The type of action performed (download or outlink). 9 | */ 10 | @RequiredArgsConstructor 11 | public enum ActionType { 12 | DOWNLOAD(MatomoRequest.MatomoRequestBuilder::downloadUrl), 13 | LINK(MatomoRequest.MatomoRequestBuilder::outlinkUrl); 14 | 15 | @NonNull 16 | private final BiConsumer consumer; 17 | 18 | /** 19 | * Applies the action URL to the given builder. 20 | * 21 | * @param builder The builder to apply the action URL to. 22 | * @param actionUrl The action URL to apply. 23 | * 24 | * @return The builder with the action URL applied. 25 | */ 26 | public MatomoRequest.MatomoRequestBuilder applyUrl( 27 | @NonNull MatomoRequest.MatomoRequestBuilder builder, 28 | @NonNull String actionUrl 29 | ) { 30 | consumer.accept(builder, actionUrl); 31 | return builder; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/org/piwik/java/tracking/CustomVariable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.piwik.java.tracking; 9 | 10 | /** 11 | * A user defined custom variable. 12 | * 13 | *

Renamed to {@link org.matomo.java.tracking.parameters.CustomVariable} in 3.0.0. 14 | * 15 | * @author brettcsorba 16 | * @deprecated Use {@link org.matomo.java.tracking.parameters.CustomVariable} instead. 17 | */ 18 | @Deprecated 19 | public class CustomVariable extends org.matomo.java.tracking.parameters.CustomVariable { 20 | 21 | /** 22 | * Instantiates a new custom variable. 23 | * 24 | * @param key the key of the custom variable (required) 25 | * @param value the value of the custom variable (required) 26 | * @deprecated Use {@link org.matomo.java.tracking.parameters.CustomVariable} instead. 27 | */ 28 | @Deprecated 29 | public CustomVariable(String key, String value) { 30 | super(key, value); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/EcommerceItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | 11 | /** 12 | * A user defined custom variable. 13 | * 14 | * @author brettcsorba 15 | * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. 16 | */ 17 | @Deprecated 18 | public class EcommerceItem extends org.matomo.java.tracking.parameters.EcommerceItem { 19 | 20 | 21 | /** 22 | * Instantiates a new ecommerce item. 23 | * 24 | * @param sku the sku (Stock Keeping Unit) of the item 25 | * @param name the name of the item 26 | * @param category the category of the item 27 | * @param price the price of the item 28 | * @param quantity the quantity of the item 29 | */ 30 | public EcommerceItem( 31 | String sku, String name, String category, Double price, Integer quantity 32 | ) { 33 | super(sku, name, category, price, quantity); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/EcommerceItems.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | import lombok.AllArgsConstructor; 14 | import lombok.Builder; 15 | import lombok.EqualsAndHashCode; 16 | import lombok.Getter; 17 | import lombok.NoArgsConstructor; 18 | import lombok.Setter; 19 | import lombok.Singular; 20 | import lombok.experimental.Delegate; 21 | 22 | /** 23 | * Multiple things that you can buy online. 24 | */ 25 | @Builder 26 | @AllArgsConstructor 27 | @NoArgsConstructor 28 | @EqualsAndHashCode 29 | @Getter 30 | @Setter 31 | public class EcommerceItems { 32 | 33 | @Delegate 34 | @Singular 35 | private List items = new ArrayList<>(); 36 | 37 | public String toString() { 38 | return items.stream().map(String::valueOf).collect(Collectors.joining(",", "[", "]")); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/BulkRequestTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static java.util.Collections.emptyList; 4 | import static java.util.Collections.singleton; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | class BulkRequestTest { 11 | 12 | @Test 13 | void formatsQueriesAsJson() { 14 | BulkRequest bulkRequest = BulkRequest.builder() 15 | .queries(singleton("idsite=1&rec=1&action_name=TestBulkRequest")) 16 | .authToken("token") 17 | .build(); 18 | 19 | byte[] bytes = bulkRequest.toBytes(); 20 | 21 | assertThat(new String(bytes)).isEqualTo("{\"requests\":[\"?idsite=1&rec=1&action_name=TestBulkRequest\"],\"token_auth\":\"token\"}"); 22 | } 23 | 24 | @Test 25 | void failsIfQueriesAreEmpty() { 26 | 27 | BulkRequest bulkRequest = BulkRequest.builder().queries(emptyList()).build(); 28 | 29 | assertThatThrownBy(bulkRequest::toBytes) 30 | .isInstanceOf(IllegalArgumentException.class) 31 | .hasMessage("Queries must not be empty"); 32 | 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/MatomoLocale.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | import static java.util.Objects.requireNonNull; 11 | 12 | import edu.umd.cs.findbugs.annotations.NonNull; 13 | import java.util.Locale; 14 | import lombok.Getter; 15 | import lombok.Setter; 16 | import org.matomo.java.tracking.parameters.Country; 17 | 18 | /** 19 | * Object representing a locale required by some Matomo query parameters. 20 | * 21 | * @author brettcsorba 22 | * @deprecated Use {@link Country} instead 23 | */ 24 | @Setter 25 | @Getter 26 | @Deprecated 27 | public class MatomoLocale extends Country { 28 | 29 | /** 30 | * Constructs a new MatomoLocale. 31 | * 32 | * @param locale The locale to get the country code from 33 | * @deprecated Please use {@link Country} 34 | */ 35 | @Deprecated 36 | public MatomoLocale( 37 | @NonNull 38 | Locale locale 39 | ) { 40 | super(requireNonNull(locale, "Locale must not be null")); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/java/org/piwik/java/tracking/PiwikRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.piwik.java.tracking; 9 | 10 | import static java.util.Objects.requireNonNull; 11 | 12 | import java.net.URL; 13 | import org.matomo.java.tracking.MatomoRequest; 14 | 15 | /** 16 | * A request object that can be used to send requests to Matomo. This class is deprecated and will be removed in the 17 | * future. 18 | * 19 | * @author brettcsorba 20 | * @deprecated Use {@link MatomoRequest} instead. 21 | */ 22 | @Deprecated 23 | public class PiwikRequest extends MatomoRequest { 24 | 25 | /** 26 | * Creates a new request object with the specified site ID and action URL. 27 | * 28 | * @param siteId the site ID 29 | * @param actionUrl the action URL. Must not be null. 30 | * @deprecated Use {@link MatomoRequest} instead. 31 | */ 32 | @Deprecated 33 | public PiwikRequest(int siteId, URL actionUrl) { 34 | super(siteId, requireNonNull(actionUrl, "Action URL must not be null").toString()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/CustomVariableTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class CustomVariableTest { 9 | 10 | @Test 11 | void createsCustomVariable() { 12 | CustomVariable customVariable = new CustomVariable("key", "value"); 13 | 14 | assertThat(customVariable.getKey()).isEqualTo("key"); 15 | assertThat(customVariable.getValue()).isEqualTo("value"); 16 | } 17 | 18 | @Test 19 | void failsOnNullKey() { 20 | assertThatThrownBy(() -> new CustomVariable( 21 | null, 22 | "value" 23 | )).isInstanceOf(NullPointerException.class); 24 | } 25 | 26 | @Test 27 | void failsOnNullValue() { 28 | assertThatThrownBy(() -> new CustomVariable( 29 | "key", 30 | null 31 | )).isInstanceOf(NullPointerException.class); 32 | } 33 | 34 | @Test 35 | void failsOnNullKeyAndValue() { 36 | assertThatThrownBy(() -> new CustomVariable( 37 | null, 38 | null 39 | )).isInstanceOf(NullPointerException.class); 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/java/org/piwik/java/tracking/EcommerceItem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.piwik.java.tracking; 9 | 10 | /** 11 | * Describes an item in an ecommerce transaction. 12 | * 13 | * @author brettcsorba 14 | * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. 15 | */ 16 | @Deprecated 17 | public class EcommerceItem extends org.matomo.java.tracking.parameters.EcommerceItem { 18 | 19 | /** 20 | * Creates a new ecommerce item. 21 | * 22 | * @param sku the sku (Stock Keeping Unit) of the item. 23 | * @param name the name of the item. 24 | * @param category the category of the item. 25 | * @param price the price of the item. 26 | * @param quantity the quantity of the item. 27 | * @deprecated Use {@link org.matomo.java.tracking.parameters.EcommerceItem} instead. 28 | */ 29 | @Deprecated 30 | public EcommerceItem(String sku, String name, String category, Double price, Integer quantity) { 31 | super(sku, name, category, price, quantity); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /core/src/test/java/org/piwik/java/tracking/PiwikTrackerIT.java: -------------------------------------------------------------------------------- 1 | package org.piwik.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class PiwikTrackerIT { 8 | 9 | private PiwikTracker piwikTracker; 10 | 11 | @Test 12 | void createsNewPiwikTrackerInstanceWithHostUrl() { 13 | 14 | piwikTracker = new PiwikTracker("http://localhost:8080"); 15 | 16 | assertThat(piwikTracker).isNotNull(); 17 | 18 | } 19 | 20 | @Test 21 | void createsNewPiwikTrackerInstanceWithHostUrlAndTimeout() { 22 | 23 | piwikTracker = new PiwikTracker("http://localhost:8080", 1000); 24 | 25 | assertThat(piwikTracker).isNotNull(); 26 | 27 | } 28 | 29 | @Test 30 | void createsNewPiwikTrackerInstanceWithHostUrlAndProxySettings() { 31 | 32 | piwikTracker = new PiwikTracker("http://localhost:8080", "localhost", 8080); 33 | 34 | assertThat(piwikTracker).isNotNull(); 35 | 36 | } 37 | 38 | @Test 39 | void createsNewPiwikTrackerInstanceWithHostUrlAndProxySettingsAndTimeout() { 40 | 41 | piwikTracker = new PiwikTracker("http://localhost:8080", "localhost", 8080, 1000); 42 | 43 | assertThat(piwikTracker).isNotNull(); 44 | 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/PiwikDateTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.TimeZone; 6 | import org.junit.jupiter.api.Test; 7 | import org.piwik.java.tracking.PiwikDate; 8 | 9 | 10 | class PiwikDateTest { 11 | 12 | /** 13 | * Test of constructor, of class PiwikDate. 14 | */ 15 | @Test 16 | void testConstructor0() { 17 | PiwikDate date = new PiwikDate(); 18 | assertThat(date).isNotNull(); 19 | } 20 | 21 | @Test 22 | void testConstructor1() { 23 | PiwikDate date = new PiwikDate(1433186085092L); 24 | assertThat(date).isNotNull(); 25 | assertThat(date.getTime()).isEqualTo(1433186085092L); 26 | } 27 | 28 | @Test 29 | void testConstructor2() { 30 | PiwikDate date = new PiwikDate(1467437553000L); 31 | assertThat(date.getTime()).isEqualTo(1467437553000L); 32 | } 33 | 34 | /** 35 | * Test of setTimeZone method, of class PiwikDate. 36 | */ 37 | @Test 38 | void testSetTimeZone() { 39 | PiwikDate date = new PiwikDate(1433186085092L); 40 | date.setTimeZone(TimeZone.getTimeZone("America/New_York")); 41 | assertThat(date.getTime()).isEqualTo(1433186085092L); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/parameters/EcommerceItemsTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.parameters; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | 8 | class EcommerceItemsTest { 9 | 10 | @Test 11 | void formatsJson() { 12 | EcommerceItems ecommerceItems = EcommerceItems 13 | .builder() 14 | .item(EcommerceItem 15 | .builder() 16 | .sku("XYZ12345") 17 | .name("Matomo - The big book about web analytics") 18 | .category("Education & Teaching") 19 | .price(23.1) 20 | .quantity(2) 21 | .build()) 22 | .item(EcommerceItem 23 | .builder() 24 | .sku("B0C2WV3MRJ") 25 | .name("Matomo for data visualization") 26 | .category("Education & Teaching") 27 | .price(15.1) 28 | .quantity(1) 29 | .build()) 30 | .build(); 31 | assertThat(ecommerceItems).hasToString("[[\"XYZ12345\",\"Matomo - The big book about web analytics\",\"Education & Teaching\",23.1,2],[\"B0C2WV3MRJ\",\"Matomo for data visualization\",\"Education & Teaching\",15.1,1]]"); 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/BulkRequest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.Nullable; 4 | import java.nio.charset.StandardCharsets; 5 | import java.util.Collection; 6 | import java.util.Iterator; 7 | import lombok.Builder; 8 | import lombok.NonNull; 9 | import lombok.Value; 10 | 11 | @Builder 12 | @Value 13 | class BulkRequest { 14 | 15 | @NonNull 16 | Collection queries; 17 | 18 | @Nullable 19 | String authToken; 20 | 21 | byte[] toBytes( 22 | 23 | ) { 24 | if (queries.isEmpty()) { 25 | throw new IllegalArgumentException("Queries must not be empty"); 26 | } 27 | StringBuilder payload = new StringBuilder("{\"requests\":["); 28 | Iterator iterator = queries.iterator(); 29 | while (iterator.hasNext()) { 30 | String query = iterator.next(); 31 | payload.append("\"?").append(query).append('"'); 32 | if (iterator.hasNext()) { 33 | payload.append(','); 34 | } 35 | } 36 | payload.append(']'); 37 | if (authToken != null) { 38 | payload.append(",\"token_auth\":\"").append(authToken).append('"'); 39 | } 40 | return payload.append('}').toString().getBytes(StandardCharsets.UTF_8); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/parameters/HexTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.parameters; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import java.util.stream.Stream; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.Arguments; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | 12 | class HexTest { 13 | 14 | 15 | 16 | @Test 17 | void failsIfBytesAreNull() { 18 | assertThatThrownBy(() -> Hex.fromBytes(null)) 19 | .isInstanceOf(NullPointerException.class) 20 | .hasMessage("bytes is marked non-null but is null"); 21 | } 22 | 23 | private static Stream testBytes() { 24 | return Stream.of( 25 | Arguments.of(new byte[] {0x00, 0x01, 0x02, 0x03}, "00010203"), 26 | Arguments.of(new byte[] {(byte) 0xFF, (byte) 0xFE, (byte) 0xFD, (byte) 0xFC}, "fffefdfc"), 27 | Arguments.of(new byte[0], "") 28 | ); 29 | } 30 | 31 | @ParameterizedTest 32 | @MethodSource("testBytes") 33 | void convertsBytesIntoHex(byte[] bytes, String expected) { 34 | assertThat(Hex.fromBytes(bytes)).isEqualTo(expected); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/ServiceLoaderSenderFactory.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static java.util.stream.Collectors.toMap; 4 | 5 | import java.util.Map; 6 | import java.util.ServiceLoader; 7 | import java.util.function.Function; 8 | import java.util.stream.StreamSupport; 9 | 10 | class ServiceLoaderSenderFactory implements SenderFactory { 11 | 12 | @Override 13 | public Sender createSender(TrackerConfiguration trackerConfiguration, QueryCreator queryCreator) { 14 | ServiceLoader serviceLoader = ServiceLoader.load(SenderProvider.class); 15 | Map senderProviders = StreamSupport 16 | .stream(serviceLoader.spliterator(), false) 17 | .collect(toMap(senderProvider -> senderProvider.getClass().getName(), Function.identity())); 18 | SenderProvider senderProvider = senderProviders.get("org.matomo.java.tracking.Java11SenderProvider"); 19 | if (senderProvider == null) { 20 | senderProvider = senderProviders.get("org.matomo.java.tracking.Java8SenderProvider"); 21 | } 22 | if (senderProvider == null) { 23 | throw new MatomoException("No SenderProvider found"); 24 | } 25 | return senderProvider.provideSender(trackerConfiguration, new QueryCreator(trackerConfiguration)); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /spring/src/main/java/org/matomo/java/tracking/spring/TrackerConfigurationBuilderCustomizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.spring; 9 | 10 | import org.matomo.java.tracking.TrackerConfiguration; 11 | import org.springframework.lang.NonNull; 12 | 13 | /** 14 | * Allows to customize the {@link TrackerConfiguration.TrackerConfigurationBuilder} with additional properties. 15 | * 16 | *

Implementations of this interface are detected automatically by the {@link MatomoTrackerAutoConfiguration}. 17 | * 18 | * @see MatomoTrackerAutoConfiguration 19 | * @see TrackerConfiguration 20 | * @see TrackerConfiguration.TrackerConfigurationBuilder 21 | */ 22 | @FunctionalInterface 23 | public interface TrackerConfigurationBuilderCustomizer { 24 | 25 | /** 26 | * Customize the {@link TrackerConfiguration.TrackerConfigurationBuilder}. 27 | * 28 | * @param builder the {@link TrackerConfiguration.TrackerConfigurationBuilder} instance (never {@code null}) 29 | * @see TrackerConfiguration#builder() 30 | * @see MatomoTrackerProperties 31 | */ 32 | void customize(@NonNull TrackerConfiguration.TrackerConfigurationBuilder builder); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /test/src/main/java/org/matomo/java/tracking/test/SendExample.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.test; 2 | 3 | import java.net.URI; 4 | import org.matomo.java.tracking.MatomoRequests; 5 | import org.matomo.java.tracking.MatomoTracker; 6 | import org.matomo.java.tracking.TrackerConfiguration; 7 | import org.matomo.java.tracking.parameters.VisitorId; 8 | 9 | /** 10 | * Example for sending a request. 11 | */ 12 | public class SendExample { 13 | 14 | /** 15 | * Example for sending a request. 16 | * 17 | * @param args ignored 18 | */ 19 | public static void main(String[] args) { 20 | 21 | TrackerConfiguration configuration = TrackerConfiguration 22 | .builder() 23 | .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php")) 24 | .defaultSiteId(1) 25 | .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") 26 | .logFailedTracking(true) 27 | .build(); 28 | 29 | try (MatomoTracker tracker = new MatomoTracker(configuration)) { 30 | tracker.sendRequestAsync(MatomoRequests 31 | .event("Training", "Workout completed", "Bench press", 60.0) 32 | .visitorId(VisitorId.fromString("customer@mail.com")) 33 | .build() 34 | ); 35 | } catch (Exception e) { 36 | throw new RuntimeException("Could not close tracker", e); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/ExecutorServiceCloserTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.concurrent.ExecutorService; 6 | import java.util.concurrent.Executors; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class ExecutorServiceCloserTest { 10 | 11 | @Test 12 | void shutsDownExecutorService() { 13 | 14 | ExecutorService executorService = Executors.newFixedThreadPool(2, new DaemonThreadFactory()); 15 | 16 | ExecutorServiceCloser.close(executorService); 17 | 18 | assertThat(executorService.isTerminated()).isTrue(); 19 | assertThat(executorService.isShutdown()).isTrue(); 20 | 21 | } 22 | 23 | @Test 24 | void shutsDownExecutorServiceImmediately() throws Exception { 25 | 26 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 27 | executorService.submit(() -> { 28 | try { 29 | Thread.sleep(10000L); 30 | } catch (InterruptedException e) { 31 | throw new RuntimeException(e); 32 | } 33 | }); 34 | 35 | Thread thread = new Thread(() -> { 36 | ExecutorServiceCloser.close(executorService); 37 | }); 38 | thread.start(); 39 | Thread.sleep(1000L); 40 | thread.interrupt(); 41 | 42 | assertThat(executorService.isShutdown()).isTrue(); 43 | 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/ExecutorServiceCloser.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.TimeUnit; 5 | import lombok.NonNull; 6 | 7 | /** 8 | * Helps to close an executor service. 9 | */ 10 | public class ExecutorServiceCloser { 11 | 12 | /** 13 | * Closes the given executor service. 14 | * 15 | *

This will check whether the executor service is already terminated, and if not, it 16 | * initiates a shutdown and waits a minute. If the minute expires, the executor service 17 | * is shutdown immediately. 18 | * 19 | * @param executorService The executor service to close 20 | */ 21 | public static void close(@NonNull ExecutorService executorService) { 22 | boolean terminated = executorService.isTerminated(); 23 | if (!terminated) { 24 | executorService.shutdown(); 25 | boolean interrupted = false; 26 | while (!terminated) { 27 | try { 28 | terminated = executorService.awaitTermination(1L, TimeUnit.MINUTES); 29 | } catch (InterruptedException e) { 30 | if (!interrupted) { 31 | executorService.shutdownNow(); 32 | interrupted = true; 33 | } 34 | } 35 | } 36 | if (interrupted) { 37 | Thread.currentThread().interrupt(); 38 | } 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release with Maven 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | publish: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-java@v4 10 | with: 11 | java-version: '17' 12 | distribution: 'temurin' 13 | cache: maven 14 | server-id: ossrh 15 | server-username: OSSRH_USERNAME 16 | server-password: OSSRH_PASSWORD 17 | gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} 18 | gpg-passphrase: GPG_PASSPHRASE 19 | - run: | 20 | git config user.email "matomo-java-tracker@daniel-heid.de" 21 | git config user.name "Matomo Java Tracker" 22 | - id: version 23 | run: | 24 | VERSION=$( mvn -B help:evaluate -Dexpression=project.version -q -DforceStdout ) 25 | echo "::set-output name=version::${VERSION%-SNAPSHOT}" 26 | - run: mvn -B release:prepare release:perform 27 | env: 28 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 29 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 30 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 31 | - uses: release-drafter/release-drafter@v6 32 | with: 33 | version: ${{ steps.version.outputs.version }} 34 | publish: true 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/AuthToken.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | import edu.umd.cs.findbugs.annotations.Nullable; 11 | 12 | final class AuthToken { 13 | 14 | private AuthToken() { 15 | // utility 16 | } 17 | 18 | @Nullable 19 | static String determineAuthToken( 20 | @Nullable 21 | String overrideAuthToken, 22 | @Nullable 23 | Iterable requests, 24 | @Nullable 25 | TrackerConfiguration trackerConfiguration 26 | ) { 27 | if (isNotBlank(overrideAuthToken)) { 28 | return overrideAuthToken; 29 | } 30 | if (requests != null) { 31 | for (MatomoRequest request : requests) { 32 | if (request != null && isNotBlank(request.getAuthToken())) { 33 | return request.getAuthToken(); 34 | } 35 | } 36 | } 37 | if (trackerConfiguration != null && isNotBlank(trackerConfiguration.getDefaultAuthToken())) { 38 | return trackerConfiguration.getDefaultAuthToken(); 39 | } 40 | return null; 41 | } 42 | 43 | private static boolean isNotBlank( 44 | @Nullable 45 | String str 46 | ) { 47 | return str != null && !str.isEmpty() && !str.trim().isEmpty(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/parameters/AcceptLanguageTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import static java.util.Collections.singletonList; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.params.ParameterizedTest; 15 | import org.junit.jupiter.params.provider.NullAndEmptySource; 16 | 17 | class AcceptLanguageTest { 18 | 19 | @Test 20 | void fromHeader() { 21 | 22 | AcceptLanguage acceptLanguage = 23 | AcceptLanguage.fromHeader("de,de-DE;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"); 24 | 25 | assertThat(acceptLanguage).hasToString( 26 | "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"); 27 | 28 | } 29 | 30 | @ParameterizedTest 31 | @NullAndEmptySource 32 | void fromHeaderToleratesNull(String header) { 33 | 34 | AcceptLanguage acceptLanguage = AcceptLanguage.fromHeader(header); 35 | 36 | assertThat(acceptLanguage).isNull(); 37 | 38 | } 39 | 40 | @Test 41 | void failsOnNullLanguageRange() { 42 | assertThat(AcceptLanguage 43 | .builder() 44 | .languageRanges(singletonList(null)) 45 | .build()).hasToString(""); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /servlet-javax/src/main/java/org/matomo/java/tracking/servlet/MatomoTrackerFilter.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.servlet; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import java.io.IOException; 5 | import javax.servlet.FilterChain; 6 | import javax.servlet.ServletException; 7 | import javax.servlet.http.HttpFilter; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.matomo.java.tracking.MatomoRequest; 13 | import org.matomo.java.tracking.MatomoTracker; 14 | 15 | /** 16 | * This filter can be used to automatically send a request to the Matomo server for every request 17 | * that is received by the servlet container. 18 | */ 19 | @RequiredArgsConstructor 20 | @Slf4j 21 | public class MatomoTrackerFilter extends HttpFilter { 22 | 23 | private final MatomoTracker tracker; 24 | 25 | @Override 26 | protected void doFilter(@NonNull HttpServletRequest req, @NonNull HttpServletResponse res, 27 | @NonNull FilterChain chain) 28 | throws IOException, ServletException { 29 | MatomoRequest matomoRequest = ServletMatomoRequest 30 | .fromServletRequest(JavaxHttpServletWrapper.fromHttpServletRequest(req)).build(); 31 | log.debug("Sending request {}", matomoRequest); 32 | tracker.sendRequestAsync(matomoRequest); 33 | super.doFilter(req, res, chain); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /servlet-jakarta/src/test/java/org/matomo/java/tracking/servlet/JakartaHttpServletWrapperTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.servlet; 2 | 3 | import static java.util.Collections.singletonMap; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import jakarta.servlet.http.Cookie; 7 | import java.util.List; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class JakartaHttpServletWrapperTest { 11 | 12 | @Test 13 | void wrapsHttpServletRequest() { 14 | 15 | MockHttpServletRequest servlet = new MockHttpServletRequest(); 16 | servlet.setRequestURL(new StringBuffer("http://localhost")); 17 | servlet.setRemoteUser("remote-user"); 18 | servlet.setHeaders(singletonMap("Accept-Language", "en-US,en;q=0.9,de;q=0.8")); 19 | servlet.setCookies(List.of(new Cookie("foo", "bar"))); 20 | 21 | HttpServletRequestWrapper httpServletRequestWrapper = 22 | JakartaHttpServletWrapper.fromHttpServletRequest(servlet); 23 | 24 | assertThat(httpServletRequestWrapper.getRequestURL()).hasToString("http://localhost"); 25 | assertThat(httpServletRequestWrapper.getRemoteUser()).hasToString("remote-user"); 26 | assertThat(httpServletRequestWrapper.getHeaders()) 27 | .containsEntry("accept-language", "en-US,en;q=0.9,de;q=0.8"); 28 | assertThat(httpServletRequestWrapper.getCookies()) 29 | .hasSize(1) 30 | .satisfiesExactly(cookieWrapper -> { 31 | assertThat(cookieWrapper.getName()).isEqualTo("foo"); 32 | assertThat(cookieWrapper.getValue()).isEqualTo("bar"); 33 | }); 34 | } 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, General Electric Company 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the copyright holder nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /servlet-javax/src/test/java/org/matomo/java/tracking/servlet/JavaxHttpServletWrapperTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.servlet; 2 | 3 | import static java.util.Collections.singleton; 4 | import static java.util.Collections.singletonMap; 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | import java.util.List; 8 | import javax.servlet.http.Cookie; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class JavaxHttpServletWrapperTest { 12 | 13 | @Test 14 | void wrapsHttpServletRequest() { 15 | 16 | MockHttpServletRequest servlet = new MockHttpServletRequest(); 17 | servlet.setRequestURL(new StringBuffer("http://localhost")); 18 | servlet.setRemoteUser("remote-user"); 19 | servlet.setHeaders(singletonMap("Accept-Language", "en-US,en;q=0.9,de;q=0.8")); 20 | servlet.setCookies(singleton(new Cookie("foo", "bar"))); 21 | 22 | HttpServletRequestWrapper httpServletRequestWrapper = 23 | JavaxHttpServletWrapper.fromHttpServletRequest(servlet); 24 | 25 | assertThat(httpServletRequestWrapper.getRequestURL()).hasToString("http://localhost"); 26 | assertThat(httpServletRequestWrapper.getRemoteUser()).hasToString("remote-user"); 27 | assertThat(httpServletRequestWrapper.getHeaders()) 28 | .containsEntry("accept-language", "en-US,en;q=0.9,de;q=0.8"); 29 | assertThat(httpServletRequestWrapper.getCookies()) 30 | .hasSize(1) 31 | .satisfiesExactly(cookieWrapper -> { 32 | assertThat(cookieWrapper.getName()).isEqualTo("foo"); 33 | assertThat(cookieWrapper.getValue()).isEqualTo("bar"); 34 | }); 35 | } 36 | 37 | } 38 | 39 | -------------------------------------------------------------------------------- /test/src/main/java/org/matomo/java/tracking/test/ServletMatomoRequestExample.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.test; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import org.matomo.java.tracking.MatomoRequest; 5 | import org.matomo.java.tracking.MatomoRequests; 6 | import org.matomo.java.tracking.MatomoTracker; 7 | import org.matomo.java.tracking.parameters.VisitorId; 8 | import org.matomo.java.tracking.servlet.JakartaHttpServletWrapper; 9 | import org.matomo.java.tracking.servlet.ServletMatomoRequest; 10 | 11 | /** 12 | * This is an example of how to use the ServletMatomoRequest class. 13 | */ 14 | public class ServletMatomoRequestExample { 15 | 16 | private final MatomoTracker tracker; 17 | 18 | public ServletMatomoRequestExample(MatomoTracker tracker) { 19 | this.tracker = tracker; 20 | } 21 | 22 | /** 23 | * Example for sending a request from a servlet request. 24 | * 25 | * @param request the servlet request 26 | */ 27 | public void someControllerMethod(HttpServletRequest request) { 28 | MatomoRequest matomoRequest = ServletMatomoRequest 29 | .addServletRequestHeaders( 30 | MatomoRequests.contentImpression( 31 | "Latest Product Announced", 32 | "Main Blog Text", 33 | "https://www.yourdomain.com/blog/2018/10/01/new-product-launches" 34 | ), 35 | JakartaHttpServletWrapper.fromHttpServletRequest(request) 36 | ).visitorId(VisitorId.fromString("customer@mail.com")) 37 | // ... 38 | .build(); 39 | tracker.sendRequestAsync(matomoRequest); 40 | // ... 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /servlet-jakarta/src/main/java/org/matomo/java/tracking/servlet/MatomoTrackerFilter.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.servlet; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpFilter; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.matomo.java.tracking.MatomoRequest; 12 | import org.matomo.java.tracking.MatomoTracker; 13 | 14 | /** 15 | * This filter can be used to automatically send a request to the Matomo server for every request 16 | * that is received by the servlet container. 17 | */ 18 | @RequiredArgsConstructor 19 | @Slf4j 20 | public class MatomoTrackerFilter extends HttpFilter { 21 | 22 | private final MatomoTracker tracker; 23 | 24 | @Override 25 | protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) 26 | throws IOException, ServletException { 27 | MatomoRequest matomoRequest = ServletMatomoRequest 28 | .fromServletRequest(JakartaHttpServletWrapper.fromHttpServletRequest(req)).build(); 29 | log.debug("Sending request {}", matomoRequest); 30 | tracker.sendRequestAsync(matomoRequest); 31 | super.doFilter(req, res, chain); 32 | } 33 | 34 | @Override 35 | public void destroy() { 36 | if (tracker != null) { 37 | try { 38 | tracker.close(); 39 | } catch (Exception e) { 40 | throw new RuntimeException("Could not close tracker", e); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/parameters/DeviceResolutionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | import org.junit.jupiter.api.Test; 14 | 15 | class DeviceResolutionTest { 16 | 17 | @Test 18 | void formatsDeviceResolution() { 19 | 20 | DeviceResolution deviceResolution = DeviceResolution.builder().width(1280).height(1080).build(); 21 | 22 | assertThat(deviceResolution).hasToString("1280x1080"); 23 | 24 | } 25 | 26 | @Test 27 | void returnsNullOnNull() { 28 | 29 | DeviceResolution deviceResolution = DeviceResolution.fromString(null); 30 | 31 | assertThat(deviceResolution).isNull(); 32 | 33 | } 34 | 35 | @Test 36 | void failsOnWrongDimensionSize() { 37 | assertThatThrownBy(() -> DeviceResolution.fromString("1920x1080x720")) 38 | .isInstanceOf(IllegalArgumentException.class) 39 | .hasMessage("Wrong dimension size"); 40 | } 41 | 42 | @Test 43 | void failsIfDeviceResolutionIsTooShort() { 44 | assertThatThrownBy(() -> DeviceResolution.fromString("1")) 45 | .isInstanceOf(IllegalArgumentException.class) 46 | .hasMessage("Wrong device resolution size"); 47 | } 48 | 49 | @Test 50 | void returnsNullIfDeviceResolutionIsEmpty() { 51 | assertThat(DeviceResolution.fromString("")).isNull(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/RandomValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import java.security.SecureRandom; 11 | import java.util.Random; 12 | 13 | /** 14 | * A random value to avoid the tracking request being cached by the browser or a proxy. 15 | */ 16 | public class RandomValue { 17 | 18 | private static final Random RANDOM = new SecureRandom(); 19 | 20 | private final byte[] representation = new byte[10]; 21 | 22 | private String override; 23 | 24 | /** 25 | * Static factory to generate a random value. 26 | * 27 | * @return A randomly generated value 28 | */ 29 | public static RandomValue random() { 30 | RandomValue randomValue = new RandomValue(); 31 | RANDOM.nextBytes(randomValue.representation); 32 | return randomValue; 33 | } 34 | 35 | /** 36 | * Static factory to generate a random value from a given string. The string will be used as is and not hashed. 37 | * 38 | * @param override The string to use as random value 39 | * @return A random value from the given string 40 | */ 41 | public static RandomValue fromString(String override) { 42 | RandomValue randomValue = new RandomValue(); 43 | randomValue.override = override; 44 | return randomValue; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | if (override != null) { 50 | return override; 51 | } 52 | return Hex.fromBytes(representation); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/java/org/piwik/java/tracking/PiwikDate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.piwik.java.tracking; 9 | 10 | import java.time.Instant; 11 | import java.time.ZonedDateTime; 12 | import java.util.TimeZone; 13 | import org.matomo.java.tracking.MatomoDate; 14 | 15 | /** 16 | * A date object that can be used to send dates to Matomo. This class is deprecated and will be removed in a future. 17 | * 18 | * @author brettcsorba 19 | * @deprecated Please use {@link Instant} 20 | */ 21 | @Deprecated 22 | public class PiwikDate extends MatomoDate { 23 | 24 | /** 25 | * Creates a new date object with the current time. 26 | * 27 | * @deprecated Use {@link Instant} instead. 28 | */ 29 | @Deprecated 30 | public PiwikDate() { 31 | } 32 | 33 | /** 34 | * Creates a new date object with the specified time. The time is specified in milliseconds since the epoch. 35 | * 36 | * @param epochMilli The time in milliseconds since the epoch 37 | * @deprecated Use {@link Instant} instead. 38 | */ 39 | @Deprecated 40 | public PiwikDate(long epochMilli) { 41 | super(epochMilli); 42 | } 43 | 44 | /** 45 | * Sets the time zone for this date object. This is used to convert the date to UTC before sending it to Matomo. 46 | * 47 | * @param zone the time zone to use 48 | * @deprecated Use {@link ZonedDateTime#toInstant()} instead. 49 | */ 50 | @Deprecated 51 | public void setTimeZone(TimeZone zone) { 52 | setTimeZone(zone.toZoneId()); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /test/src/main/java/org/matomo/java/tracking/test/BulkExample.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.test; 2 | 3 | import java.net.URI; 4 | import org.matomo.java.tracking.MatomoRequests; 5 | import org.matomo.java.tracking.MatomoTracker; 6 | import org.matomo.java.tracking.TrackerConfiguration; 7 | import org.matomo.java.tracking.parameters.VisitorId; 8 | 9 | /** 10 | * Example for sending multiple requests in one bulk request. 11 | */ 12 | public class BulkExample { 13 | 14 | /** 15 | * Example for sending multiple requests in one bulk request. 16 | * 17 | * @param args ignored 18 | */ 19 | public static void main(String[] args) { 20 | 21 | TrackerConfiguration configuration = TrackerConfiguration 22 | .builder() 23 | .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php")) 24 | .defaultSiteId(1) 25 | .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") 26 | .logFailedTracking(true) 27 | .build(); 28 | 29 | try (MatomoTracker tracker = new MatomoTracker(configuration)) { 30 | VisitorId visitorId = VisitorId.fromString("customer@mail.com"); 31 | tracker.sendBulkRequestAsync( 32 | MatomoRequests.siteSearch("Running shoes", "Running", 120L) 33 | .visitorId(visitorId).build(), 34 | MatomoRequests.pageView("VelocityStride ProX Running Shoes") 35 | .visitorId(visitorId).build(), 36 | MatomoRequests.ecommerceOrder("QXZ-789LMP", 100.0, 124.0, 19.0, 10.0, 5.0) 37 | .visitorId(visitorId) 38 | .build() 39 | ); 40 | } catch (Exception e) { 41 | throw new RuntimeException("Could not close tracker", e); 42 | } 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /test/src/main/java/org/matomo/java/tracking/test/MatomoServletTester.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.test; 2 | 3 | import java.net.URI; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.eclipse.jetty.ee10.servlet.DefaultServlet; 6 | import org.eclipse.jetty.ee10.servlet.FilterHolder; 7 | import org.eclipse.jetty.ee10.servlet.ServletContextHandler; 8 | import org.eclipse.jetty.ee10.servlet.ServletHolder; 9 | import org.eclipse.jetty.server.Server; 10 | import org.matomo.java.tracking.MatomoTracker; 11 | import org.matomo.java.tracking.TrackerConfiguration; 12 | import org.matomo.java.tracking.servlet.MatomoTrackerFilter; 13 | 14 | @Slf4j 15 | class MatomoServletTester { 16 | public static void main(String[] args) throws Exception { 17 | 18 | ServletHolder servletHolder = new ServletHolder("default", new DefaultServlet()); 19 | servletHolder.setInitParameter( 20 | "resourceBase", 21 | MatomoServletTester.class.getClassLoader().getResource("web").toExternalForm() 22 | ); 23 | 24 | ServletContextHandler context = new ServletContextHandler(); 25 | context.setContextPath("/"); 26 | context.addServlet(servletHolder, "/"); 27 | context.addFilter(new FilterHolder(new MatomoTrackerFilter(new MatomoTracker( 28 | TrackerConfiguration 29 | .builder() 30 | .apiEndpoint(URI.create("http://localhost:8080/matomo.php")) 31 | .defaultSiteId(1) 32 | .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") 33 | .logFailedTracking(true) 34 | .build()))), "/*", null); 35 | 36 | Server server = new Server(8090); 37 | server.setHandler(context); 38 | server.start(); 39 | server.join(); 40 | 41 | } 42 | } -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/UniqueId.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import java.security.SecureRandom; 11 | import java.util.Random; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.IntStream; 14 | import lombok.AccessLevel; 15 | import lombok.RequiredArgsConstructor; 16 | 17 | /** 18 | * A six character unique ID consisting of the characters [0-9a-Z]. 19 | */ 20 | @RequiredArgsConstructor(access = AccessLevel.PRIVATE) 21 | public final class UniqueId { 22 | 23 | private static final String CHARS = 24 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 25 | 26 | private static final Random RANDOM = new SecureRandom(); 27 | 28 | private final long value; 29 | 30 | /** 31 | * Static factory to generate a random unique id. 32 | * 33 | * @return A randomly generated unique id 34 | */ 35 | public static UniqueId random() { 36 | return fromValue(RANDOM.nextLong()); 37 | } 38 | 39 | /** 40 | * Creates a unique id from a number. 41 | * 42 | * @param value A number to create this unique id from 43 | * @return The unique id for the given value 44 | */ 45 | public static UniqueId fromValue(long value) { 46 | return new UniqueId(value); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return IntStream 52 | .range(0, 6) 53 | .map(i -> (int) (value >> i * 8)) 54 | .mapToObj(codePoint -> String.valueOf(CHARS.charAt(Math.abs(codePoint % CHARS.length())))) 55 | .collect(Collectors.joining()); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.piwik.java.tracking 6 | matomo-java-tracker-parent 7 | 3.4.1-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | matomo-java-tracker-core 12 | jar 13 | 14 | Matomo Java Tracker Core 15 | 16 | 17 | 18 | com.github.spotbugs 19 | spotbugs-annotations 20 | 21 | 22 | org.slf4j 23 | slf4j-api 24 | 25 | 26 | org.projectlombok 27 | lombok 28 | provided 29 | 30 | 31 | org.junit.jupiter 32 | junit-jupiter 33 | test 34 | 35 | 36 | org.assertj 37 | assertj-core 38 | test 39 | 40 | 41 | org.slf4j 42 | slf4j-simple 43 | test 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /test/src/main/java/org/matomo/java/tracking/test/ConsumerExample.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.test; 2 | 3 | import java.net.URI; 4 | import org.matomo.java.tracking.MatomoRequest; 5 | import org.matomo.java.tracking.MatomoRequests; 6 | import org.matomo.java.tracking.MatomoTracker; 7 | import org.matomo.java.tracking.TrackerConfiguration; 8 | import org.matomo.java.tracking.parameters.VisitorId; 9 | 10 | /** 11 | * Example for sending a request and performing an action when the request was sent successfully. 12 | */ 13 | public class ConsumerExample { 14 | 15 | /** 16 | * Example for sending a request and performing an action when the request was sent successfully. 17 | * 18 | * @param args ignored 19 | */ 20 | public static void main(String[] args) { 21 | 22 | TrackerConfiguration configuration = TrackerConfiguration 23 | .builder() 24 | .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php")) 25 | .defaultSiteId(1) 26 | .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") 27 | .logFailedTracking(true) 28 | .build(); 29 | 30 | try (MatomoTracker tracker = new MatomoTracker(configuration)) { 31 | MatomoRequest request = MatomoRequests 32 | .event("Training", "Workout completed", "Bench press", 60.0) 33 | .visitorId(VisitorId.fromString("customer@mail.com")) 34 | .build(); 35 | 36 | tracker.sendRequestAsync(request) 37 | .thenAccept(req -> System.out.printf("Sent request %s%n", req)) 38 | .exceptionally(throwable -> { 39 | System.err.printf("Failed to send request: %s%n", throwable.getMessage()); 40 | return null; 41 | }); 42 | } catch (Exception e) { 43 | throw new RuntimeException("Could not close tracker", e); 44 | } 45 | 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | categories: 5 | - title: 💥 Breaking changes 6 | labels: 7 | - breaking 8 | - title: 🚨 Removed 9 | labels: 10 | - removed 11 | - title: 🎉 Major features and improvements 12 | labels: 13 | - major-enhancement 14 | - major-rfe 15 | - title: 🐛 Major bug fixes 16 | labels: 17 | - major-bug 18 | - title: ⚠️ Deprecated 19 | labels: 20 | - deprecated 21 | - title: 🚀 New features and improvements 22 | labels: 23 | - enhancement 24 | - feature 25 | - rfe 26 | - title: 🐛 Bug fixes 27 | labels: 28 | - bug 29 | - fix 30 | - bugfix 31 | - regression 32 | - regression-fix 33 | - title: 🌐 Localization and translation 34 | labels: 35 | - localization 36 | - title: 👷 Changes for developers 37 | labels: 38 | - developer 39 | - title: 📝 Documentation updates 40 | labels: 41 | - documentation 42 | - title: 👻 Maintenance 43 | labels: 44 | - chore 45 | - internal 46 | - maintenance 47 | - title: 🚦 Tests 48 | labels: 49 | - test 50 | - tests 51 | - title: ✍ Other changes 52 | - title: 📦 Dependency updates 53 | labels: 54 | - dependencies 55 | collapse-after: 15 56 | 57 | exclude-labels: 58 | - reverted 59 | - no-changelog 60 | - skip-changelog 61 | - invalid 62 | 63 | template: | 64 | ## Changes 65 | 66 | $CHANGES 67 | 68 | autolabeler: 69 | - label: 'documentation' 70 | files: 71 | - '*.md' 72 | branch: 73 | - '/docs{0,1}\/.+/' 74 | - label: 'bug' 75 | branch: 76 | - '/fix\/.+/' 77 | title: 78 | - '/fix/i' 79 | - label: 'enhancement' 80 | branch: 81 | - '/feature\/.+/' 82 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/ProxyAuthenticatorTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.net.Authenticator; 6 | import java.net.Authenticator.RequestorType; 7 | import java.net.InetAddress; 8 | import java.net.PasswordAuthentication; 9 | import java.net.URL; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ProxyAuthenticatorTest { 13 | 14 | private PasswordAuthentication passwordAuthentication; 15 | 16 | @Test 17 | void createsPasswordAuthentication() throws Exception { 18 | 19 | ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator("user", "password"); 20 | Authenticator.setDefault(proxyAuthenticator); 21 | givenPasswordAuthentication(RequestorType.PROXY); 22 | 23 | assertThat(passwordAuthentication.getUserName()).isEqualTo("user"); 24 | assertThat(passwordAuthentication.getPassword()).contains( 25 | 'p', 26 | 'a', 27 | 's', 28 | 's', 29 | 'w', 30 | 'o', 31 | 'r', 32 | 'd' 33 | ); 34 | 35 | } 36 | 37 | private void givenPasswordAuthentication(RequestorType proxy) throws Exception { 38 | passwordAuthentication = Authenticator.requestPasswordAuthentication("host", 39 | InetAddress.getLocalHost(), 40 | 8080, 41 | "http", 42 | "prompt", 43 | "https", 44 | new URL("https://www.daniel-heid.de"), 45 | proxy 46 | ); 47 | } 48 | 49 | @Test 50 | void returnsNullIfNoPasswordAuthentication() throws Exception { 51 | 52 | ProxyAuthenticator proxyAuthenticator = new ProxyAuthenticator("user", "password"); 53 | Authenticator.setDefault(proxyAuthenticator); 54 | givenPasswordAuthentication(RequestorType.SERVER); 55 | 56 | assertThat(passwordAuthentication).isNull(); 57 | 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/DeviceResolution.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import edu.umd.cs.findbugs.annotations.Nullable; 11 | import lombok.Builder; 12 | import lombok.RequiredArgsConstructor; 13 | 14 | /** 15 | * The resolution (width and height) of the user's output device (monitor / phone). 16 | */ 17 | @Builder 18 | @RequiredArgsConstructor 19 | public class DeviceResolution { 20 | 21 | private final int width; 22 | 23 | private final int height; 24 | 25 | /** 26 | * Creates a device resolution from a string representation. 27 | * 28 | *

The string must be in the format "widthxheight", e.g. "1920x1080". 29 | * 30 | * @param deviceResolution The string representation of the device resolution, e.g. "1920x1080" 31 | * @return The device resolution representation 32 | */ 33 | @Nullable 34 | public static DeviceResolution fromString( 35 | @Nullable 36 | String deviceResolution 37 | ) { 38 | if (deviceResolution == null || deviceResolution.trim().isEmpty()) { 39 | return null; 40 | } 41 | if (deviceResolution.length() < 3) { 42 | throw new IllegalArgumentException("Wrong device resolution size"); 43 | } 44 | String[] dimensions = deviceResolution.split("x"); 45 | if (dimensions.length != 2) { 46 | throw new IllegalArgumentException("Wrong dimension size"); 47 | } 48 | return builder() 49 | .width(Integer.parseInt(dimensions[0])) 50 | .height(Integer.parseInt(dimensions[1])) 51 | .build(); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return String.format("%dx%d", width, height); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/RequestValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | 11 | import edu.umd.cs.findbugs.annotations.Nullable; 12 | import java.time.Instant; 13 | import java.time.temporal.ChronoUnit; 14 | import lombok.NonNull; 15 | 16 | final class RequestValidator { 17 | 18 | private RequestValidator() { 19 | // utility 20 | } 21 | 22 | static void validate( 23 | @NonNull 24 | MatomoRequest request, 25 | @Nullable 26 | CharSequence authToken 27 | ) { 28 | 29 | if (request.getSearchResultsCount() != null && request.getSearchQuery() == null) { 30 | throw new MatomoException("Search query must be set if search results count is set"); 31 | } 32 | if (authToken == null) { 33 | if (request.getVisitorLongitude() != null || request.getVisitorLatitude() != null 34 | || request.getVisitorRegion() != null || request.getVisitorCity() != null 35 | || request.getVisitorCountry() != null || request.getVisitorIp() != null) { 36 | throw new MatomoException( 37 | "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); 38 | } 39 | if (request.getRequestTimestamp() != null && request 40 | .getRequestTimestamp() 41 | .isBefore(Instant.now().minus(4, ChronoUnit.HOURS))) { 42 | throw new MatomoException( 43 | "Auth token must be present if request timestamp is more than four hours ago"); 44 | } 45 | } else { 46 | if (authToken.length() != 32) { 47 | throw new IllegalArgumentException("Auth token must be exactly 32 characters long"); 48 | } 49 | } 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /servlet-javax/src/test/java/org/matomo/java/tracking/TestSender.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import edu.umd.cs.findbugs.annotations.Nullable; 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.concurrent.CompletableFuture; 8 | import lombok.Getter; 9 | import lombok.RequiredArgsConstructor; 10 | 11 | /** 12 | * A {@link Sender} implementation that does not send anything but stores the requests. 13 | * 14 | *

This class is intended for testing purposes only. It does not send anything to the Matomo server. Instead, it 15 | * stores the requests and queries in collections that can be accessed via {@link #getRequests()}. 16 | */ 17 | @RequiredArgsConstructor 18 | @Getter 19 | class TestSender implements Sender { 20 | 21 | private final Collection requests = new ArrayList<>(); 22 | 23 | private final TrackerConfiguration trackerConfiguration; 24 | 25 | private final QueryCreator queryCreator; 26 | 27 | @NonNull 28 | @Override 29 | public CompletableFuture sendSingleAsync(@NonNull MatomoRequest request) { 30 | requests.add(request); 31 | return CompletableFuture.completedFuture(request); 32 | } 33 | 34 | @Override 35 | public void sendSingle(@NonNull MatomoRequest request) { 36 | throw new UnsupportedOperationException(); 37 | } 38 | 39 | @Override 40 | public void sendBulk( 41 | @NonNull Iterable requests, @Nullable String overrideAuthToken 42 | ) { 43 | throw new UnsupportedOperationException(); 44 | } 45 | 46 | @NonNull 47 | @Override 48 | public CompletableFuture sendBulkAsync( 49 | @NonNull Collection requests, @Nullable String overrideAuthToken 50 | ) { 51 | throw new UnsupportedOperationException(); 52 | } 53 | 54 | @Override 55 | public void close() { 56 | // do nothing 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /servlet-jakarta/src/test/java/org/matomo/java/tracking/TestSender.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | import edu.umd.cs.findbugs.annotations.Nullable; 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.concurrent.CompletableFuture; 8 | import lombok.Getter; 9 | import lombok.RequiredArgsConstructor; 10 | 11 | /** 12 | * A {@link Sender} implementation that does not send anything but stores the requests. 13 | * 14 | *

This class is intended for testing purposes only. It does not send anything to the Matomo server. Instead, it 15 | * stores the requests and queries in collections that can be accessed via {@link #getRequests()}. 16 | */ 17 | @RequiredArgsConstructor 18 | @Getter 19 | class TestSender implements Sender { 20 | 21 | private final Collection requests = new ArrayList<>(); 22 | 23 | private final TrackerConfiguration trackerConfiguration; 24 | 25 | private final QueryCreator queryCreator; 26 | 27 | @NonNull 28 | @Override 29 | public CompletableFuture sendSingleAsync(@NonNull MatomoRequest request) { 30 | requests.add(request); 31 | return CompletableFuture.completedFuture(request); 32 | } 33 | 34 | @Override 35 | public void sendSingle(@NonNull MatomoRequest request) { 36 | throw new UnsupportedOperationException(); 37 | } 38 | 39 | @Override 40 | public void sendBulk( 41 | @NonNull Iterable requests, @Nullable String overrideAuthToken 42 | ) { 43 | throw new UnsupportedOperationException(); 44 | } 45 | 46 | @NonNull 47 | @Override 48 | public CompletableFuture sendBulkAsync( 49 | @NonNull Collection requests, @Nullable String overrideAuthToken 50 | ) { 51 | throw new UnsupportedOperationException(); 52 | } 53 | 54 | @Override 55 | public void close() { 56 | // Do nothing 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/CustomVariable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import lombok.AllArgsConstructor; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.Getter; 13 | import lombok.NonNull; 14 | import lombok.Setter; 15 | import lombok.ToString; 16 | 17 | /** 18 | * A key-value pair that represents custom information. See 19 | * How do I use Custom Variables? 20 | * 21 | *

If you are not already using Custom Variables to measure your custom data, Matomo recommends to use the 22 | * Custom Dimensions feature instead. 23 | * There are many advantages of Custom Dimensions over Custom 24 | * variables. Custom variables will be deprecated in the future. 25 | * 26 | * @deprecated Should not be used according to the Matomo FAQ: How do I use Custom Variables? 27 | */ 28 | @Getter 29 | @Setter 30 | @AllArgsConstructor 31 | @ToString 32 | @EqualsAndHashCode(exclude = "index") 33 | @Deprecated 34 | public class CustomVariable { 35 | 36 | private int index; 37 | 38 | @NonNull 39 | private String key; 40 | 41 | @NonNull 42 | private String value; 43 | 44 | /** 45 | * Instantiates a new custom variable. 46 | * 47 | * @param key the key of the custom variable (required) 48 | * @param value the value of the custom variable (required) 49 | */ 50 | public CustomVariable(@NonNull String key, @NonNull String value) { 51 | this.key = key; 52 | this.value = value; 53 | } 54 | } 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/MatomoRequestBuilder.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.Nullable; 4 | import java.util.Map; 5 | import org.matomo.java.tracking.parameters.AcceptLanguage; 6 | 7 | /** 8 | * The former MatomoRequestBuilder class has been moved to MatomoRequest.MatomoRequestBuilder. 9 | * This class is only here for backwards compatibility. 10 | * 11 | * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder} instead. 12 | */ 13 | @Deprecated 14 | public class MatomoRequestBuilder extends MatomoRequest.MatomoRequestBuilder { 15 | 16 | 17 | /** 18 | * Sets the tracking parameter for the accept languages of a user. Only here for backwards 19 | * compatibility. 20 | * 21 | * @param headerAcceptLanguage The accept language header of a user. Must be in the format 22 | * specified in RFC 2616. 23 | * @return This builder 24 | * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#headerAcceptLanguage(AcceptLanguage)} 25 | * in combination with {@link AcceptLanguage#fromHeader(String)} instead. 26 | */ 27 | @Deprecated 28 | public MatomoRequestBuilder headerAcceptLanguage(@Nullable String headerAcceptLanguage) { 29 | headerAcceptLanguage(AcceptLanguage.fromHeader(headerAcceptLanguage)); 30 | return this; 31 | } 32 | 33 | /** 34 | * Sets the custom tracking parameters to the given parameters. 35 | * 36 | *

This converts the given map to a map of collections. Only included for backwards 37 | * compatibility. 38 | * 39 | * @param parameters The custom tracking parameters to set 40 | * @return This builder 41 | * @deprecated Use {@link MatomoRequest.MatomoRequestBuilder#additionalParameters(Map)}} 42 | */ 43 | @Deprecated 44 | public MatomoRequestBuilder customTrackingParameters(@Nullable Map parameters) { 45 | additionalParameters(parameters); 46 | return this; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /servlet-javax/src/main/java/org/matomo/java/tracking/servlet/JavaxHttpServletWrapper.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.servlet; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.List; 5 | import java.util.Locale; 6 | import java.util.Map; 7 | import java.util.stream.Collectors; 8 | import java.util.stream.Stream; 9 | import javax.servlet.http.HttpServletRequest; 10 | import lombok.NonNull; 11 | 12 | /** 13 | * Converts a javax {@link HttpServletRequest} to a {@link HttpServletRequestWrapper}. 14 | */ 15 | public final class JavaxHttpServletWrapper { 16 | 17 | private JavaxHttpServletWrapper() { 18 | // utility 19 | } 20 | 21 | /** 22 | * Takes a javax {@link HttpServletRequest} and converts it to a 23 | * {@link HttpServletRequestWrapper}. 24 | * 25 | * @param request the request to convert to a wrapper object (must not be {@code null}). 26 | * @return the wrapper object (never {@code null}). 27 | */ 28 | @edu.umd.cs.findbugs.annotations.NonNull 29 | public static HttpServletRequestWrapper fromHttpServletRequest(@NonNull HttpServletRequest request) { 30 | Map headers = new LinkedHashMap<>(); 31 | request.getHeaderNames() 32 | .asIterator() 33 | .forEachRemaining(name -> headers.put(name.toLowerCase(Locale.ROOT), request.getHeader(name))); 34 | List cookies = null; 35 | if (request.getCookies() != null) { 36 | cookies = Stream.of(request.getCookies()) 37 | .map(cookie -> new CookieWrapper(cookie.getName(), cookie.getValue())) 38 | .collect(Collectors.toList()); 39 | } 40 | return HttpServletRequestWrapper 41 | .builder() 42 | .requestURL(request.getRequestURL()) 43 | .remoteAddr(request.getRemoteAddr()) 44 | .remoteUser(request.getRemoteUser()) 45 | .headers(headers) 46 | .cookies(cookies == null ? null : cookies.toArray(new CookieWrapper[0])) 47 | .build(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /servlet-jakarta/src/main/java/org/matomo/java/tracking/servlet/JakartaHttpServletWrapper.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.servlet; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import java.util.LinkedHashMap; 5 | import java.util.List; 6 | import java.util.Locale; 7 | import java.util.Map; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.Stream; 10 | import lombok.NonNull; 11 | 12 | /** 13 | * Converts a Jakarta {@link HttpServletRequest} to a {@link HttpServletRequestWrapper}. 14 | */ 15 | public final class JakartaHttpServletWrapper { 16 | 17 | private JakartaHttpServletWrapper() { 18 | // utility 19 | } 20 | 21 | /** 22 | * Takes a Jakarta {@link HttpServletRequest} and converts it to a 23 | * {@link HttpServletRequestWrapper}. 24 | * 25 | * @param request the request to convert to a wrapper object (must not be {@code null}). 26 | * @return the wrapper object (never {@code null}). 27 | */ 28 | @edu.umd.cs.findbugs.annotations.NonNull 29 | public static HttpServletRequestWrapper fromHttpServletRequest(@NonNull HttpServletRequest request) { 30 | Map headers = new LinkedHashMap<>(); 31 | request.getHeaderNames() 32 | .asIterator() 33 | .forEachRemaining(name -> headers.put(name.toLowerCase(Locale.ROOT), request.getHeader(name))); 34 | List cookies = null; 35 | if (request.getCookies() != null) { 36 | cookies = Stream.of(request.getCookies()) 37 | .map(cookie -> new CookieWrapper(cookie.getName(), cookie.getValue())) 38 | .collect(Collectors.toList()); 39 | } 40 | return HttpServletRequestWrapper 41 | .builder() 42 | .requestURL(request.getRequestURL()) 43 | .remoteAddr(request.getRemoteAddr()) 44 | .remoteUser(request.getRemoteUser()) 45 | .headers(headers) 46 | .cookies(cookies == null ? null : cookies.toArray(new CookieWrapper[0])) 47 | .build(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/EcommerceItemTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class EcommerceItemTest { 8 | 9 | private EcommerceItem ecommerceItem = new EcommerceItem(null, null, null, null, null); 10 | 11 | /** 12 | * Test of constructor, of class EcommerceItem. 13 | */ 14 | @Test 15 | void testConstructor() { 16 | EcommerceItem ecommerceItem = new EcommerceItem("sku", "name", "category", 2.0, 2); 17 | assertThat(ecommerceItem.getSku()).isEqualTo("sku"); 18 | assertThat(ecommerceItem.getName()).isEqualTo("name"); 19 | assertThat(ecommerceItem.getCategory()).isEqualTo("category"); 20 | assertThat(ecommerceItem.getPrice()).isEqualTo(2.0); 21 | assertThat(ecommerceItem.getQuantity()).isEqualTo(2); 22 | } 23 | 24 | /** 25 | * Test of getSku method, of class EcommerceItem. 26 | */ 27 | @Test 28 | void testGetSku() { 29 | ecommerceItem.setSku("sku"); 30 | assertThat(ecommerceItem.getSku()).isEqualTo("sku"); 31 | } 32 | 33 | /** 34 | * Test of getName method, of class EcommerceItem. 35 | */ 36 | @Test 37 | void testGetName() { 38 | ecommerceItem.setName("name"); 39 | assertThat(ecommerceItem.getName()).isEqualTo("name"); 40 | } 41 | 42 | /** 43 | * Test of getCategory method, of class EcommerceItem. 44 | */ 45 | @Test 46 | void testGetCategory() { 47 | ecommerceItem.setCategory("category"); 48 | assertThat(ecommerceItem.getCategory()).isEqualTo("category"); 49 | } 50 | 51 | /** 52 | * Test of getPrice method, of class EcommerceItem. 53 | */ 54 | @Test 55 | void testGetPrice() { 56 | ecommerceItem.setPrice(2.0); 57 | assertThat(ecommerceItem.getPrice()).isEqualTo(2.0); 58 | } 59 | 60 | /** 61 | * Test of getQuantity method, of class EcommerceItem. 62 | */ 63 | @Test 64 | void testGetQuantity() { 65 | ecommerceItem.setQuantity(2); 66 | assertThat(ecommerceItem.getQuantity()).isEqualTo(2); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/TrackingParameterMethod.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | import java.lang.reflect.Method; 11 | import java.util.regex.Pattern; 12 | import lombok.Builder; 13 | import lombok.NonNull; 14 | import lombok.Value; 15 | 16 | @Builder 17 | @Value 18 | class TrackingParameterMethod { 19 | 20 | String parameterName; 21 | 22 | Method method; 23 | 24 | Pattern pattern; 25 | 26 | double min; 27 | 28 | double max; 29 | 30 | int maxLength; 31 | 32 | void validateParameterValue(@NonNull Object parameterValue) { 33 | if (pattern != null && parameterValue instanceof CharSequence && !pattern 34 | .matcher((CharSequence) parameterValue) 35 | .matches()) { 36 | throw new MatomoException(String.format("Invalid value for %s. Must match regex %s", 37 | parameterName, 38 | pattern 39 | )); 40 | } 41 | if (maxLength != 0 && parameterValue.toString().length() > maxLength) { 42 | throw new MatomoException(String.format("Invalid value for %s. Must be less or equal than %d characters", 43 | parameterName, 44 | maxLength 45 | )); 46 | } 47 | if (parameterValue instanceof Number) { 48 | Number number = (Number) parameterValue; 49 | if (number.doubleValue() < min) { 50 | throw new MatomoException(String.format( 51 | "Invalid value for %s. Must be greater or equal than %s", 52 | parameterName, 53 | min % 1 == 0 ? Long.toString((long) min) : min 54 | )); 55 | 56 | } 57 | if (number.doubleValue() > max) { 58 | throw new MatomoException(String.format( 59 | "Invalid value for %s. Must be less or equal than %s", 60 | parameterName, 61 | max % 1 == 0 ? Long.toString((long) max) : max 62 | )); 63 | } 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/servlet/HttpServletRequestWrapper.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.servlet; 2 | 3 | import edu.umd.cs.findbugs.annotations.Nullable; 4 | import java.util.Collections; 5 | import java.util.Enumeration; 6 | import java.util.Locale; 7 | import java.util.Map; 8 | import lombok.Builder; 9 | import lombok.NonNull; 10 | import lombok.Value; 11 | 12 | /** 13 | * Wraps a HttpServletRequest to be compatible with both the Jakarta and the Java EE API. 14 | */ 15 | @Builder 16 | @Value 17 | public class HttpServletRequestWrapper { 18 | 19 | @Nullable 20 | StringBuffer requestURL; 21 | 22 | @Nullable 23 | String remoteAddr; 24 | 25 | @Nullable 26 | String remoteUser; 27 | 28 | @Nullable 29 | Map headers; 30 | 31 | @Nullable 32 | CookieWrapper[] cookies; 33 | 34 | /** 35 | * Returns an enumeration of all the header names this request contains. If the request has no 36 | * headers, this method returns an empty enumeration. 37 | * 38 | * @return an enumeration of all the header names sent with this request 39 | */ 40 | public Enumeration getHeaderNames() { 41 | return headers == null ? Collections.emptyEnumeration() : 42 | Collections.enumeration(headers.keySet()); 43 | } 44 | 45 | /** 46 | * Returns the value of the specified request header as a String. If the request did not include a 47 | * header of the specified name, this method returns null. If there are multiple headers with the 48 | * same name, this method returns the last header in the request. The header name is case 49 | * insensitive. You can use this method with any request header. 50 | * 51 | * @param name a String specifying the header name (case insensitive) - must not be {@code null}. 52 | * @return a String containing the value of the requested header, or null if the request does not 53 | * have a header of that name 54 | */ 55 | @Nullable 56 | public String getHeader(@NonNull String name) { 57 | return headers == null ? null : headers.get(name.toLowerCase(Locale.ROOT)); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /test/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.piwik.java.tracking 6 | matomo-java-tracker-parent 7 | 3.4.1-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | matomo-java-tracker-test 12 | 13 | Matomo Java Tracker Test 14 | Test application for Matomo Java Tracker 15 | 16 | 17 | 11 18 | 11 19 | 20 | 21 | 22 | 23 | org.piwik.java.tracking 24 | matomo-java-tracker-java11 25 | ${project.version} 26 | 27 | 28 | org.piwik.java.tracking 29 | matomo-java-tracker-servlet-jakarta 30 | ${project.version} 31 | 32 | 33 | com.github.javafaker 34 | javafaker 35 | 1.0.2 36 | 37 | 38 | org.projectlombok 39 | lombok 40 | provided 41 | 42 | 43 | org.slf4j 44 | slf4j-simple 45 | 46 | 47 | org.eclipse.jetty.ee10 48 | jetty-ee10-servlet 49 | 12.0.16 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/MatomoDate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking; 9 | 10 | import java.time.Instant; 11 | import java.time.ZoneId; 12 | import java.time.ZoneOffset; 13 | import java.time.ZonedDateTime; 14 | import lombok.Getter; 15 | 16 | /** 17 | * A datetime object that will return the datetime in the format {@code yyyy-MM-dd hh:mm:ss}. 18 | * 19 | * @author brettcsorba 20 | * @deprecated Please use {@link Instant} 21 | */ 22 | @Deprecated 23 | @Getter 24 | public class MatomoDate { 25 | 26 | private ZonedDateTime zonedDateTime; 27 | 28 | /** 29 | * Allocates a Date object and initializes it so that it represents the time 30 | * at which it was allocated, measured to the nearest millisecond. 31 | */ 32 | @Deprecated 33 | public MatomoDate() { 34 | zonedDateTime = ZonedDateTime.now(ZoneOffset.UTC); 35 | } 36 | 37 | /** 38 | * Allocates a Date object and initializes it to represent the specified number 39 | * of milliseconds since the standard base time known as "the epoch", namely 40 | * January 1, 1970, 00:00:00 GMT. 41 | * 42 | * @param epochMilli the milliseconds since January 1, 1970, 00:00:00 GMT. 43 | */ 44 | @Deprecated 45 | public MatomoDate(long epochMilli) { 46 | zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneOffset.UTC); 47 | } 48 | 49 | /** 50 | * Sets the time zone of the String that will be returned by {@link #toString()}. 51 | * Defaults to UTC. 52 | * 53 | * @param zone the TimeZone to set 54 | */ 55 | public void setTimeZone(ZoneId zone) { 56 | zonedDateTime = zonedDateTime.withZoneSameInstant(zone); 57 | } 58 | 59 | /** 60 | * Converts this datetime to the number of milliseconds from the epoch 61 | * of 1970-01-01T00:00:00Z. 62 | * 63 | * @return the number of milliseconds since the epoch of 1970-01-01T00:00:00Z 64 | * @throws ArithmeticException if numeric overflow occurs 65 | */ 66 | public long getTime() { 67 | return zonedDateTime.toInstant().toEpochMilli(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/src/main/java/org/matomo/java/tracking/test/EcommerceExample.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.test; 2 | 3 | import java.net.URI; 4 | import org.matomo.java.tracking.MatomoRequests; 5 | import org.matomo.java.tracking.MatomoTracker; 6 | import org.matomo.java.tracking.TrackerConfiguration; 7 | import org.matomo.java.tracking.parameters.EcommerceItem; 8 | import org.matomo.java.tracking.parameters.EcommerceItems; 9 | import org.matomo.java.tracking.parameters.VisitorId; 10 | 11 | /** 12 | * Example for sending an ecommerce request. 13 | */ 14 | public class EcommerceExample { 15 | 16 | /** 17 | * Example for sending an ecommerce request. 18 | * 19 | * @param args ignored 20 | */ 21 | public static void main(String[] args) { 22 | 23 | TrackerConfiguration configuration = TrackerConfiguration 24 | .builder() 25 | .apiEndpoint(URI.create("https://www.yourdomain.com/matomo.php")) 26 | .defaultSiteId(1) 27 | .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") 28 | .logFailedTracking(true) 29 | .build(); 30 | 31 | try (MatomoTracker tracker = new MatomoTracker(configuration)) { 32 | tracker.sendBulkRequestAsync(MatomoRequests 33 | .ecommerceCartUpdate(50.0) 34 | .ecommerceItems(EcommerceItems 35 | .builder() 36 | .item(EcommerceItem 37 | .builder() 38 | .sku("XYZ12345") 39 | .name("Matomo - The big book about web analytics") 40 | .category("Education & Teaching") 41 | .price(23.1) 42 | .quantity(2) 43 | .build()) 44 | .item(EcommerceItem 45 | .builder() 46 | .sku("B0C2WV3MRJ") 47 | .name("Matomo for data visualization") 48 | .category("Education & Teaching") 49 | .price(15.0) 50 | .quantity(1) 51 | .build()) 52 | .build()) 53 | .visitorId(VisitorId.fromString("customer@mail.com")) 54 | .build() 55 | ); 56 | } catch (Exception e) { 57 | throw new RuntimeException("Could not close tracker", e); 58 | } 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/parameters/CustomVariableTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.fail; 12 | 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.Test; 15 | 16 | class CustomVariableTest { 17 | 18 | private CustomVariable customVariable; 19 | 20 | @BeforeEach 21 | void setUp() { 22 | customVariable = new CustomVariable("key", "value"); 23 | } 24 | 25 | @Test 26 | void testConstructorNullKey() { 27 | try { 28 | new CustomVariable(null, null); 29 | fail("Exception should have been throw."); 30 | } catch (NullPointerException e) { 31 | assertThat(e.getLocalizedMessage()).isEqualTo("key is marked non-null but is null"); 32 | } 33 | } 34 | 35 | @Test 36 | void testConstructorNullValue() { 37 | try { 38 | new CustomVariable("key", null); 39 | fail("Exception should have been throw."); 40 | } catch (NullPointerException e) { 41 | assertThat(e.getLocalizedMessage()).isEqualTo("value is marked non-null but is null"); 42 | } 43 | } 44 | 45 | @Test 46 | void testGetKey() { 47 | assertThat(customVariable.getKey()).isEqualTo("key"); 48 | } 49 | 50 | @Test 51 | void testGetValue() { 52 | assertThat(customVariable.getValue()).isEqualTo("value"); 53 | } 54 | 55 | @Test 56 | void equalsCustomVariable() { 57 | CustomVariable variableA = new CustomVariable("a", "b"); 58 | CustomVariable variableB = new CustomVariable("a", "b"); 59 | assertThat(variableA).isEqualTo(variableB); 60 | assertThat(variableA.hashCode()).isEqualTo(variableB.hashCode()); 61 | CustomVariable c = new CustomVariable("a", "c"); 62 | assertThat(variableA).isNotEqualTo(c); 63 | assertThat(variableA.hashCode()).isNotEqualTo(c.hashCode()); 64 | CustomVariable d = new CustomVariable("d", "b"); 65 | assertThat(variableA).isNotEqualTo(d); 66 | assertThat(variableA.hashCode()).isNotEqualTo(d.hashCode()); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /spring/src/main/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.spring; 9 | 10 | import java.net.URI; 11 | import org.matomo.java.tracking.TrackerConfiguration; 12 | import org.springframework.boot.context.properties.PropertyMapper; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.lang.NonNull; 15 | 16 | class StandardTrackerConfigurationBuilderCustomizer implements TrackerConfigurationBuilderCustomizer, Ordered { 17 | 18 | private final MatomoTrackerProperties properties; 19 | 20 | StandardTrackerConfigurationBuilderCustomizer(MatomoTrackerProperties properties) { 21 | this.properties = properties; 22 | } 23 | 24 | @Override 25 | public int getOrder() { 26 | return 0; 27 | } 28 | 29 | @Override 30 | public void customize(@NonNull TrackerConfiguration.TrackerConfigurationBuilder builder) { 31 | PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); 32 | map.from(properties::getApiEndpoint).as(URI::create).to(builder::apiEndpoint); 33 | map.from(properties::getDefaultSiteId).to(builder::defaultSiteId); 34 | map.from(properties::getDefaultAuthToken).to(builder::defaultAuthToken); 35 | map.from(properties::getEnabled).to(builder::enabled); 36 | map.from(properties::getConnectTimeout).to(builder::connectTimeout); 37 | map.from(properties::getSocketTimeout).to(builder::socketTimeout); 38 | map.from(properties::getProxyHost).to(builder::proxyHost); 39 | map.from(properties::getProxyPort).to(builder::proxyPort); 40 | map.from(properties::getProxyUsername).to(builder::proxyUsername); 41 | map.from(properties::getProxyPassword).to(builder::proxyPassword); 42 | map.from(properties::getUserAgent).to(builder::userAgent); 43 | map.from(properties::getLogFailedTracking).to(builder::logFailedTracking); 44 | map.from(properties::getDisableSslCertValidation).to(builder::disableSslCertValidation); 45 | map.from(properties::getDisableSslHostVerification).to(builder::disableSslHostVerification); 46 | map.from(properties::getThreadPoolSize).to(builder::threadPoolSize); 47 | } 48 | 49 | 50 | } -------------------------------------------------------------------------------- /servlet-jakarta/src/test/java/org/matomo/java/tracking/MatomoTrackerFilterIT.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.net.URI; 6 | import java.net.http.HttpClient; 7 | import java.net.http.HttpRequest; 8 | import java.net.http.HttpResponse; 9 | import org.eclipse.jetty.ee10.servlet.FilterHolder; 10 | import org.eclipse.jetty.ee10.servlet.ServletContextHandler; 11 | import org.eclipse.jetty.server.Server; 12 | import org.junit.jupiter.api.Test; 13 | import org.matomo.java.tracking.servlet.MatomoTrackerFilter; 14 | 15 | class MatomoTrackerFilterIT { 16 | 17 | @Test 18 | void sendsAnAsyncRequestOnFilter() throws Exception { 19 | 20 | 21 | TestSenderFactory senderFactory = new TestSenderFactory(); 22 | 23 | MatomoTracker tracker = new MatomoTracker( 24 | TrackerConfiguration 25 | .builder() 26 | .apiEndpoint(URI.create("http://localhost:8080/matomo.php")) 27 | .defaultSiteId(1) 28 | .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") 29 | .logFailedTracking(true) 30 | .build()); 31 | tracker.setSenderFactory(senderFactory); 32 | 33 | ServletContextHandler context = new ServletContextHandler(); 34 | context.setContextPath("/"); 35 | context.addFilter(new FilterHolder(new MatomoTrackerFilter(tracker)), "/*", null); 36 | Server server = new Server(0); 37 | server.setHandler(context); 38 | 39 | server.start(); 40 | URI uri = server.getURI(); 41 | HttpClient.newHttpClient().send( 42 | HttpRequest.newBuilder() 43 | .header("Accept-Language", "en-US,en;q=0.9,de;q=0.8") 44 | .uri(uri) 45 | .build(), 46 | HttpResponse.BodyHandlers.discarding() 47 | ); 48 | server.stop(); 49 | 50 | TestSender testSender = senderFactory.getTestSender(); 51 | assertThat(testSender.getRequests()).hasSize(1).satisfiesExactly(matomoRequest -> { 52 | assertThat(matomoRequest.getActionUrl()).isEqualTo(uri.toString()); 53 | assertThat(matomoRequest.getVisitorId()).isNotNull(); 54 | assertThat(matomoRequest.getVisitorIp()).isNotNull(); 55 | assertThat(matomoRequest.getHeaders()).containsEntry( 56 | "accept-language", 57 | "en-US,en;q=0.9,de;q=0.8" 58 | ); 59 | }); 60 | 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /servlet-javax/src/test/java/org/matomo/java/tracking/MatomoTrackerFilterIT.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.net.URI; 6 | import java.net.http.HttpClient; 7 | import java.net.http.HttpRequest; 8 | import java.net.http.HttpResponse; 9 | import org.eclipse.jetty.server.Server; 10 | import org.eclipse.jetty.servlet.FilterHolder; 11 | import org.eclipse.jetty.servlet.ServletContextHandler; 12 | import org.junit.jupiter.api.Test; 13 | import org.matomo.java.tracking.servlet.MatomoTrackerFilter; 14 | 15 | class MatomoTrackerFilterIT { 16 | 17 | @Test 18 | void sendsAnAsyncRequestOnFilter() throws Exception { 19 | 20 | 21 | TestSenderFactory senderFactory = new TestSenderFactory(); 22 | 23 | MatomoTracker tracker = new MatomoTracker( 24 | TrackerConfiguration 25 | .builder() 26 | .apiEndpoint(URI.create("http://localhost:8080/matomo.php")) 27 | .defaultSiteId(1) 28 | .defaultAuthToken("ee6e3dd9ed1b61f5328cf5978b5a8c71") 29 | .logFailedTracking(true) 30 | .build()); 31 | tracker.setSenderFactory(senderFactory); 32 | 33 | ServletContextHandler context = new ServletContextHandler(); 34 | context.setContextPath("/"); 35 | context.addFilter(new FilterHolder(new MatomoTrackerFilter(tracker)), "/*", null); 36 | Server server = new Server(0); 37 | server.setHandler(context); 38 | 39 | server.start(); 40 | URI uri = server.getURI(); 41 | HttpClient.newHttpClient().send( 42 | HttpRequest.newBuilder() 43 | .header("Accept-Language", "en-US,en;q=0.9,de;q=0.8") 44 | .uri(uri) 45 | .build(), 46 | HttpResponse.BodyHandlers.discarding() 47 | ); 48 | server.stop(); 49 | 50 | TestSender testSender = senderFactory.getTestSender(); 51 | assertThat(testSender.getRequests()).hasSize(1).satisfiesExactly(matomoRequest -> { 52 | assertThat(matomoRequest.getActionUrl()).isEqualTo(uri.toString()); 53 | assertThat(matomoRequest.getVisitorId()).isNotNull(); 54 | assertThat(matomoRequest.getVisitorIp()).isNotNull(); 55 | assertThat(matomoRequest.getHeaders()).containsEntry( 56 | "accept-language", 57 | "en-US,en;q=0.9,de;q=0.8" 58 | ); 59 | }); 60 | 61 | tracker.close(); 62 | 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /java8/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.piwik.java.tracking 6 | matomo-java-tracker-parent 7 | 3.4.1-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | matomo-java-tracker 12 | 3.4.1-SNAPSHOT 13 | jar 14 | 15 | Matomo Java Tracker Java 8 16 | Official Java implementation of the Matomo Tracking HTTP API for Java 8. 17 | 18 | 19 | 20 | org.piwik.java.tracking 21 | matomo-java-tracker-core 22 | ${project.version} 23 | 24 | 25 | org.slf4j 26 | slf4j-api 27 | 28 | 29 | com.github.spotbugs 30 | spotbugs-annotations 31 | 32 | 33 | org.projectlombok 34 | lombok 35 | provided 36 | 37 | 38 | org.junit.jupiter 39 | junit-jupiter 40 | test 41 | 42 | 43 | org.assertj 44 | assertj-core 45 | test 46 | 47 | 48 | com.github.tomakehurst 49 | wiremock-standalone 50 | 2.27.2 51 | test 52 | 53 | 54 | org.slf4j 55 | slf4j-simple 56 | test 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/AcceptLanguage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import edu.umd.cs.findbugs.annotations.NonNull; 11 | import edu.umd.cs.findbugs.annotations.Nullable; 12 | import java.util.List; 13 | import java.util.Locale.LanguageRange; 14 | import java.util.Objects; 15 | import java.util.stream.Collectors; 16 | import lombok.Builder; 17 | import lombok.Singular; 18 | import lombok.Value; 19 | 20 | /** 21 | * Describes the content for the Accept-Language header field that can be overridden by a custom parameter. The format 22 | * is specified in the corresponding RFC 4647 Matching of Language Tags 23 | * 24 | *

Example: "en-US,en;q=0.8,de;q=0.6" 25 | */ 26 | @Builder 27 | @Value 28 | public class AcceptLanguage { 29 | 30 | @Singular 31 | List languageRanges; 32 | 33 | /** 34 | * Creates the Accept-Language definition for a given header. 35 | * 36 | *

Please see {@link LanguageRange#parse(String)} for more information. Example: "en-US,en;q=0.8,de;q=0.6" 37 | * 38 | * @param header A header that can be null 39 | * @return The parsed header (probably reformatted). null if the header is null. 40 | * @see LanguageRange#parse(String) 41 | */ 42 | @Nullable 43 | public static AcceptLanguage fromHeader( 44 | @Nullable 45 | String header 46 | ) { 47 | if (header == null || header.trim().isEmpty()) { 48 | return null; 49 | } 50 | return new AcceptLanguage(LanguageRange.parse(header)); 51 | } 52 | 53 | /** 54 | * Returns the Accept Language header value. 55 | * 56 | * @return The header value, e.g. "en-US,en;q=0.8,de;q=0.6" 57 | */ 58 | @NonNull 59 | public String toString() { 60 | return languageRanges 61 | .stream() 62 | .filter(Objects::nonNull) 63 | .map(AcceptLanguage::format) 64 | .collect(Collectors.joining(",")); 65 | } 66 | 67 | private static String format( 68 | @NonNull 69 | LanguageRange languageRange 70 | ) { 71 | return languageRange.getWeight() == LanguageRange.MAX_WEIGHT ? languageRange.getRange() : 72 | String.format("%s;q=%s", languageRange.getRange(), languageRange.getWeight()); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/AuthTokenTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static java.util.Collections.singleton; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import java.net.URI; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class AuthTokenTest { 10 | 11 | @Test 12 | void determineAuthTokenReturnsAuthTokenFromRequest() { 13 | 14 | MatomoRequest request = 15 | MatomoRequests 16 | .event("Inbox", "Open", null, null) 17 | .authToken("bdeca231a312ab12cde124131bedfa23").build(); 18 | 19 | String authToken = AuthToken.determineAuthToken(null, singleton(request), null); 20 | 21 | assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); 22 | 23 | } 24 | 25 | @Test 26 | void determineAuthTokenReturnsAuthTokenFromTrackerConfiguration() { 27 | 28 | TrackerConfiguration trackerConfiguration = TrackerConfiguration 29 | .builder() 30 | .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo.")) 31 | .defaultAuthToken("bdeca231a312ab12cde124131bedfa23") 32 | .build(); 33 | 34 | String authToken = AuthToken.determineAuthToken(null, null, trackerConfiguration); 35 | 36 | assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); 37 | } 38 | 39 | @Test 40 | void determineAuthTokenFromTrackerConfigurationIfRequestTokenIsEmpty() { 41 | 42 | MatomoRequest request = MatomoRequests.ping().authToken("").build(); 43 | 44 | TrackerConfiguration trackerConfiguration = TrackerConfiguration 45 | .builder() 46 | .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo")) 47 | .defaultAuthToken("bdeca231a312ab12cde124131bedfa23") 48 | .build(); 49 | 50 | String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); 51 | 52 | assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); 53 | 54 | } 55 | 56 | @Test 57 | void determineAuthTokenFromTrackerConfigurationIfRequestTokenIsBlank() { 58 | 59 | MatomoRequest request = MatomoRequests.pageView("Help").authToken(" ").build(); 60 | 61 | TrackerConfiguration trackerConfiguration = TrackerConfiguration 62 | .builder() 63 | .apiEndpoint(URI.create("https://your-matomo-domain.example/matomo")) 64 | .defaultAuthToken("bdeca231a312ab12cde124131bedfa23") 65 | .build(); 66 | 67 | String authToken = AuthToken.determineAuthToken(null, singleton(request), trackerConfiguration); 68 | 69 | assertThat(authToken).isEqualTo("bdeca231a312ab12cde124131bedfa23"); 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /core/src/main/java/org/piwik/java/tracking/PiwikTracker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.piwik.java.tracking; 9 | 10 | import edu.umd.cs.findbugs.annotations.NonNull; 11 | import edu.umd.cs.findbugs.annotations.Nullable; 12 | import org.matomo.java.tracking.MatomoTracker; 13 | 14 | /** 15 | * Creates a new PiwikTracker instance. This class is deprecated and will be removed in the future. 16 | * 17 | * @author brettcsorba 18 | * @deprecated Use {@link MatomoTracker} instead. 19 | */ 20 | @Deprecated 21 | public class PiwikTracker extends MatomoTracker { 22 | 23 | /** 24 | * Creates a new PiwikTracker instance with the given host URL. 25 | * 26 | * @param hostUrl the host URL of the Matomo server 27 | * @deprecated Use {@link MatomoTracker} instead. 28 | */ 29 | @Deprecated 30 | public PiwikTracker(@NonNull String hostUrl) { 31 | super(hostUrl); 32 | } 33 | 34 | /** 35 | * Creates a new PiwikTracker instance with the given host URL and timeout in milliseconds. Use -1 for no timeout. 36 | * 37 | * @param hostUrl the host URL of the Matomo server 38 | * @param timeout the timeout in milliseconds or -1 for no timeout 39 | * @deprecated Use {@link MatomoTracker} instead. 40 | */ 41 | @Deprecated 42 | public PiwikTracker(@NonNull String hostUrl, int timeout) { 43 | super(hostUrl, timeout); 44 | } 45 | 46 | /** 47 | * Creates a new PiwikTracker instance with the given host URL and proxy settings. 48 | * 49 | * @param hostUrl the host URL of the Matomo server 50 | * @param proxyHost the proxy host 51 | * @param proxyPort the proxy port 52 | * @deprecated Use {@link MatomoTracker} instead. 53 | */ 54 | @Deprecated 55 | public PiwikTracker(@NonNull String hostUrl, @Nullable String proxyHost, int proxyPort) { 56 | super(hostUrl, proxyHost, proxyPort); 57 | } 58 | 59 | /** 60 | * Creates a new PiwikTracker instance with the given host URL, proxy settings and timeout in milliseconds. Use -1 for 61 | * no timeout. 62 | * 63 | * @param hostUrl the host URL of the Matomo server 64 | * @param proxyHost the proxy host 65 | * @param proxyPort the proxy port 66 | * @param timeout the timeout in milliseconds or -1 for no timeout 67 | * @deprecated Use {@link MatomoTracker} instead. 68 | */ 69 | @Deprecated 70 | public PiwikTracker(@NonNull String hostUrl, @Nullable String proxyHost, int proxyPort, int timeout) { 71 | super(hostUrl, proxyHost, proxyPort, timeout); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/TestSender.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static java.util.Collections.singleton; 4 | 5 | import edu.umd.cs.findbugs.annotations.NonNull; 6 | import edu.umd.cs.findbugs.annotations.Nullable; 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.concurrent.CompletableFuture; 10 | import lombok.Getter; 11 | import lombok.RequiredArgsConstructor; 12 | 13 | /** 14 | * A {@link Sender} implementation that does not send anything but stores the requests and queries. 15 | * 16 | *

This class is intended for testing purposes only. It does not send anything to the Matomo server. Instead, it 17 | * stores the requests and queries in collections that can be accessed via {@link #getRequests()} and {@link 18 | * #getQueries()}. 19 | */ 20 | @RequiredArgsConstructor 21 | @Getter 22 | class TestSender implements Sender { 23 | 24 | private final Collection requests = new ArrayList<>(); 25 | 26 | private final Collection queries = new ArrayList<>(); 27 | 28 | private final TrackerConfiguration trackerConfiguration; 29 | 30 | private final QueryCreator queryCreator; 31 | 32 | @NonNull 33 | @Override 34 | public CompletableFuture sendSingleAsync(@NonNull MatomoRequest request) { 35 | createQueryAndAddRequest(request, null); 36 | return CompletableFuture.completedFuture(request); 37 | } 38 | 39 | @Override 40 | public void sendSingle(@NonNull MatomoRequest request) { 41 | createQueryAndAddRequest(request, null); 42 | } 43 | 44 | @Override 45 | public void sendBulk( 46 | @NonNull Iterable requests, @Nullable String overrideAuthToken 47 | ) { 48 | for (MatomoRequest request : requests) { 49 | createQueryAndAddRequest(request, overrideAuthToken); 50 | } 51 | } 52 | 53 | @NonNull 54 | @Override 55 | public CompletableFuture sendBulkAsync( 56 | @NonNull Collection requests, @Nullable String overrideAuthToken 57 | ) { 58 | for (MatomoRequest request : requests) { 59 | createQueryAndAddRequest(request, overrideAuthToken); 60 | } 61 | return CompletableFuture.completedFuture(null); 62 | } 63 | 64 | 65 | 66 | private void createQueryAndAddRequest(@lombok.NonNull MatomoRequest request, @Nullable String overrideAuthToken) { 67 | String authToken = AuthToken.determineAuthToken(overrideAuthToken, singleton(request), trackerConfiguration); 68 | queries.add(queryCreator.createQuery(request, authToken)); 69 | requests.add(request); 70 | } 71 | 72 | @Override 73 | public void close() { 74 | // do nothing 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /servlet-javax/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.piwik.java.tracking 6 | matomo-java-tracker-parent 7 | 3.4.1-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | matomo-java-tracker-servlet-javax 12 | 3.4.1-SNAPSHOT 13 | jar 14 | 15 | Matomo Java Tracker Servlet Javax 16 | Integrates Matomo Java Tracker into your javax servlet based application 17 | 18 | 19 | 1.8 20 | 1.8 21 | 22 | 23 | 24 | 25 | org.piwik.java.tracking 26 | matomo-java-tracker 27 | ${project.version} 28 | 29 | 30 | javax.servlet 31 | javax.servlet-api 32 | 4.0.1 33 | provided 34 | 35 | 36 | org.slf4j 37 | slf4j-api 38 | 39 | 40 | com.github.spotbugs 41 | spotbugs-annotations 42 | 43 | 44 | org.projectlombok 45 | lombok 46 | provided 47 | 48 | 49 | org.junit.jupiter 50 | junit-jupiter 51 | test 52 | 53 | 54 | org.eclipse.jetty 55 | jetty-servlet 56 | 11.0.0 57 | test 58 | 59 | 60 | org.assertj 61 | assertj-core 62 | test 63 | 64 | 65 | org.slf4j 66 | slf4j-simple 67 | test 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /servlet-jakarta/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.piwik.java.tracking 6 | matomo-java-tracker-parent 7 | 3.4.1-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | matomo-java-tracker-servlet-jakarta 12 | 3.4.1-SNAPSHOT 13 | jar 14 | 15 | Matomo Java Tracker Servlet Jakarta 16 | Integrates Matomo Java Tracker into your Jakarta servlet based application 17 | 18 | 19 | 11 20 | 11 21 | 22 | 23 | 24 | 25 | org.piwik.java.tracking 26 | matomo-java-tracker-java11 27 | ${project.version} 28 | 29 | 30 | jakarta.servlet 31 | jakarta.servlet-api 32 | 6.1.0 33 | provided 34 | 35 | 36 | org.slf4j 37 | slf4j-api 38 | 39 | 40 | com.github.spotbugs 41 | spotbugs-annotations 42 | 43 | 44 | org.projectlombok 45 | lombok 46 | provided 47 | 48 | 49 | org.junit.jupiter 50 | junit-jupiter 51 | test 52 | 53 | 54 | org.eclipse.jetty.ee10 55 | jetty-ee10-servlet 56 | 12.0.16 57 | test 58 | 59 | 60 | org.assertj 61 | assertj-core 62 | test 63 | 64 | 65 | org.slf4j 66 | slf4j-simple 67 | test 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /spring/src/test/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfigurationIT.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.spring; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.net.URI; 6 | import java.time.Duration; 7 | import org.junit.jupiter.api.Test; 8 | import org.matomo.java.tracking.MatomoTracker; 9 | import org.matomo.java.tracking.TrackerConfiguration; 10 | import org.springframework.boot.autoconfigure.AutoConfigurations; 11 | import org.springframework.boot.test.context.runner.ApplicationContextRunner; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | 15 | class MatomoTrackerAutoConfigurationIT { 16 | 17 | private final ApplicationContextRunner contextRunner = 18 | new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(MatomoTrackerAutoConfiguration.class)); 19 | 20 | 21 | @Test 22 | void matomoTrackerRegistration() { 23 | contextRunner.withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php").run(context -> { 24 | assertThat(context).hasSingleBean(MatomoTracker.class).hasBean("matomoTracker"); 25 | }); 26 | } 27 | 28 | @Test 29 | void additionalTrackerConfigurationBuilderCustomization() { 30 | this.contextRunner 31 | .withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php") 32 | .withUserConfiguration(TrackerConfigurationBuilderCustomizerConfig.class) 33 | .run(context -> { 34 | TrackerConfiguration trackerConfiguration = context.getBean(TrackerConfiguration.class); 35 | assertThat(trackerConfiguration.getConnectTimeout()).isEqualTo(Duration.ofMinutes(1L)); 36 | }); 37 | } 38 | 39 | @Test 40 | void customTrackerConfigurationBuilder() { 41 | this.contextRunner 42 | .withPropertyValues("matomo.tracker.api-endpoint:https://test.com/matomo.php") 43 | .withUserConfiguration(TrackerConfigurationBuilderConfig.class) 44 | .run(context -> { 45 | TrackerConfiguration trackerConfiguration = context.getBean(TrackerConfiguration.class); 46 | assertThat(trackerConfiguration.isDisableSslHostVerification()).isTrue(); 47 | }); 48 | } 49 | 50 | @Configuration 51 | static class TrackerConfigurationBuilderCustomizerConfig { 52 | 53 | @Bean 54 | TrackerConfigurationBuilderCustomizer customConnectTimeout() { 55 | return configurationBuilder -> configurationBuilder.connectTimeout(Duration.ofMinutes(1L)); 56 | } 57 | 58 | } 59 | 60 | @Configuration 61 | static class TrackerConfigurationBuilderConfig { 62 | 63 | @Bean 64 | TrackerConfiguration.TrackerConfigurationBuilder customTrackerConfigurationBuilder() { 65 | return TrackerConfiguration.builder().apiEndpoint(URI.create("https://test.com/matomo.php")).disableSslHostVerification(true); 66 | } 67 | 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /spring/src/test/java/org/matomo/java/tracking/spring/StandardTrackerConfigurationBuilderCustomizerIT.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.spring; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.time.Duration; 6 | import org.junit.jupiter.api.Test; 7 | import org.matomo.java.tracking.TrackerConfiguration; 8 | 9 | class StandardTrackerConfigurationBuilderCustomizerIT { 10 | 11 | @Test 12 | void createsStandardTrackerConfigurationBuilderCustomizer() { 13 | MatomoTrackerProperties properties = new MatomoTrackerProperties(); 14 | properties.setApiEndpoint("https://test.com/matomo.php"); 15 | properties.setDefaultSiteId(1); 16 | properties.setDefaultAuthToken("abc123def4563123abc123def4563123"); 17 | properties.setEnabled(true); 18 | properties.setConnectTimeout(Duration.ofMinutes(1L)); 19 | properties.setSocketTimeout(Duration.ofMinutes(2L)); 20 | properties.setProxyHost("proxy.example.com"); 21 | properties.setProxyPort(8080); 22 | properties.setProxyUsername("user"); 23 | properties.setProxyPassword("password"); 24 | properties.setUserAgent("Mozilla/5.0 (compatible; AcmeInc/1.0; +https://example.com/bot.html)"); 25 | properties.setLogFailedTracking(true); 26 | properties.setDisableSslCertValidation(true); 27 | properties.setDisableSslHostVerification(true); 28 | properties.setThreadPoolSize(10); 29 | StandardTrackerConfigurationBuilderCustomizer customizer = 30 | new StandardTrackerConfigurationBuilderCustomizer(properties); 31 | TrackerConfiguration.TrackerConfigurationBuilder builder = TrackerConfiguration.builder(); 32 | 33 | customizer.customize(builder); 34 | 35 | assertThat(customizer.getOrder()).isZero(); 36 | TrackerConfiguration configuration = builder.build(); 37 | assertThat(configuration.getApiEndpoint()).hasToString("https://test.com/matomo.php"); 38 | assertThat(configuration.getDefaultSiteId()).isEqualTo(1); 39 | assertThat(configuration.getDefaultAuthToken()).isEqualTo("abc123def4563123abc123def4563123"); 40 | assertThat(configuration.isEnabled()).isTrue(); 41 | assertThat(configuration.getConnectTimeout()).hasSeconds(60L); 42 | assertThat(configuration.getSocketTimeout()).hasSeconds(120L); 43 | assertThat(configuration.getProxyHost()).isEqualTo("proxy.example.com"); 44 | assertThat(configuration.getProxyPort()).isEqualTo(8080); 45 | assertThat(configuration.getProxyUsername()).isEqualTo("user"); 46 | assertThat(configuration.getProxyPassword()).isEqualTo("password"); 47 | assertThat(configuration.getUserAgent()).isEqualTo( 48 | "Mozilla/5.0 (compatible; AcmeInc/1.0; +https://example.com/bot.html)"); 49 | assertThat(configuration.isLogFailedTracking()).isTrue(); 50 | assertThat(configuration.isDisableSslCertValidation()).isTrue(); 51 | assertThat(configuration.isDisableSslHostVerification()).isTrue(); 52 | assertThat(configuration.getThreadPoolSize()).isEqualTo(10); 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /java11/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.piwik.java.tracking 6 | matomo-java-tracker-parent 7 | 3.4.1-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | matomo-java-tracker-java11 12 | 3.4.1-SNAPSHOT 13 | jar 14 | 15 | Matomo Java Tracker Java 11 16 | Official Java implementation of the Matomo Tracking HTTP API for Java 11. 17 | 18 | 19 | 11 20 | 11 21 | 22 | 23 | 24 | 25 | org.piwik.java.tracking 26 | matomo-java-tracker-core 27 | ${project.version} 28 | 29 | 30 | org.projectlombok 31 | lombok 32 | provided 33 | 34 | 35 | com.github.spotbugs 36 | spotbugs-annotations 37 | 38 | 39 | org.junit.jupiter 40 | junit-jupiter 41 | test 42 | 43 | 44 | org.assertj 45 | assertj-core 46 | test 47 | 48 | 49 | org.wiremock 50 | wiremock 51 | 3.11.0 52 | test 53 | 54 | 55 | org.slf4j 56 | slf4j-simple 57 | test 58 | 59 | 60 | 61 | 62 | 63 | 64 | org.apache.maven.plugins 65 | maven-failsafe-plugin 66 | 67 | 68 | true 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /java11/src/main/java/org/matomo/java/tracking/Java11SenderProvider.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import edu.umd.cs.findbugs.annotations.Nullable; 4 | import java.net.CookieManager; 5 | import java.net.InetSocketAddress; 6 | import java.net.ProxySelector; 7 | import java.net.http.HttpClient; 8 | import java.security.SecureRandom; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Executors; 11 | import javax.net.ssl.SSLContext; 12 | import javax.net.ssl.TrustManager; 13 | 14 | /** 15 | * Provides a {@link Sender} implementation based on Java 11. 16 | */ 17 | public class Java11SenderProvider implements SenderProvider { 18 | 19 | private static final TrustManager[] TRUST_ALL_MANAGERS = {new TrustingX509TrustManager()}; 20 | 21 | @Override 22 | public Sender provideSender( 23 | TrackerConfiguration trackerConfiguration, QueryCreator queryCreator 24 | ) { 25 | CookieManager cookieManager = new CookieManager(); 26 | ExecutorService executorService = Executors.newFixedThreadPool( 27 | trackerConfiguration.getThreadPoolSize(), 28 | new DaemonThreadFactory() 29 | ); 30 | HttpClient.Builder builder = HttpClient 31 | .newBuilder() 32 | .cookieHandler(cookieManager) 33 | .executor(executorService); 34 | if (trackerConfiguration.getConnectTimeout() != null 35 | && trackerConfiguration.getConnectTimeout().toMillis() > 0L) { 36 | builder.connectTimeout(trackerConfiguration.getConnectTimeout()); 37 | } 38 | if (!isEmpty(trackerConfiguration.getProxyHost()) && trackerConfiguration.getProxyPort() > 0) { 39 | builder.proxy(ProxySelector.of(new InetSocketAddress( 40 | trackerConfiguration.getProxyHost(), 41 | trackerConfiguration.getProxyPort() 42 | ))); 43 | if (!isEmpty(trackerConfiguration.getProxyUsername()) 44 | && !isEmpty(trackerConfiguration.getProxyPassword())) { 45 | builder.authenticator(new ProxyAuthenticator( 46 | trackerConfiguration.getProxyUsername(), 47 | trackerConfiguration.getProxyPassword() 48 | )); 49 | } 50 | } 51 | if (trackerConfiguration.isDisableSslCertValidation()) { 52 | try { 53 | SSLContext sslContext = SSLContext.getInstance("SSL"); 54 | sslContext.init(null, TRUST_ALL_MANAGERS, new SecureRandom()); 55 | builder.sslContext(sslContext); 56 | } catch (Exception e) { 57 | throw new MatomoException("Could not disable SSL certification validation", e); 58 | } 59 | } 60 | if (trackerConfiguration.isDisableSslHostVerification()) { 61 | throw new MatomoException("Please disable SSL hostname verification manually using the system parameter -Djdk.internal.httpclient.disableHostnameVerification=true"); 62 | } 63 | 64 | return new Java11Sender( 65 | trackerConfiguration, 66 | queryCreator, 67 | builder.build(), 68 | cookieManager.getCookieStore(), 69 | executorService 70 | ); 71 | } 72 | 73 | private static boolean isEmpty( 74 | @Nullable String str 75 | ) { 76 | return str == null || str.isEmpty() || str.trim().isEmpty(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/TrackingParameterMethodTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 4 | 5 | import java.util.regex.Pattern; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class TrackingParameterMethodTest { 9 | 10 | @Test 11 | void validateParameterValueFailsIfPatternDoesNotMatch() { 12 | TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod 13 | .builder() 14 | .parameterName("foo") 15 | .pattern(Pattern.compile("bar")) 16 | .build(); 17 | 18 | assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue("baz")) 19 | .isInstanceOf(MatomoException.class) 20 | .hasMessage("Invalid value for foo. Must match regex bar"); 21 | } 22 | 23 | @Test 24 | void doNothingIfPatternIsNull() { 25 | TrackingParameterMethod trackingParameterMethod = 26 | TrackingParameterMethod.builder().parameterName("foo").maxLength(255).build(); 27 | 28 | trackingParameterMethod.validateParameterValue("baz"); 29 | } 30 | 31 | @Test 32 | void doNothingIfParameterValueIsNotCharSequence() { 33 | TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod 34 | .builder() 35 | .parameterName("foo") 36 | .pattern(Pattern.compile("bar")) 37 | .maxLength(255) 38 | .min(1) 39 | .max(1) 40 | .build(); 41 | 42 | trackingParameterMethod.validateParameterValue(1); 43 | } 44 | 45 | @Test 46 | void failIfParameterValueIsNull() { 47 | TrackingParameterMethod trackingParameterMethod = TrackingParameterMethod 48 | .builder() 49 | .parameterName("foo") 50 | .pattern(Pattern.compile("bar")) 51 | .maxLength(255) 52 | .build(); 53 | 54 | assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue(null)) 55 | .isInstanceOf(NullPointerException.class) 56 | .hasMessage("parameterValue is marked non-null but is null"); 57 | } 58 | 59 | @Test 60 | void validateParameterValueFailsIfMaxLengthIsExceeded() { 61 | TrackingParameterMethod trackingParameterMethod = 62 | TrackingParameterMethod.builder().parameterName("foo").maxLength(3).build(); 63 | 64 | assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue("foobar")) 65 | .isInstanceOf(MatomoException.class) 66 | .hasMessage("Invalid value for foo. Must be less or equal than 3 characters"); 67 | } 68 | 69 | @Test 70 | void failIfParameterValueIsLessThanMin() { 71 | TrackingParameterMethod trackingParameterMethod = 72 | TrackingParameterMethod.builder().parameterName("foo").min(3.0).build(); 73 | 74 | assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue(1)) 75 | .isInstanceOf(MatomoException.class) 76 | .hasMessage("Invalid value for foo. Must be greater or equal than 3"); 77 | } 78 | 79 | @Test 80 | void failIfParameterValueIsGreaterThanMax() { 81 | TrackingParameterMethod trackingParameterMethod = 82 | TrackingParameterMethod.builder().parameterName("foo").max(3.0).build(); 83 | 84 | assertThatThrownBy(() -> trackingParameterMethod.validateParameterValue(4)) 85 | .isInstanceOf(MatomoException.class) 86 | .hasMessage("Invalid value for foo. Must be less or equal than 3"); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /spring/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.piwik.java.tracking 6 | matomo-java-tracker-parent 7 | 3.4.1-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | matomo-java-tracker-spring-boot-starter 12 | jar 13 | 14 | Matomo Java Tracker Spring Boot Starter 15 | Spring integration of Matomo Java Tracker 16 | 17 | 18 | 17 19 | 17 20 | 3.4.2 21 | 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-dependencies 28 | ${spring-boot.version} 29 | pom 30 | import 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.piwik.java.tracking 38 | matomo-java-tracker-java11 39 | ${project.version} 40 | 41 | 42 | org.piwik.java.tracking 43 | matomo-java-tracker-servlet-jakarta 44 | ${project.version} 45 | 46 | 47 | jakarta.servlet 48 | jakarta.servlet-api 49 | provided 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-autoconfigure-processor 58 | true 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-configuration-processor 63 | true 64 | 65 | 66 | org.projectlombok 67 | lombok 68 | provided 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-starter-test 73 | test 74 | 75 | 76 | org.slf4j 77 | slf4j-simple 78 | test 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/RequestValidatorTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 4 | 5 | import java.util.Locale; 6 | import org.junit.jupiter.api.Test; 7 | import org.piwik.java.tracking.PiwikDate; 8 | import org.piwik.java.tracking.PiwikLocale; 9 | 10 | class RequestValidatorTest { 11 | 12 | private final MatomoRequest request = new MatomoRequest(); 13 | 14 | 15 | @Test 16 | void testSearchResultsCount() { 17 | 18 | request.setSearchResultsCount(100L); 19 | 20 | assertThatThrownBy(() -> RequestValidator.validate(request, null)) 21 | .isInstanceOf(MatomoException.class) 22 | .hasMessage("Search query must be set if search results count is set"); 23 | 24 | } 25 | 26 | @Test 27 | void testVisitorLongitude() { 28 | request.setVisitorLongitude(20.5); 29 | 30 | assertThatThrownBy(() -> RequestValidator.validate(request, null)) 31 | .isInstanceOf(MatomoException.class) 32 | .hasMessage( 33 | "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); 34 | } 35 | 36 | @Test 37 | void testVisitorLatitude() { 38 | request.setVisitorLatitude(10.5); 39 | 40 | assertThatThrownBy(() -> RequestValidator.validate(request, null)) 41 | .isInstanceOf(MatomoException.class) 42 | .hasMessage( 43 | "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); 44 | } 45 | 46 | @Test 47 | void testVisitorCity() { 48 | request.setVisitorCity("city"); 49 | 50 | assertThatThrownBy(() -> RequestValidator.validate(request, null)) 51 | .isInstanceOf(MatomoException.class) 52 | .hasMessage( 53 | "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); 54 | } 55 | 56 | @Test 57 | void testVisitorRegion() { 58 | request.setVisitorRegion("region"); 59 | 60 | assertThatThrownBy(() -> RequestValidator.validate(request, null)) 61 | .isInstanceOf(MatomoException.class) 62 | .hasMessage( 63 | "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); 64 | } 65 | 66 | @Test 67 | void testVisitorCountryTE() { 68 | PiwikLocale country = new PiwikLocale(Locale.US); 69 | request.setVisitorCountry(country); 70 | 71 | 72 | assertThatThrownBy(() -> RequestValidator.validate(request, null)) 73 | .isInstanceOf(MatomoException.class) 74 | .hasMessage( 75 | "Auth token must be present if visitor longitude, latitude, region, city, country or IP are set"); 76 | } 77 | 78 | @Test 79 | void testRequestDatetime() { 80 | 81 | PiwikDate date = new PiwikDate(1000L); 82 | request.setRequestDatetime(date); 83 | 84 | assertThatThrownBy(() -> RequestValidator.validate(request, null)) 85 | .isInstanceOf(MatomoException.class) 86 | .hasMessage("Auth token must be present if request timestamp is more than four hours ago"); 87 | 88 | } 89 | 90 | @Test 91 | void failsIfAuthTokenIsNot32CharactersLong() { 92 | assertThatThrownBy(() -> RequestValidator.validate(request, "123456789012345678901234567890")) 93 | .isInstanceOf(IllegalArgumentException.class) 94 | .hasMessage("Auth token must be exactly 32 characters long"); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/MatomoRequestBuilderTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static java.util.Collections.singletonMap; 4 | import static org.assertj.core.api.Assertions.assertThat; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import org.matomo.java.tracking.parameters.CustomVariables; 8 | 9 | 10 | class MatomoRequestBuilderTest { 11 | 12 | @Test 13 | void buildsRequest() { 14 | CustomVariable pageCustomVariable = 15 | new CustomVariable("pageCustomVariableName", "pageCustomVariableValue"); 16 | CustomVariable visitCustomVariable = 17 | new CustomVariable("visitCustomVariableName", "visitCustomVariableValue"); 18 | 19 | MatomoRequest matomoRequest = new MatomoRequestBuilder() 20 | .siteId(42) 21 | .actionName("ACTION_NAME") 22 | .actionUrl("https://www.your-domain.tld/some/page?query=foo") 23 | .referrerUrl("https://referrer.com") 24 | .additionalParameters(singletonMap( 25 | "trackingParameterName", 26 | "trackingParameterValue" 27 | )) 28 | .pageCustomVariables(new CustomVariables().add(pageCustomVariable, 2)) 29 | .visitCustomVariables(new CustomVariables().add(visitCustomVariable, 3)) 30 | .customAction(true) 31 | .build(); 32 | 33 | assertThat(matomoRequest.getSiteId()).isEqualTo(42); 34 | assertThat(matomoRequest.getActionName()).isEqualTo("ACTION_NAME"); 35 | assertThat(matomoRequest.getApiVersion()).isEqualTo("1"); 36 | assertThat(matomoRequest.getActionUrl()).isEqualTo( 37 | "https://www.your-domain.tld/some/page?query=foo"); 38 | assertThat(matomoRequest.getVisitorId().toString()).hasSize(16).isHexadecimal(); 39 | assertThat(matomoRequest.getRandomValue().toString()).hasSize(20).isHexadecimal(); 40 | assertThat(matomoRequest.getResponseAsImage()).isFalse(); 41 | assertThat(matomoRequest.getRequired()).isTrue(); 42 | assertThat(matomoRequest.getReferrerUrl()).isEqualTo("https://referrer.com"); 43 | assertThat(matomoRequest.getCustomTrackingParameter("trackingParameterName")).isEqualTo( 44 | "trackingParameterValue"); 45 | assertThat(matomoRequest.getPageCustomVariables()).hasToString( 46 | "{\"2\":[\"pageCustomVariableName\",\"pageCustomVariableValue\"]}"); 47 | assertThat(matomoRequest.getVisitCustomVariables()).hasToString( 48 | "{\"3\":[\"visitCustomVariableName\",\"visitCustomVariableValue\"]}"); 49 | assertThat(matomoRequest.getCustomAction()).isTrue(); 50 | 51 | } 52 | 53 | @Test 54 | void setCustomTrackingParameters() { 55 | MatomoRequest matomoRequest = new MatomoRequestBuilder() 56 | .customTrackingParameters(singletonMap("foo", "bar")) 57 | .siteId(42) 58 | .actionName("ACTION_NAME") 59 | .actionUrl("https://www.your-domain.tld/some/page?query=foo") 60 | .referrerUrl("https://referrer.com") 61 | .build(); 62 | 63 | assertThat(matomoRequest.getCustomTrackingParameter("foo")).isEqualTo("bar"); 64 | } 65 | 66 | @Test 67 | void setCustomTrackingParametersWithCollectopm() { 68 | MatomoRequest matomoRequest = new MatomoRequestBuilder() 69 | .customTrackingParameters(singletonMap("foo", "bar")) 70 | .siteId(42) 71 | .actionName("ACTION_NAME") 72 | .actionUrl("https://www.your-domain.tld/some/page?query=foo") 73 | .referrerUrl("https://referrer.com") 74 | .build(); 75 | 76 | assertThat(matomoRequest.getCustomTrackingParameter("foo")).isEqualTo("bar"); 77 | } 78 | 79 | @Test 80 | void acceptsNullAsHeaderAcceptLanguage() { 81 | MatomoRequest matomoRequest = new MatomoRequestBuilder() 82 | .headerAcceptLanguage((String) null) 83 | .build(); 84 | 85 | assertThat(matomoRequest.getHeaderAcceptLanguage()).isNull(); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/Country.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import edu.umd.cs.findbugs.annotations.Nullable; 11 | import java.util.List; 12 | import java.util.Locale; 13 | import java.util.Locale.LanguageRange; 14 | import lombok.AccessLevel; 15 | import lombok.NonNull; 16 | import lombok.RequiredArgsConstructor; 17 | 18 | /** 19 | * A two-letter country code representing a country. 20 | * 21 | *

See ISO 3166-1 alpha-2 for a list of valid codes. 22 | */ 23 | @RequiredArgsConstructor(access = AccessLevel.PRIVATE) 24 | public class Country { 25 | 26 | @NonNull 27 | private String code; 28 | 29 | /** 30 | * Only for internal use to grant downwards compatibility to {@link org.matomo.java.tracking.MatomoLocale}. 31 | * 32 | * @param locale A locale that must contain a country code 33 | */ 34 | @Deprecated 35 | protected Country( 36 | @edu.umd.cs.findbugs.annotations.NonNull 37 | Locale locale 38 | ) { 39 | setLocale(locale); 40 | } 41 | 42 | /** 43 | * Creates a country from a given code. 44 | * 45 | * @param code Must consist of two lower letters or simply null. Case is ignored 46 | * @return The country or null if code was null 47 | */ 48 | @Nullable 49 | public static Country fromCode( 50 | @Nullable 51 | String code 52 | ) { 53 | if (code == null || code.isEmpty() || code.trim().isEmpty()) { 54 | return null; 55 | } 56 | if (code.length() == 2) { 57 | return new Country(code.toLowerCase(Locale.ROOT)); 58 | } 59 | throw new IllegalArgumentException("Invalid country code"); 60 | } 61 | 62 | /** 63 | * Extracts the country from the given accept language header. 64 | * 65 | * @param ranges A language range list. See {@link LanguageRange#parse(String)} 66 | * @return The country or null if ranges was null 67 | */ 68 | @Nullable 69 | public static Country fromLanguageRanges( 70 | @Nullable 71 | String ranges 72 | ) { 73 | if (ranges == null || ranges.isEmpty() || ranges.trim().isEmpty()) { 74 | return null; 75 | } 76 | List languageRanges = LanguageRange.parse(ranges); 77 | for (LanguageRange languageRange : languageRanges) { 78 | String range = languageRange.getRange(); 79 | String[] split = range.split("-"); 80 | if (split.length == 2 && split[1].length() == 2) { 81 | return new Country(split[1].toLowerCase(Locale.ROOT)); 82 | } 83 | } 84 | throw new IllegalArgumentException("Invalid country code"); 85 | } 86 | 87 | /** 88 | * Returns the locale for this country. 89 | * 90 | * @return The locale for this country 91 | * @see Locale#forLanguageTag(String) 92 | * @deprecated Since you instantiate this class, you can determine the language on your own 93 | * using {@link Locale#forLanguageTag(String)} 94 | */ 95 | @Deprecated 96 | public Locale getLocale() { 97 | return Locale.forLanguageTag(code); 98 | } 99 | 100 | /** 101 | * Sets the locale for this country. 102 | * 103 | * @param locale A locale that must contain a country code 104 | * @see Locale#getCountry() 105 | * @deprecated Since you instantiate this class, you can determine the language on your own 106 | * using {@link Locale#getCountry()} 107 | */ 108 | @Deprecated 109 | public final void setLocale(Locale locale) { 110 | if (locale == null || locale.getCountry() == null || locale.getCountry().isEmpty()) { 111 | throw new IllegalArgumentException("Invalid locale"); 112 | } 113 | code = locale.getCountry().toLowerCase(Locale.ENGLISH); 114 | } 115 | 116 | @Override 117 | public String toString() { 118 | return code; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/parameters/CountryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 12 | 13 | import java.util.Locale; 14 | import org.junit.jupiter.api.Test; 15 | import org.junit.jupiter.params.ParameterizedTest; 16 | import org.junit.jupiter.params.provider.NullAndEmptySource; 17 | 18 | class CountryTest { 19 | 20 | @Test 21 | void createsCountryFromCode() { 22 | 23 | Country country = Country.fromCode("DE"); 24 | 25 | assertThat(country).hasToString("de"); 26 | 27 | } 28 | 29 | @Test 30 | void createsCountryFromAcceptLanguageHeader() { 31 | 32 | Country country = Country.fromLanguageRanges("en-GB;q=0.7,de,de-DE;q=0.9,en;q=0.8,en-US;q=0.6"); 33 | 34 | assertThat(country).hasToString("de"); 35 | 36 | } 37 | 38 | @ParameterizedTest 39 | @NullAndEmptySource 40 | void returnsNullOnEmptyRanges(String ranges) { 41 | 42 | Country country = Country.fromLanguageRanges(ranges); 43 | 44 | assertThat(country).isNull(); 45 | 46 | } 47 | 48 | @Test 49 | void failsOnInvalidCountryCode() { 50 | 51 | assertThatThrownBy(() -> Country.fromCode("invalid")) 52 | .isInstanceOf(IllegalArgumentException.class) 53 | .hasMessage("Invalid country code"); 54 | 55 | } 56 | 57 | @Test 58 | void failsOnInvalidCountryCodeLength() { 59 | 60 | assertThatThrownBy(() -> Country.fromCode("invalid")) 61 | .isInstanceOf(IllegalArgumentException.class) 62 | .hasMessage("Invalid country code"); 63 | 64 | } 65 | 66 | @Test 67 | void returnsNullOnNullCode() { 68 | 69 | Country country = Country.fromCode(null); 70 | 71 | assertThat(country).isNull(); 72 | 73 | } 74 | 75 | @Test 76 | void returnsNullOnEmptyCode() { 77 | 78 | Country country = Country.fromCode(""); 79 | 80 | assertThat(country).isNull(); 81 | 82 | } 83 | 84 | @Test 85 | void returnsNullOnBlankCode() { 86 | 87 | Country country = Country.fromCode(" "); 88 | 89 | assertThat(country).isNull(); 90 | 91 | } 92 | 93 | @Test 94 | void returnsNullOnNullRanges() { 95 | 96 | Country country = Country.fromLanguageRanges(null); 97 | 98 | assertThat(country).isNull(); 99 | 100 | } 101 | 102 | @Test 103 | void returnsNullOnBlankRanges() { 104 | 105 | Country country = Country.fromLanguageRanges(" "); 106 | 107 | assertThat(country).isNull(); 108 | 109 | } 110 | 111 | @Test 112 | void failsOnInvalidRanges() { 113 | 114 | assertThatThrownBy(() -> Country.fromLanguageRanges("invalid")) 115 | .isInstanceOf(IllegalArgumentException.class) 116 | .hasMessage("Invalid country code"); 117 | 118 | } 119 | 120 | @Test 121 | void failsOnLocaleWithoutCountryCode() { 122 | 123 | assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de"))) 124 | .isInstanceOf(IllegalArgumentException.class) 125 | .hasMessage("Invalid locale"); 126 | 127 | } 128 | 129 | @Test 130 | void setLocaleFailsOnNullLocale() { 131 | 132 | assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(null)) 133 | .isInstanceOf(IllegalArgumentException.class) 134 | .hasMessage("Invalid locale"); 135 | 136 | } 137 | 138 | @Test 139 | void setLocaleFailsOnNullCountryCode() { 140 | 141 | assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(Locale.forLanguageTag( 142 | "de"))).isInstanceOf(IllegalArgumentException.class).hasMessage("Invalid locale"); 143 | 144 | } 145 | 146 | @Test 147 | void setLocaleFailsOnEmptyCountryCode() { 148 | 149 | assertThatThrownBy(() -> new Country(Locale.forLanguageTag("de")).setLocale(Locale.forLanguageTag( 150 | "de"))).isInstanceOf(IllegalArgumentException.class).hasMessage("Invalid locale"); 151 | 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at mail@daniel-heid.de. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [https://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: https://contributor-covenant.org 92 | 93 | [version]: https://contributor-covenant.org/version/1/4/ 94 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/MatomoRequestTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | class MatomoRequestTest { 9 | 10 | private MatomoRequest request = new MatomoRequest(); 11 | 12 | @Test 13 | void returnsEmptyListWhenCustomTrackingParametersDoesNotContainKey() { 14 | 15 | request.setCustomTrackingParameter("foo", "bar"); 16 | 17 | assertThat(request.getCustomTrackingParameter("baz")).isNull(); 18 | assertThat(request.getAdditionalParameters()).isNotEmpty(); 19 | assertThat(request.getCustomTrackingParameter("foo")).isEqualTo("bar"); 20 | } 21 | 22 | @Test 23 | void getPageCustomVariableReturnsNullIfPageCustomVariablesIsNull() { 24 | assertThat(request.getPageCustomVariable("foo")).isNull(); 25 | } 26 | 27 | @Test 28 | void getPageCustomVariableReturnsValueIfPageCustomVariablesIsNotNull() { 29 | request.setPageCustomVariable("foo", "bar"); 30 | assertThat(request.getPageCustomVariable("foo")).isEqualTo("bar"); 31 | } 32 | 33 | @Test 34 | void setPageCustomVariableRequiresNonNullKey() { 35 | assertThatThrownBy(() -> request.setPageCustomVariable(null, "bar")).isInstanceOf( 36 | NullPointerException.class); 37 | } 38 | 39 | @Test 40 | void setPageCustomVariableDoesNothingIfValueIsNull() { 41 | request.setPageCustomVariable("foo", null); 42 | assertThat(request.getPageCustomVariable("foo")).isNull(); 43 | } 44 | 45 | @Test 46 | void setPageCustomVariableRemovesValueIfValueIsNull() { 47 | request.setPageCustomVariable("foo", "bar"); 48 | request.setPageCustomVariable("foo", null); 49 | assertThat(request.getPageCustomVariable("foo")).isNull(); 50 | } 51 | 52 | @Test 53 | void setPageCustomVariableAddsCustomVariableIfValueIsNotNull() { 54 | request.setPageCustomVariable("foo", "bar"); 55 | assertThat(request.getPageCustomVariable("foo")).isEqualTo("bar"); 56 | } 57 | 58 | @Test 59 | void setPageCustomVariableDoesNothingIfCustomVariableParameterIsNullAndIndexIsPositive() { 60 | request.setPageCustomVariable(null, 1); 61 | assertThat(request.getPageCustomVariable(1)).isNull(); 62 | } 63 | 64 | @Test 65 | void setPageCustomVariableInitializesPageCustomVariablesIfCustomVariableParameterIsNullAndIndexIsPositive() { 66 | request.setPageCustomVariable(new CustomVariable("key", "value"), 1); 67 | assertThat(request.getPageCustomVariables()).isNotNull(); 68 | } 69 | 70 | @Test 71 | void setUserCustomVariableDoesNothingIfValueIsNull() { 72 | request.setUserCustomVariable("foo", null); 73 | assertThat(request.getUserCustomVariable("foo")).isNull(); 74 | } 75 | 76 | @Test 77 | void setUserCustomVariableRemovesValueIfValueIsNull() { 78 | request.setUserCustomVariable("foo", "bar"); 79 | request.setUserCustomVariable("foo", null); 80 | assertThat(request.getUserCustomVariable("foo")).isNull(); 81 | } 82 | 83 | @Test 84 | void setVisitCustomVariableDoesNothingIfCustomVariableParameterIsNullAndIndexIsPositive() { 85 | request.setVisitCustomVariable(null, 1); 86 | assertThat(request.getVisitCustomVariable(1)).isNull(); 87 | } 88 | 89 | @Test 90 | void setVisitCustomVariableInitializesVisitCustomVariablesIfCustomVariableParameterIsNullAndIndexIsPositive() { 91 | request.setVisitCustomVariable(new CustomVariable("key", "value"), 1); 92 | assertThat(request.getVisitCustomVariables()).isNotNull(); 93 | } 94 | 95 | @Test 96 | void setsCustomParameter() { 97 | request.setParameter("foo", 1); 98 | assertThat(request.getCustomTrackingParameter("foo")).isEqualTo(1); 99 | } 100 | 101 | @Test 102 | void failsToSetCustomParameterIfKeyIsNull() { 103 | assertThatThrownBy(() -> request.setParameter( 104 | null, 105 | 1 106 | )).isInstanceOf(NullPointerException.class); 107 | } 108 | 109 | @Test 110 | void doesNothingWhenSettingCustomParameterIfValueIsNull() { 111 | request.setParameter("foo", null); 112 | assertThat(request.getAdditionalParameters()).isNull(); 113 | } 114 | 115 | @Test 116 | void removesCustomParameter() { 117 | request.setParameter("foo", 1); 118 | request.setParameter("foo", null); 119 | assertThat(request.getAdditionalParameters()).isEmpty(); 120 | } 121 | 122 | @Test 123 | void setsDeviceResolutionString() { 124 | request.setDeviceResolution("1920x1080"); 125 | assertThat(request.getDeviceResolution().toString()).isEqualTo("1920x1080"); 126 | } 127 | 128 | @Test 129 | void failsIfSetParameterParameterNameIsBlank() { 130 | assertThatThrownBy(() -> request.setParameter(" ", "bar")).isInstanceOf( 131 | IllegalArgumentException.class); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /core/src/main/java/org/matomo/java/tracking/parameters/VisitorId.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.parameters; 9 | 10 | import edu.umd.cs.findbugs.annotations.Nullable; 11 | import java.security.SecureRandom; 12 | import java.util.Random; 13 | import java.util.UUID; 14 | import java.util.regex.Pattern; 15 | import lombok.NonNull; 16 | 17 | /** 18 | * The unique visitor ID, must be a 16 characters hexadecimal string. Every unique visitor must be assigned a different 19 | * ID and this ID must not change after it is assigned. If this value is not set Matomo will still track visits, but the 20 | * unique visitors metric might be less accurate. 21 | */ 22 | public class VisitorId { 23 | 24 | private static final Random RANDOM = new SecureRandom(); 25 | private static final Pattern HEX_DIGITS = Pattern.compile("[0-9a-fA-F]+"); 26 | 27 | private final byte[] representation = new byte[8]; 28 | 29 | /** 30 | * Static factory to generate a random visitor id. 31 | * 32 | *

Please consider creating a fixed id for each visitor by getting a hash code from e.g. the username and 33 | * using {@link #fromHash(long)} or {@link #fromString(String)} instead of using this method. 34 | * 35 | * @return A randomly generated visitor id 36 | */ 37 | @edu.umd.cs.findbugs.annotations.NonNull 38 | public static VisitorId random() { 39 | VisitorId visitorId = new VisitorId(); 40 | RANDOM.nextBytes(visitorId.representation); 41 | return visitorId; 42 | } 43 | 44 | /** 45 | * Creates always the same visitor id for the given input. 46 | * 47 | *

You can use e.g. {@link Object#hashCode()} to generate a hash code for an object, e.g. a username 48 | * string as input. 49 | * 50 | * @param hash A number (a hash code) to create the visitor id from 51 | * @return Always the same visitor id for the same input 52 | */ 53 | @edu.umd.cs.findbugs.annotations.NonNull 54 | public static VisitorId fromHash(long hash) { 55 | VisitorId visitorId = new VisitorId(); 56 | long remainingHash = hash; 57 | for (int i = visitorId.representation.length - 1; i >= 0; i--) { 58 | visitorId.representation[i] = (byte) (remainingHash & 0xFF); 59 | remainingHash >>= Byte.SIZE; 60 | } 61 | return visitorId; 62 | } 63 | 64 | /** 65 | * Creates a visitor id from a UUID. 66 | * 67 | *

Uses the most significant bits of the UUID to create the visitor id. 68 | * 69 | * @param uuid A UUID to create the visitor id from 70 | * @return The visitor id for the given UUID 71 | */ 72 | @edu.umd.cs.findbugs.annotations.NonNull 73 | public static VisitorId fromUUID(@NonNull UUID uuid) { 74 | return fromHash(uuid.getMostSignificantBits()); 75 | } 76 | 77 | /** 78 | * Creates a visitor id from a hexadecimal string. 79 | * 80 | *

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

82 | * 83 | * @param inputHex A hexadecimal string to create the visitor id from 84 | * @return The visitor id for the given input 85 | */ 86 | @edu.umd.cs.findbugs.annotations.NonNull 87 | public static VisitorId fromHex(@NonNull String inputHex) { 88 | if (inputHex.trim().isEmpty()) { 89 | throw new IllegalArgumentException("Hex string must not be null or empty"); 90 | } 91 | if (inputHex.length() > 16) { 92 | throw new IllegalArgumentException("Hex string must not be longer than 16 characters"); 93 | } 94 | if (!HEX_DIGITS.matcher(inputHex).matches()) { 95 | throw new IllegalArgumentException("Input must be a valid hex string"); 96 | } 97 | VisitorId visitorId = new VisitorId(); 98 | for (int charIndex = inputHex.length() - 1, representationIndex = 99 | visitorId.representation.length - 1; 100 | charIndex >= 0; 101 | charIndex -= 2, representationIndex--) { 102 | String hex = inputHex.substring(Math.max(0, charIndex - 1), charIndex + 1); 103 | try { 104 | visitorId.representation[representationIndex] = (byte) Integer.parseInt(hex, 16); 105 | } catch (NumberFormatException e) { 106 | throw new IllegalArgumentException("Input must be a valid hex string", e); 107 | } 108 | } 109 | return visitorId; 110 | } 111 | 112 | /** 113 | * Creates a visitor id from a string. The string will be hashed to create the visitor id. 114 | * 115 | * @param str A string to create the visitor id from 116 | * @return The visitor id for the given string or null if the string is null or empty 117 | */ 118 | @Nullable 119 | public static VisitorId fromString(@Nullable String str) { 120 | if (str == null || str.trim().isEmpty()) { 121 | return null; 122 | } 123 | return fromHash(str.hashCode()); 124 | } 125 | 126 | @Override 127 | @edu.umd.cs.findbugs.annotations.NonNull 128 | public String toString() { 129 | return Hex.fromBytes(representation); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /spring/src/main/java/org/matomo/java/tracking/spring/MatomoTrackerProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.spring; 9 | 10 | import java.time.Duration; 11 | import lombok.Getter; 12 | import lombok.Setter; 13 | import org.matomo.java.tracking.TrackerConfiguration; 14 | import org.springframework.boot.context.properties.ConfigurationProperties; 15 | 16 | /** 17 | * Configuration properties for the Matomo Tracker. 18 | * 19 | *

These properties can be configured in the application.properties file. For example: 20 | *

 21 |  *   matomo.tracker.api-endpoint=https://your-matomo-domain.example/matomo.php
 22 |  *   matomo.tracker.default-site-id=1
 23 |  *   matomo.tracker.default-auth-token=1234567890abcdef1234567890abcdef
 24 |  *   matomo.tracker.enabled=true
 25 |  *   matomo.tracker.connect-timeout=10s
 26 |  *   matomo.tracker.socket-timeout=30s
 27 |  *   matomo.tracker.proxy-host=proxy.example.com
 28 |  *   matomo.tracker.proxy-port=8080
 29 |  *   matomo.tracker.proxy-username=proxyuser
 30 |  *   matomo.tracker.proxy-password=proxypassword
 31 |  *   matomo.tracker.user-agent=MatomoJavaClient
 32 |  *   matomo.tracker.log-failed-tracking=true
 33 |  *   matomo.tracker.disable-ssl-cert-validation=true
 34 |  *   matomo.tracker.disable-ssl-host-validation=true
 35 |  *   matomo.tracker.thread-pool-size=2
 36 |  * 
37 | * 38 | * @see MatomoTrackerAutoConfiguration 39 | * @see TrackerConfiguration 40 | */ 41 | @ConfigurationProperties(prefix = "matomo.tracker") 42 | @Getter 43 | @Setter 44 | public class MatomoTrackerProperties { 45 | 46 | /** 47 | * The Matomo Tracking HTTP API endpoint, for example https://your-matomo-domain.example/matomo.php 48 | */ 49 | private String apiEndpoint; 50 | 51 | /** 52 | * The default ID of the website that will be used if not specified explicitly. 53 | */ 54 | private Integer defaultSiteId; 55 | 56 | /** 57 | * The authorization token (parameter token_auth) to use if not specified explicitly. 58 | */ 59 | private String defaultAuthToken; 60 | 61 | /** 62 | * Allows to stop the tracker to send requests to the Matomo endpoint. 63 | */ 64 | private Boolean enabled = true; 65 | 66 | /** 67 | * The timeout until a connection is established. 68 | * 69 | *

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

71 | * 72 | *

Default: 10 seconds

73 | */ 74 | private Duration connectTimeout = Duration.ofSeconds(5L); 75 | 76 | /** 77 | * The socket timeout ({@code SO_TIMEOUT}), which is the timeout for waiting for data or, put differently, a maximum 78 | * period inactivity between two consecutive data packets. 79 | * 80 | *

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

82 | * 83 | *

Default: 30 seconds

84 | */ 85 | private Duration socketTimeout = Duration.ofSeconds(5L); 86 | 87 | /** 88 | * The hostname or IP address of an optional HTTP proxy. {@code proxyPort} must be configured as well 89 | */ 90 | private String proxyHost; 91 | 92 | /** 93 | * The port of an HTTP proxy. {@code proxyHost} must be configured as well. 94 | */ 95 | private Integer proxyPort; 96 | 97 | /** 98 | * If the HTTP proxy requires a username for basic authentication, it can be configured here. Proxy host, port and 99 | * password must also be set. 100 | */ 101 | private String proxyUsername; 102 | 103 | /** 104 | * The corresponding password for the basic auth proxy user. The proxy host, port and username must be set as well. 105 | */ 106 | private String proxyPassword; 107 | 108 | /** 109 | * A custom user agent to be set. Defaults to "MatomoJavaClient" 110 | */ 111 | private String userAgent = "MatomoJavaClient"; 112 | 113 | /** 114 | * Logs if the Matomo Tracking API endpoint responds with an erroneous HTTP code. Defaults to 115 | * false. 116 | */ 117 | private Boolean logFailedTracking; 118 | 119 | /** 120 | * Disables SSL certificate validation. This is useful for testing with self-signed certificates. 121 | * Do not use in production environments. Defaults to false. 122 | * 123 | *

Attention: This slows down performance 124 | 125 | * @see #disableSslHostVerification 126 | */ 127 | private Boolean disableSslCertValidation; 128 | 129 | /** 130 | * Disables SSL host verification. This is useful for testing with self-signed certificates. Do 131 | * not use in production environments. Defaults to false. 132 | * 133 | *

Attention: This slows down performance 134 | * 135 | * @see #disableSslCertValidation 136 | */ 137 | private Boolean disableSslHostVerification; 138 | 139 | /** 140 | * The thread pool size for the async sender. Defaults to 2. 141 | * 142 | *

Attention: If you use this library in a web application, make sure that this thread pool 143 | * does not exceed the thread pool of the web application. Otherwise, you might run into 144 | * problems. 145 | */ 146 | private Integer threadPoolSize = 2; 147 | 148 | } 149 | -------------------------------------------------------------------------------- /spring/src/main/java/org/matomo/java/tracking/spring/MatomoTrackerAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Matomo Java Tracker 3 | * 4 | * @link https://github.com/matomo/matomo-java-tracker 5 | * @license https://github.com/matomo/matomo-java-tracker/blob/master/LICENSE BSD-3 Clause 6 | */ 7 | 8 | package org.matomo.java.tracking.spring; 9 | 10 | import java.net.URI; 11 | import java.util.List; 12 | import org.matomo.java.tracking.MatomoTracker; 13 | import org.matomo.java.tracking.TrackerConfiguration; 14 | import org.matomo.java.tracking.servlet.MatomoTrackerFilter; 15 | import org.springframework.boot.autoconfigure.AutoConfiguration; 16 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 17 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 18 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 19 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.lang.NonNull; 22 | 23 | /** 24 | * {@link AutoConfiguration Auto configuration} for Matomo Tracker. 25 | * 26 | * @see MatomoTrackerProperties 27 | * @see TrackerConfiguration 28 | * @see MatomoTracker 29 | */ 30 | @AutoConfiguration 31 | @ConditionalOnProperty(prefix = "matomo.tracker", name = "api-endpoint") 32 | @EnableConfigurationProperties(MatomoTrackerProperties.class) 33 | public class MatomoTrackerAutoConfiguration { 34 | 35 | /** 36 | * Creates a {@link TrackerConfiguration.TrackerConfigurationBuilder} and applies all 37 | * {@link TrackerConfigurationBuilderCustomizer}s. Can be overridden by custom beans. 38 | * 39 | * @param customizers the customizers to apply to the builder instance (never {@code null}) 40 | * @return the {@link TrackerConfiguration.TrackerConfigurationBuilder} instance (never {@code null}) 41 | * @see TrackerConfiguration#builder() 42 | */ 43 | @Bean 44 | @ConditionalOnMissingBean 45 | @NonNull 46 | public TrackerConfiguration.TrackerConfigurationBuilder trackerConfigurationBuilder( 47 | @NonNull List customizers 48 | ) { 49 | TrackerConfiguration.TrackerConfigurationBuilder builder = TrackerConfiguration.builder(); 50 | customizers.forEach(customizer -> customizer.customize(builder)); 51 | return builder; 52 | } 53 | 54 | /** 55 | * Creates a {@link TrackerConfiguration} instance based on the current configuration. Can be 56 | * overridden by custom beans. 57 | * 58 | *

If you define your own {@link TrackerConfiguration} bean, please don't forget to set the 59 | * API endpoint. 60 | * 61 | * @param builder the {@link TrackerConfiguration.TrackerConfigurationBuilder} instance (never {@code null}) 62 | * @return the {@link TrackerConfiguration} instance (never {@code null}) 63 | * @see TrackerConfiguration#builder() 64 | * @see TrackerConfiguration.TrackerConfigurationBuilder#apiEndpoint(URI) 65 | */ 66 | @Bean 67 | @ConditionalOnMissingBean 68 | @NonNull 69 | public TrackerConfiguration trackerConfiguration( 70 | TrackerConfiguration.TrackerConfigurationBuilder builder 71 | ) { 72 | return builder.build(); 73 | } 74 | 75 | /** 76 | * Configures the {@link TrackerConfiguration.TrackerConfigurationBuilder} with the properties from 77 | * {@link MatomoTrackerProperties}. 78 | * 79 | * @param properties the {@link MatomoTrackerProperties} instance (never {@code null}) 80 | * @return the {@link StandardTrackerConfigurationBuilderCustomizer} instance (never {@code null}) 81 | * @see MatomoTrackerProperties 82 | * @see TrackerConfiguration.TrackerConfigurationBuilder 83 | */ 84 | @Bean 85 | @NonNull 86 | public StandardTrackerConfigurationBuilderCustomizer standardTrackerConfigurationBuilderCustomizer( 87 | @NonNull MatomoTrackerProperties properties 88 | ) { 89 | return new StandardTrackerConfigurationBuilderCustomizer(properties); 90 | } 91 | 92 | /** 93 | * A {@link MatomoTracker} instance based on the current configuration. Only created if a bean of the same type is not 94 | * already configured. 95 | * 96 | * @param trackerConfiguration the {@link TrackerConfiguration} instance (never {@code null}) 97 | * @return the {@link MatomoTracker} instance (never {@code null}) 98 | * @see MatomoTracker 99 | * @see TrackerConfiguration 100 | */ 101 | @Bean 102 | @ConditionalOnMissingBean 103 | @NonNull 104 | public MatomoTracker matomoTracker(@NonNull TrackerConfiguration trackerConfiguration) { 105 | return new MatomoTracker(trackerConfiguration); 106 | } 107 | 108 | /** 109 | * A {@link FilterRegistrationBean} for the {@link MatomoTrackerFilter}. 110 | * 111 | *

Only created if a bean of the same type is not already configured. The filter is only registered if 112 | * {@code matomo.tracker.filter.enabled} is set to {@code true}. 113 | * 114 | * @param matomoTracker the {@link MatomoTracker} instance (never {@code null}) 115 | * @return the {@link FilterRegistrationBean} instance (never {@code null}) 116 | */ 117 | @Bean 118 | @ConditionalOnProperty(value = "matomo.tracker.filter.enabled", havingValue = "true") 119 | @NonNull 120 | public FilterRegistrationBean matomoTrackerSpringFilter( 121 | @NonNull MatomoTracker matomoTracker 122 | ) { 123 | return new FilterRegistrationBean<>(new MatomoTrackerFilter(matomoTracker)); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | mail@daniel-heid.de. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /core/src/test/java/org/matomo/java/tracking/parameters/VisitorIdTest.java: -------------------------------------------------------------------------------- 1 | package org.matomo.java.tracking.parameters; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import java.util.stream.Stream; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.Arguments; 10 | import org.junit.jupiter.params.provider.MethodSource; 11 | import org.junit.jupiter.params.provider.ValueSource; 12 | 13 | class VisitorIdTest { 14 | 15 | private static Stream validHexStrings() { 16 | return Stream.of( 17 | Arguments.of("0", "0000000000000000"), 18 | Arguments.of("0000", "0000000000000000"), 19 | Arguments.of("1", "0000000000000001"), 20 | Arguments.of("a", "000000000000000a"), 21 | Arguments.of("1a", "000000000000001a"), 22 | Arguments.of("01a", "000000000000001a"), 23 | Arguments.of("1a2b", "0000000000001a2b"), 24 | Arguments.of("1a2b3c", "00000000001a2b3c"), 25 | Arguments.of("1a2b3c4d", "000000001a2b3c4d"), 26 | Arguments.of("1a2b3c4d5e", "0000001a2b3c4d5e"), 27 | Arguments.of("1A2B3C4D5E", "0000001a2b3c4d5e"), 28 | Arguments.of("1a2b3c4d5e6f", "00001a2b3c4d5e6f"), 29 | Arguments.of("1a2b3c4d5e6f7a", "001a2b3c4d5e6f7a") 30 | ); 31 | } 32 | 33 | @Test 34 | void hasCorrectFormat() { 35 | 36 | VisitorId visitorId = VisitorId.random(); 37 | 38 | assertThat(visitorId.toString()).matches("^[a-z0-9]{16}$"); 39 | 40 | } 41 | 42 | @Test 43 | void createsRandomVisitorId() { 44 | 45 | VisitorId first = VisitorId.random(); 46 | VisitorId second = VisitorId.random(); 47 | 48 | assertThat(first).doesNotHaveToString(second.toString()); 49 | 50 | } 51 | 52 | @Test 53 | void fixedVisitorIdForLongHash() { 54 | 55 | VisitorId visitorId = VisitorId.fromHash(987654321098765432L); 56 | 57 | assertThat(visitorId).hasToString("0db4da5f49f8b478"); 58 | 59 | } 60 | 61 | @Test 62 | void fixedVisitorIdForIntHash() { 63 | 64 | VisitorId visitorId = VisitorId.fromHash(777777777); 65 | 66 | assertThat(visitorId).hasToString("000000002e5bf271"); 67 | 68 | } 69 | 70 | @Test 71 | void sameVisitorIdForSameHash() { 72 | 73 | VisitorId first = VisitorId.fromHash(1234567890L); 74 | VisitorId second = VisitorId.fromHash(1234567890); 75 | 76 | assertThat(first).hasToString(second.toString()); 77 | 78 | } 79 | 80 | @Test 81 | void alwaysTheSameToString() { 82 | 83 | VisitorId visitorId = VisitorId.random(); 84 | 85 | assertThat(visitorId).hasToString(visitorId.toString()); 86 | 87 | } 88 | 89 | @Test 90 | void createsVisitorIdFrom16CharacterHex() { 91 | 92 | VisitorId visitorId = VisitorId.fromHex("1234567890abcdef"); 93 | 94 | assertThat(visitorId).hasToString("1234567890abcdef"); 95 | 96 | } 97 | 98 | @Test 99 | void createsVisitorIdFrom1CharacterHex() { 100 | 101 | VisitorId visitorId = VisitorId.fromHex("a"); 102 | 103 | assertThat(visitorId).hasToString("000000000000000a"); 104 | 105 | } 106 | 107 | @Test 108 | void createsVisitorIdFrom2CharacterHex() { 109 | 110 | VisitorId visitorId = VisitorId.fromHex("12"); 111 | 112 | assertThat(visitorId).hasToString("0000000000000012"); 113 | 114 | } 115 | 116 | @Test 117 | void failsOnInvalidHexString() { 118 | 119 | assertThatThrownBy(() -> VisitorId.fromHex("invalid123456789")) 120 | .isInstanceOf(IllegalArgumentException.class) 121 | .hasMessage("Input must be a valid hex string"); 122 | 123 | } 124 | 125 | @ParameterizedTest 126 | @ValueSource(strings = 127 | {"g", "gh", "ghi", "ghij", "ghijk", "ghijkl", "ghijklm", "ghijklmn", "ghijklmn", "-1"}) 128 | void failsOnInvalidHexString(String hex) { 129 | assertThatThrownBy(() -> VisitorId.fromHex(hex)) 130 | .isInstanceOf(IllegalArgumentException.class) 131 | .hasMessage("Input must be a valid hex string"); 132 | } 133 | 134 | @ParameterizedTest 135 | @MethodSource("validHexStrings") 136 | void createsVisitorIdFromHex( 137 | String hex, String expected 138 | ) { 139 | 140 | VisitorId visitorId = VisitorId.fromHex(hex); 141 | 142 | assertThat(visitorId).hasToString(expected); 143 | 144 | } 145 | 146 | @ParameterizedTest 147 | @ValueSource(strings = {"", " "}) 148 | void failsOnEmptyStrings(String hex) { 149 | assertThatThrownBy(() -> VisitorId.fromHex(hex)) 150 | .isInstanceOf(IllegalArgumentException.class) 151 | .hasMessage("Hex string must not be null or empty"); 152 | } 153 | 154 | @ParameterizedTest 155 | @ValueSource(strings = {"1234567890abcdefg", "1234567890abcdeff"}) 156 | void failsOnInvalidHexStringLength(String hex) { 157 | assertThatThrownBy(() -> VisitorId.fromHex(hex)) 158 | .isInstanceOf(IllegalArgumentException.class) 159 | .hasMessage("Hex string must not be longer than 16 characters"); 160 | } 161 | 162 | @Test 163 | void createsVisitorIdFromUUID() { 164 | 165 | VisitorId visitorId = VisitorId.fromUUID( 166 | java.util.UUID.fromString("12345678-90ab-cdef-1234-567890abcdef") 167 | ); 168 | 169 | assertThat(visitorId).hasToString("1234567890abcdef"); 170 | 171 | } 172 | 173 | @Test 174 | void failsOnNullUUID() { 175 | assertThatThrownBy(() -> VisitorId.fromUUID(null)) 176 | .isInstanceOf(NullPointerException.class) 177 | .hasMessage("uuid is marked non-null but is null"); 178 | } 179 | 180 | @Test 181 | void createsVisitorIdFromString() { 182 | 183 | VisitorId visitorId = VisitorId.fromString("test"); 184 | 185 | assertThat(visitorId).hasToString("0000000000364492"); 186 | 187 | } 188 | 189 | } 190 | --------------------------------------------------------------------------------