diff --git a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java index bd56ad465163..880a72d0a6c7 100644 --- a/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/BitstreamServiceImpl.java @@ -12,8 +12,10 @@ import java.sql.SQLException; import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; +import java.util.stream.Collectors; import jakarta.annotation.Nullable; import org.apache.commons.collections4.CollectionUtils; @@ -496,4 +498,14 @@ public List getNotReferencedBitstreams(Context context) throws SQLExc public Long getLastModified(Bitstream bitstream) throws IOException { return bitstreamStorageService.getLastModified(bitstream); } + + @Override + public boolean isInBundle(Bitstream bitstream, java.util.Collection bundleNames) throws SQLException { + Set bundles = + bitstream.getBundles() + .stream() + .map(Bundle::getName) + .collect(Collectors.toSet()); + return bundleNames.stream().anyMatch(bundles::contains); + } } diff --git a/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java b/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java index c22428f11a96..2af76166492b 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/BitstreamService.java @@ -235,4 +235,14 @@ public InputStream retrieve(Context context, Bitstream bitstream) */ @Nullable Long getLastModified(Bitstream bitstream) throws IOException; + + /** + * Checks if the given bitstream is inside one of the bundle + * + * @param bitstream bitstream to verify + * @param bundleNames names of the bundles to serch for + * @return true if is in one of the bundles, false otherwise + * @throws SQLException + */ + boolean isInBundle(Bitstream bitstream, java.util.Collection bundleNames) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/google/GoogleAsyncEventListener.java b/dspace-api/src/main/java/org/dspace/google/GoogleAsyncEventListener.java index f5b27fcbd496..54fbd4fd2e3d 100644 --- a/dspace-api/src/main/java/org/dspace/google/GoogleAsyncEventListener.java +++ b/dspace-api/src/main/java/org/dspace/google/GoogleAsyncEventListener.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @@ -23,8 +24,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.content.Bitstream; -import org.dspace.content.Bundle; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.google.client.GoogleAnalyticsClient; @@ -57,6 +58,9 @@ public class GoogleAsyncEventListener extends AbstractUsageEventListener { @Autowired private ClientInfoService clientInfoService; + @Autowired + private BitstreamService bitstreamService; + @Autowired private List googleAnalyticsClients; @@ -181,25 +185,35 @@ private String getDocumentPath(HttpServletRequest request) { */ private boolean isContentBitstream(UsageEvent usageEvent) { // check if event is a VIEW event and object is a Bitstream - if (usageEvent.getAction() == UsageEvent.Action.VIEW - && usageEvent.getObject().getType() == Constants.BITSTREAM) { - // check if bitstream belongs to a configured bundle - List allowedBundles = List.of(configurationService - .getArrayProperty("google-analytics.bundles", new String[]{Constants.CONTENT_BUNDLE_NAME})); - if (allowedBundles.contains("none")) { - // GA events for bitstream views were turned off in config - return false; - } - List bitstreamBundles; - try { - bitstreamBundles = ((Bitstream) usageEvent.getObject()) - .getBundles().stream().map(Bundle::getName).collect(Collectors.toList()); - } catch (SQLException e) { - throw new RuntimeException(e.getMessage(), e); - } - return allowedBundles.stream().anyMatch(bitstreamBundles::contains); + if (!isBitstreamView(usageEvent)) { + return false; + } + // check if bitstream belongs to a configured bundle + Set allowedBundles = + Set.of( + configurationService.getArrayProperty( + "google-analytics.bundles", + new String[]{Constants.CONTENT_BUNDLE_NAME} + ) + ); + if (allowedBundles.contains("none")) { + // GA events for bitstream views were turned off in config + return false; + } + return isInBundle((Bitstream) usageEvent.getObject(), allowedBundles); + } + + private boolean isInBundle(Bitstream bitstream, Set allowedBundles) { + try { + return this.bitstreamService.isInBundle(bitstream, allowedBundles); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); } - return false; + } + + private boolean isBitstreamView(UsageEvent usageEvent) { + return usageEvent.getAction() == UsageEvent.Action.VIEW + && usageEvent.getObject().getType() == Constants.BITSTREAM; } private boolean isGoogleAnalyticsKeyNotConfigured() { diff --git a/dspace-api/src/main/java/org/dspace/matomo/MatomoEventListener.java b/dspace-api/src/main/java/org/dspace/matomo/MatomoEventListener.java index eaf0fdd1166e..1a4f66e4567d 100644 --- a/dspace-api/src/main/java/org/dspace/matomo/MatomoEventListener.java +++ b/dspace-api/src/main/java/org/dspace/matomo/MatomoEventListener.java @@ -7,10 +7,15 @@ */ package org.dspace.matomo; +import java.sql.SQLException; import java.util.List; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.dspace.content.Bitstream; +import org.dspace.content.service.BitstreamService; +import org.dspace.core.Constants; import org.dspace.services.ConfigurationService; import org.dspace.services.model.Event; import org.dspace.usage.AbstractUsageEventListener; @@ -28,14 +33,17 @@ public class MatomoEventListener extends AbstractUsageEventListener { private static final Logger log = LogManager.getLogger(MatomoEventListener.class); private final ConfigurationService configurationService; + private final BitstreamService bitstreamService; private final List matomoUsageEventHandlers; public MatomoEventListener( @Autowired List matomoUsageEventHandlers, - @Autowired ConfigurationService configurationService + @Autowired ConfigurationService configurationService, + @Autowired BitstreamService bitstreamService ) { this.matomoUsageEventHandlers = matomoUsageEventHandlers; this.configurationService = configurationService; + this.bitstreamService = bitstreamService; } @Override @@ -49,6 +57,10 @@ public void receiveEvent(Event event) { return; } + if (!isContentBitstream(usageEvent)) { + return; + } + if (log.isDebugEnabled()) { log.debug("Usage event received {}", event.getName()); } @@ -64,4 +76,48 @@ private boolean matomoEnabled() { return this.configurationService.getBooleanProperty("matomo.enabled", false); } + /** + * Verifies if the usage event is a content bitstream view event, by checking if: + *
    + *
  • the usage event is a view event
  • + *
  • the object of the usage event is a bitstream
  • + *
  • the bitstream belongs to one of the configured bundles (fallback: ORIGINAL bundle)
  • + *
+ */ + private boolean isContentBitstream(UsageEvent usageEvent) { + // check if event is a VIEW event and object is a Bitstream + if (!isBitstreamView(usageEvent)) { + return false; + } + // check if bitstream belongs to a configured bundle + Set allowedBundles = getTrackedBundles(); + if (allowedBundles.contains("none")) { + // events for bitstream views were turned off in config + return false; + } + return isInBundle(((Bitstream) usageEvent.getObject()), allowedBundles); + } + + private Set getTrackedBundles() { + return Set.of( + configurationService.getArrayProperty( + "matomo.track.bundles", + new String[] {Constants.CONTENT_BUNDLE_NAME} + ) + ); + } + + protected boolean isInBundle(Bitstream bitstream, Set allowedBundles) { + try { + return this.bitstreamService.isInBundle(bitstream, allowedBundles); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + private boolean isBitstreamView(UsageEvent usageEvent) { + return usageEvent.getAction() == UsageEvent.Action.VIEW + && usageEvent.getObject().getType() == Constants.BITSTREAM; + } + } diff --git a/dspace-api/src/test/java/org/dspace/matomo/MatomoEventListenerTest.java b/dspace-api/src/test/java/org/dspace/matomo/MatomoEventListenerTest.java index 5c1ffc88c01a..f38f8ccfbed0 100644 --- a/dspace-api/src/test/java/org/dspace/matomo/MatomoEventListenerTest.java +++ b/dspace-api/src/test/java/org/dspace/matomo/MatomoEventListenerTest.java @@ -7,9 +7,15 @@ */ package org.dspace.matomo; +import java.sql.SQLException; import java.util.List; +import java.util.Set; import org.dspace.AbstractUnitTest; +import org.dspace.content.Bitstream; +import org.dspace.content.Item; +import org.dspace.content.service.BitstreamService; +import org.dspace.core.Constants; import org.dspace.services.ConfigurationService; import org.dspace.usage.UsageEvent; import org.junit.Before; @@ -25,13 +31,15 @@ public class MatomoEventListenerTest extends AbstractUnitTest { MatomoSyncEventHandler matomoHandler2; @Mock ConfigurationService configurationService; + @Mock + BitstreamService bitstreamService; MatomoEventListener matomoEventListener; @Before public void setUp() throws Exception { matomoEventListener = - new MatomoEventListener(List.of(matomoHandler1, matomoHandler2), configurationService); + new MatomoEventListener(List.of(matomoHandler1, matomoHandler2), configurationService, bitstreamService); } @Test @@ -46,17 +54,60 @@ public void testDisabledMatomo() { @Test - public void testHandleEvent() { + public void testDontHandleGenericViewEventWithMatomoEnabled() { UsageEvent event = Mockito.mock(UsageEvent.class); + Mockito.when(event.getAction()).thenReturn(UsageEvent.Action.VIEW); + Mockito.when(event.getObject()).thenReturn(Mockito.spy(Item.class)); Mockito.when(configurationService.getBooleanProperty("matomo.enabled", false)) .thenReturn(true); matomoEventListener.receiveEvent(event); + Mockito.verifyNoInteractions(matomoHandler1); + Mockito.verifyNoInteractions(matomoHandler2); + } + + + @Test + public void testHandleBitstreamViewEvent() throws SQLException { + UsageEvent event = Mockito.mock(UsageEvent.class); + Mockito.when(event.getAction()).thenReturn(UsageEvent.Action.VIEW); + + Bitstream bitstream = Mockito.spy(Bitstream.class); + Mockito.when(bitstreamService.isInBundle(Mockito.eq(bitstream), Mockito.eq(Set.of(Constants.CONTENT_BUNDLE_NAME)))) + .thenReturn(true); + + Mockito.when(event.getObject()).thenReturn(bitstream); + + Mockito.when(configurationService.getBooleanProperty(Mockito.eq("matomo.enabled"), Mockito.eq(false))) + .thenReturn(true); + Mockito.when(configurationService.getArrayProperty(Mockito.eq("matomo.track.bundles"), Mockito.any())) + .thenReturn(new String[] { }); + + matomoEventListener.receiveEvent(event); + + Mockito.verifyNoInteractions(matomoHandler1); + Mockito.verifyNoInteractions(matomoHandler2); + + // none bundle, will skip processing + Mockito.when(configurationService.getArrayProperty(Mockito.eq("matomo.track.bundles"), Mockito.any())) + .thenReturn(new String[] {"none"}); + + matomoEventListener.receiveEvent(event); + + Mockito.verifyNoMoreInteractions(matomoHandler1); + Mockito.verifyNoMoreInteractions(matomoHandler2); + + // default ( original bundle only ) then proceed with the invocation + Mockito.when(configurationService.getArrayProperty(Mockito.eq("matomo.track.bundles"), Mockito.any())) + .thenReturn(new String[] { Constants.CONTENT_BUNDLE_NAME }); + + matomoEventListener.receiveEvent(event); + Mockito.verify(matomoHandler1, Mockito.times(1)).handleEvent(event); Mockito.verify(matomoHandler2, Mockito.times(1)).handleEvent(event); - Mockito.verifyNoMoreInteractions(matomoHandler1, matomoHandler2); + } } \ No newline at end of file diff --git a/dspace/config/modules/matomo.cfg b/dspace/config/modules/matomo.cfg index c8dc6fac9c61..af3702af499d 100644 --- a/dspace/config/modules/matomo.cfg +++ b/dspace/config/modules/matomo.cfg @@ -6,6 +6,9 @@ matomo.enabled = false # Configured `siteid` inside the matomo dashboard matomo.request.siteid = 1 +# Specifies bitstream's bundle that will be tracked ( default is ORIGINAL ) +# Add 'none' to disable the tracking for bitstreams +# matomo.track.bundles = ORIGINAL #---------------------------------------------------------------# #----------------MATOMO CLIENTS CONFIGURATION-------------------#