Skip to content

Commit

Permalink
AAE-29493 Add timestamp range filters to audit events search (#1650)
Browse files Browse the repository at this point in the history
* AAE-28084 remove blank line

* AAE-28084 add date time formats

* AAE-28084 add equality to range filters

* AAE-28084 fix swagger

* AAE-29492 add date filters with tests

* AAE-29492 rename controller method

* AAE-29492 fix date formatter in test

* AAE-29492 add license headers

* AAE-29492 sonarcloud issues
  • Loading branch information
tom-dal authored Dec 18, 2024
1 parent 55cc81a commit 992f260
Show file tree
Hide file tree
Showing 12 changed files with 373 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.activiti.cloud.api.model.shared.events.CloudRuntimeEvent;
import org.activiti.cloud.services.audit.api.converters.CloudRuntimeEventType;
import org.activiti.cloud.services.audit.api.resources.EventsLinkRelationProvider;
import org.activiti.cloud.services.audit.api.search.SearchParams;
import org.springframework.data.domain.Pageable;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.MediaTypes;
Expand All @@ -26,7 +27,6 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -39,8 +39,8 @@ public interface AuditEventsController {
EntityModel<CloudRuntimeEvent<?, CloudRuntimeEventType>> findById(@PathVariable String eventId);

@RequestMapping(method = RequestMethod.GET)
PagedModel<EntityModel<CloudRuntimeEvent<?, CloudRuntimeEventType>>> findAll(
@RequestParam(value = "search", required = false) String search,
PagedModel<EntityModel<CloudRuntimeEvent<?, CloudRuntimeEventType>>> search(
SearchParams searchParams,
Pageable pageable
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2017-2020 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.activiti.cloud.services.audit.api.search;

import java.util.Date;

public record SearchParams(String search, Date eventTimeFrom, Date eventTimeTo) {}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ public Predicate toPredicate(
return builder.notEqual(root.get(criteria.getKey()), criteria.getValue());
case GREATER_THAN:
return builder.greaterThan(root.get(criteria.getKey()), criteria.getValue().toString());
case GREATER_THAN_EQUAL:
return builder.greaterThanOrEqualTo(root.get(criteria.getKey()), criteria.getValue().toString());
case LESS_THAN:
return builder.lessThan(root.get(criteria.getKey()), criteria.getValue().toString());
case LESS_THAN_EQUAL:
return builder.lessThanOrEqualTo(root.get(criteria.getKey()), criteria.getValue().toString());
case LIKE:
return builder.like(root.get(criteria.getKey()), criteria.getValue().toString());
case STARTS_WITH:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public final EventSpecificationsBuilder with(
final String prefix,
final String suffix
) {
SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
SearchOperation op = SearchOperation.getSimpleOperation(operation);
if (op != null) {
if (op == SearchOperation.EQUALITY) { // the operation may be complex operation
final boolean startWithAsterisk = prefix != null && prefix.contains(SearchOperation.ZERO_OR_MORE_REGEX);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ public enum SearchOperation {
EQUALITY,
NEGATION,
GREATER_THAN,
GREATER_THAN_EQUAL,
LESS_THAN,
LESS_THAN_EQUAL,
LIKE,
STARTS_WITH,
ENDS_WITH,
Expand All @@ -39,16 +41,17 @@ public enum SearchOperation {

public static final String RIGHT_PARANTHESIS = ")";

public static SearchOperation getSimpleOperation(final char input) {
public static SearchOperation getSimpleOperation(final String operation) {
char input = operation.charAt(0);
switch (input) {
case ':':
return EQUALITY;
case '!':
return NEGATION;
case '>':
return GREATER_THAN;
return operation.length() > 1 && operation.charAt(1) == '=' ? GREATER_THAN_EQUAL : GREATER_THAN;
case '<':
return LESS_THAN;
return operation.length() > 1 && operation.charAt(1) == '=' ? LESS_THAN_EQUAL : LESS_THAN;
case '~':
return LIKE;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public SpecSearchCriteria(
}

public SpecSearchCriteria(String key, String operation, String prefix, String value, String suffix) {
SearchOperation op = SearchOperation.getSimpleOperation(operation.charAt(0));
SearchOperation op = SearchOperation.getSimpleOperation(operation);
if (op != null) {
if (op == SearchOperation.EQUALITY) { // the operation may be complex operation
final boolean startWithAsterisk = prefix != null && prefix.contains(SearchOperation.ZERO_OR_MORE_REGEX);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.activiti.cloud.services.audit.api.converters.CloudRuntimeEventType;
import org.activiti.cloud.services.audit.api.converters.EventToEntityConverter;
import org.activiti.cloud.services.audit.api.resources.EventsLinkRelationProvider;
import org.activiti.cloud.services.audit.api.search.SearchParams;
import org.activiti.cloud.services.audit.jpa.assembler.EventRepresentationModelAssembler;
import org.activiti.cloud.services.audit.jpa.events.AuditEventEntity;
import org.activiti.cloud.services.audit.jpa.repository.EventSpecificationsBuilder;
Expand All @@ -53,7 +54,6 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand Down Expand Up @@ -115,11 +115,11 @@ public EntityModel<CloudRuntimeEvent<?, CloudRuntimeEventType>> findById(@PathVa
}

@RequestMapping(method = RequestMethod.GET)
public PagedModel<EntityModel<CloudRuntimeEvent<?, CloudRuntimeEventType>>> findAll(
@RequestParam(value = "search", required = false) String search,
public PagedModel<EntityModel<CloudRuntimeEvent<?, CloudRuntimeEventType>>> search(
SearchParams searchParams,
Pageable pageable
) {
Specification<AuditEventEntity> spec = createSearchSpec(search);
Specification<AuditEventEntity> spec = createSearchSpec(searchParams);

spec = securityPoliciesApplicationService.createSpecWithSecurity(spec, SecurityPolicyAccess.READ);

Expand All @@ -144,8 +144,9 @@ public PagedModel<EntityModel<CloudRuntimeEvent<?, CloudRuntimeEventType>>> find
);
}

private Specification<AuditEventEntity> createSearchSpec(String search) {
private Specification<AuditEventEntity> createSearchSpec(SearchParams searchParams) {
EventSpecificationsBuilder builder = new EventSpecificationsBuilder();
String search = searchParams.search();
if (search != null && !search.isEmpty()) {
String operationSetExpr = Arrays
.asList(SearchOperation.SIMPLE_OPERATION_SET)
Expand All @@ -159,7 +160,12 @@ private Specification<AuditEventEntity> createSearchSpec(String search) {
builder.with(matcher.group(1), matcher.group(2), matcher.group(4), matcher.group(3), matcher.group(5));
}
}

if (searchParams.eventTimeFrom() != null) {
builder.with("timestamp", ">=", searchParams.eventTimeFrom().getTime(), null, null);
}
if (searchParams.eventTimeTo() != null) {
builder.with("timestamp", "<=", searchParams.eventTimeTo().getTime(), null, null);
}
return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2017-2020 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.activiti.cloud.services.audit.jpa;

import org.activiti.cloud.services.audit.jpa.util.TestConverter;
import org.springframework.context.annotation.Bean;

public class AuditTestConfiguration {

@Bean
public TestConverter getTestConverter() {
return new TestConverter();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2017-2020 Alfresco Software, Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.activiti.cloud.services.audit.jpa.controller;

import static io.restassured.module.mockmvc.RestAssuredMockMvc.given;
import static io.restassured.module.mockmvc.RestAssuredMockMvc.webAppContextSetup;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;

import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
import org.activiti.api.runtime.shared.security.SecurityManager;
import org.activiti.cloud.alfresco.config.AlfrescoWebAutoConfiguration;
import org.activiti.cloud.services.audit.jpa.AuditTestConfiguration;
import org.activiti.cloud.services.audit.jpa.events.ActivityCompletedAuditEventEntity;
import org.activiti.cloud.services.audit.jpa.events.ActivityStartedAuditEventEntity;
import org.activiti.cloud.services.audit.jpa.events.AuditEventEntity;
import org.activiti.cloud.services.audit.jpa.events.ProcessStartedAuditEventEntity;
import org.activiti.cloud.services.audit.jpa.repository.EventsRepository;
import org.activiti.cloud.services.audit.jpa.util.TestConverter;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.context.WebApplicationContext;

@SpringBootTest(properties = { "spring.main.banner-mode=off" })
@Import({ AlfrescoWebAutoConfiguration.class, AuditTestConfiguration.class })
class AuditEventsControllerImpIT {

@Autowired
private EventsRepository<AuditEventEntity> eventsRepository;

@Autowired
private WebApplicationContext context;

@MockBean
private UserDetailsService userDetailsService;

@MockBean
private SecurityManager securityManager;

private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
.withZone(ZoneOffset.UTC);

private static final String ENTRIES_ROOT = "_embedded.events";
private static final String EVENTS_ID_ROOT = ENTRIES_ROOT + ".id";

@BeforeEach
void setUp() {
webAppContextSetup(context);
}

@AfterEach
void cleanUp() {
eventsRepository.deleteAll();
}

@Test
void should_returnAuditEvents_filteredByEventTimeFrom() {
AuditEventEntity audit1 = new ProcessStartedAuditEventEntity();
audit1.setTimestamp(1000L);
audit1.setEventType(TestConverter.EVENT_TYPE);

AuditEventEntity audit2 = new ActivityStartedAuditEventEntity();
audit2.setTimestamp(2000L);
audit2.setEventType(TestConverter.EVENT_TYPE);

AuditEventEntity audit3 = new ActivityCompletedAuditEventEntity();
audit3.setTimestamp(3000L);
audit3.setEventType(TestConverter.EVENT_TYPE);

eventsRepository.saveAll(List.of(audit1, audit2, audit3));

given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.param("eventTimeFrom", dateTimeFormatter.format(Instant.ofEpochMilli(2000L).atZone(ZoneOffset.UTC)))
.when()
.get("/v1/events")
.then()
.statusCode(200)
.body(ENTRIES_ROOT, hasSize(2))
.body(EVENTS_ID_ROOT, contains(audit2.getId().toString(), audit3.getId().toString()));
eventsRepository.deleteAll();
}

@Test
void should_returnAuditEvents_filteredByEventTimeTo() {
AuditEventEntity audit1 = new ProcessStartedAuditEventEntity();
audit1.setTimestamp(1000L);
audit1.setEventType(TestConverter.EVENT_TYPE);

AuditEventEntity audit2 = new ActivityStartedAuditEventEntity();
audit2.setTimestamp(2000L);
audit2.setEventType(TestConverter.EVENT_TYPE);

AuditEventEntity audit3 = new ActivityCompletedAuditEventEntity();
audit3.setTimestamp(3000L);
audit3.setEventType(TestConverter.EVENT_TYPE);

eventsRepository.saveAll(List.of(audit1, audit2, audit3));

given()
.contentType(MediaType.APPLICATION_JSON_VALUE)
.param("eventTimeTo", dateTimeFormatter.format(Instant.ofEpochMilli(2000L).atZone(ZoneOffset.UTC)))
.when()
.get("/v1/events")
.then()
.statusCode(200)
.body(ENTRIES_ROOT, hasSize(2))
.body(EVENTS_ID_ROOT, contains(audit1.getId().toString(), audit2.getId().toString()));
}
}
Loading

0 comments on commit 992f260

Please sign in to comment.