Skip to content

Commit 4ab0197

Browse files
committed
WIP for #594
1 parent b24c302 commit 4ab0197

6 files changed

Lines changed: 175 additions & 10 deletions

File tree

server/src/main/java/access/api/ConnectionController.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import access.jira.JiraIssue;
77
import access.manage.ChangeRequest;
88
import access.manage.ConnectionProviderConverter;
9+
import access.manage.ListMerger;
910
import access.manage.Manage;
1011
import access.manage.PathUpdateType;
1112
import access.manage.RequestType;
@@ -19,6 +20,7 @@
1920
import access.repository.ApplicationRepository;
2021
import access.repository.ConnectionRepository;
2122
import access.repository.UserRepository;
23+
import com.fasterxml.jackson.databind.ObjectMapper;
2224
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
2325
import lombok.SneakyThrows;
2426
import org.apache.commons.logging.Log;
@@ -44,9 +46,11 @@
4446

4547
import java.time.Instant;
4648
import java.util.Collections;
49+
import java.util.HashMap;
4750
import java.util.List;
4851
import java.util.Map;
4952
import java.util.Optional;
53+
import java.util.stream.Collectors;
5054

5155
import static access.SwaggerOpenIdConfig.API_TOKENS_SCHEME_NAME;
5256
import static access.SwaggerOpenIdConfig.OPEN_ID_SCHEME_NAME;
@@ -72,19 +76,22 @@ public class ConnectionController implements UserAccessRights {
7276
private final PasswordGenerator passwordGenerator = new PasswordGenerator();
7377
private final List<CharacterRule> rules = initPasswordGeneratorRules();
7478
private final ConnectionProviderConverter connectionProviderConverter;
79+
private final ObjectMapper objectMapper;
7580

7681
public ConnectionController(ConnectionRepository connectionRepository,
7782
ApplicationRepository applicationRepository,
7883
UserRepository userRepository,
7984
Manage manage,
8085
JiraClient jiraClient,
81-
ConnectionProviderConverter connectionProviderConverter) {
86+
ConnectionProviderConverter connectionProviderConverter,
87+
ObjectMapper objectMapper) {
8288
this.connectionRepository = connectionRepository;
8389
this.applicationRepository = applicationRepository;
8490
this.userRepository = userRepository;
8591
this.manage = manage;
8692
this.jiraClient = jiraClient;
8793
this.connectionProviderConverter = connectionProviderConverter;
94+
this.objectMapper = objectMapper;
8895
}
8996

9097
private List<CharacterRule> initPasswordGeneratorRules() {
@@ -322,11 +329,42 @@ private Connection productionReadyChangeRequests(Connection connection, User use
322329
//Now we need to ensure that previous change requests, with the same pathUpdate and value a List, does not overwrite changes
323330
//And therefore we don't create a new change request, but update the existing one
324331
Map<String, Object> existingChangeRequest = existingChangeRequests.getFirst();
325-
ChangeRequest changeRequest = changeRequestOptional.get();
326-
existingChangeRequest.get("pathUpdates");
327-
//TODO , add / override all new pathUpdates, except if the value is a List, then sort out the difference
328-
329-
332+
ChangeRequest newChangeRequest = changeRequestOptional.get();
333+
Map<String, Object> existingPathUpdates = (Map<String, Object>) existingChangeRequest.get("pathUpdates");
334+
Map<String, Object> newPathUpdates = newChangeRequest.getPathUpdates();
335+
newPathUpdates.forEach((key, value) -> {
336+
if (key.equals("arp") && existingPathUpdates.containsKey(key)) {
337+
//three way merge on the attributes and profile, motivation from the latest change
338+
Map<String, Object> attributes = (Map<String, Object>) ((Map<String, Object>)value).get("attributes");
339+
List<String> attibuteNames = attributes.keySet().stream().toList();
340+
341+
Map<String, Object> arpPath = (Map<String, Object>) existingPathUpdates.get("arp");
342+
Map<String, Object> pathAttributes = (Map<String, Object>) arpPath.get("attributes");
343+
List<String> pathValues = pathAttributes.keySet().stream().toList();
344+
345+
Map<String, Object> baseArp= (Map<String, Object>) getData(provider).get("arp");
346+
Map<String, Object> baseAttributes = (Map<String, Object>) baseArp.get("attributes");
347+
List<String> baseValues = baseAttributes.keySet().stream().toList();
348+
349+
List<String> newValues = ListMerger.threeWayMerge(baseValues, pathValues, attibuteNames);
350+
//Now we need to construct a new attributes Map with all the values from the three attributes Map
351+
Map<String, Object> newAttributes = newValues.stream().collect(Collectors.toMap(
352+
attrName -> attrName,
353+
attrName -> attributes.getOrDefault(attrName, pathAttributes.getOrDefault(attrName, baseAttributes.get(attrName)))));
354+
arpPath.put("attributes", newAttributes);
355+
} else if (value instanceof List && existingPathUpdates.containsKey(key)) {
356+
//three way merge
357+
List<String> pathUpdateValue = (List<String>) existingPathUpdates.get(key);
358+
List<String> base = (List<String>) getMetaDataFields(getData(provider)).get(key.substring(key.indexOf(".") + 1));
359+
List<String> newValues = ListMerger.threeWayMerge(base, pathUpdateValue, (List<String>) value);
360+
existingPathUpdates.put(key, newValues);
361+
} else {
362+
//simply override
363+
existingPathUpdates.put(key, value);
364+
}
365+
});
366+
ChangeRequest changeRequest = objectMapper.convertValue(existingChangeRequest, ChangeRequest.class);
367+
manage.updateChangeRequest(Environment.PROD, changeRequest);
330368
}
331369
}
332370

server/src/main/java/access/api/DefaultErrorController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ public class DefaultErrorController implements ErrorController {
3232

3333
private static final Log LOG = LogFactory.getLog(DefaultErrorController.class);
3434

35-
private static final List<Class> suppressStackTraceClasses = List.of(
36-
UserRestrictionException.class,
37-
NotFoundException.class);
35+
private static final List<Class<?>> suppressStackTraceClasses = List.of(
36+
NotFoundException.class
37+
);
3838

3939
private final ErrorAttributes errorAttributes;
4040

server/src/main/java/access/exception/UserRestrictionException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import org.springframework.security.core.AuthenticationException;
55
import org.springframework.web.bind.annotation.ResponseStatus;
66

7-
@ResponseStatus(HttpStatus.FORBIDDEN)
7+
@ResponseStatus(HttpStatus.FORBIDDEN)
88
public class UserRestrictionException extends AuthenticationException {
99

1010
public UserRestrictionException(String msg) {

server/src/main/java/access/manage/ChangeRequest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010

1111
import java.io.Serializable;
12+
import java.time.Instant;
1213
import java.util.Map;
1314
import java.util.Objects;
1415

@@ -37,6 +38,8 @@ public class ChangeRequest implements Serializable {
3738

3839
private RequestType requestType;
3940

41+
private Instant created;
42+
4043
@Setter
4144
private String ticketKey;
4245

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package access.manage;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Objects;
8+
9+
public class ListMerger {
10+
11+
private ListMerger() {
12+
}
13+
14+
public static List<String> threeWayMerge(
15+
List<String> base, // list1: original
16+
List<String> left, // list2: your changes
17+
List<String> right // list3: their changes
18+
) {
19+
// Compute diffs against the base
20+
Map<Integer, String> leftChanges = diff(base, left);
21+
Map<Integer, String> rightChanges = diff(base, right);
22+
23+
// Start from the longer of left/right to preserve additions
24+
int maxSize = Math.max(left.size(), right.size());
25+
List<String> result = new ArrayList<>();
26+
27+
for (int i = 0; i < maxSize; i++) {
28+
boolean leftChanged = leftChanges.containsKey(i);
29+
boolean rightChanged = rightChanges.containsKey(i);
30+
31+
if (leftChanged && rightChanged) {
32+
String leftVal = leftChanges.get(i);
33+
String rightVal = rightChanges.get(i);
34+
35+
if (Objects.equals(leftVal, rightVal)) {
36+
// Both made the same change — no real conflict
37+
if (leftVal != null) result.add(leftVal);
38+
} else if (i >= base.size()) {
39+
// ✅ CHANGED: both added different items past the end of base — keep both
40+
if (leftVal != null) result.add(leftVal);
41+
if (rightVal != null) result.add(rightVal);
42+
} else {
43+
// True conflict on an existing line: prefer left
44+
if (leftVal != null) {
45+
result.add(leftVal);
46+
}
47+
}
48+
} else if (leftChanged) {
49+
// Only left modified this index
50+
String val = leftChanges.get(i);
51+
if (val != null) result.add(val); // null = deleted
52+
53+
} else if (rightChanged) {
54+
// Only right modified this index
55+
String val = rightChanges.get(i);
56+
if (val != null) result.add(val); // null = deleted
57+
58+
} else {
59+
// Neither side touched this index — take from base
60+
if (i < base.size()) result.add(base.get(i));
61+
}
62+
}
63+
64+
return result;
65+
}
66+
67+
/**
68+
* Produces a map of { index → newValue } representing what changed.
69+
* A null value means "deleted at this index".
70+
* Indices beyond base.size() are pure additions.
71+
*/
72+
private static Map<Integer, String> diff(List<String> base, List<String> updated) {
73+
Map<Integer, String> changes = new HashMap<>();
74+
int maxLen = Math.max(base.size(), updated.size());
75+
76+
for (int i = 0; i < maxLen; i++) {
77+
String baseVal = i < base.size() ? base.get(i) : null;
78+
String updatedVal = i < updated.size() ? updated.get(i) : null;
79+
80+
if (!Objects.equals(baseVal, updatedVal)) {
81+
changes.put(i, updatedVal); // null = deletion
82+
}
83+
}
84+
return changes;
85+
}
86+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package access.manage;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.List;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
9+
class ListMergerTest {
10+
11+
12+
@Test
13+
void threeWayMerge() {
14+
List<String> list1 = List.of("alpha", "beta", "gamma", "delta");
15+
16+
// list2: changed "beta"→"BETA", removed "delta", added "epsilon"
17+
List<String> list2 = List.of("alpha", "BETA", "gamma", "epsilon");
18+
19+
// list3: changed "alpha"→"ALPHA", changed "gamma"→"GAMMA"
20+
List<String> list3 = List.of("ALPHA", "beta", "GAMMA", "delta");
21+
22+
List<String> list4 = ListMerger.threeWayMerge(list1, list2, list3);
23+
assertEquals(list4, List.of("ALPHA", "BETA", "GAMMA", "epsilon"));
24+
25+
list1 = List.of("red1", "red2");
26+
27+
// list2: changed "beta"→"BETA", removed "delta", added "epsilon"
28+
list2 = List.of("red1", "red2", "red3");
29+
30+
// list3: changed "alpha"→"ALPHA", changed "gamma"→"GAMMA"
31+
list3 = List.of("red1", "red2", "red4");
32+
33+
list4 = ListMerger.threeWayMerge(list1, list2, list3);
34+
assertEquals(list4, List.of("red1", "red2", "red3", "red4"));
35+
}
36+
37+
38+
}

0 commit comments

Comments
 (0)