Skip to content

Commit 22a7f17

Browse files
authored
Merge pull request #295 from SpineEventEngine/multiple-args-by-option
Multiple arguments `by` option
2 parents 0d6eb35 + 5134b27 commit 22a7f17

File tree

10 files changed

+454
-29
lines changed

10 files changed

+454
-29
lines changed

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ buildscript {
3636
spineVersion = '0.7.24-SNAPSHOT'
3737
//TODO:2016-12-12:alexander.yevsyukov: Advance the plug-in version together with all the components and change
3838
// the version of the dependency below to `spineVersion` defined above.
39-
spineProtobufPluginVersion = '0.6.6-SNAPSHOT'
39+
spineProtobufPluginVersion = spineVersion
4040
}
4141

4242
dependencies {
@@ -170,7 +170,7 @@ subprojects {
170170
}
171171
}
172172
*/
173-
173+
174174
protobuf {
175175
// The below suppressions `GroovyAssignabilityCheck` is a workaround for the IDEA bug.
176176
// See: https://youtrack.jetbrains.com/issue/IDEA-141744

server/src/main/java/org/spine3/server/event/enrich/EventEnrichmentsMap.java

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
import javax.annotation.Nullable;
3434
import java.util.Collection;
3535
import java.util.HashSet;
36+
import java.util.LinkedList;
3637
import java.util.List;
3738
import java.util.Properties;
3839
import java.util.Set;
40+
import java.util.regex.Pattern;
3941

4042
import static com.google.common.base.Preconditions.checkArgument;
4143
import static com.google.common.base.Preconditions.checkNotNull;
@@ -62,6 +64,10 @@ class EventEnrichmentsMap {
6264
/** A separator between event types in the `.properties` file. */
6365
private static final String EVENT_TYPE_SEPARATOR = ",";
6466

67+
private static final Pattern PIPE_SEPARATOR_PATTERN = Pattern.compile("\\|");
68+
69+
private static final char PROTO_PACKAGE_SEPARATOR = '.';
70+
6571
private static final ImmutableMultimap<String, String> enrichmentsMap = buildEnrichmentsMap();
6672

6773
private EventEnrichmentsMap() {
@@ -88,13 +94,6 @@ private static class Builder {
8894
*/
8995
private static final String PACKAGE_WILDCARD_INDICATOR = ".*";
9096

91-
/**
92-
* Constant indicating a field qualifier with a wildcard type.
93-
*
94-
* <p>Must be a prefix of the qualifier.
95-
*/
96-
private static final String WILDCARD_TYPE_INDICATOR = "*.";
97-
9897
private final Iterable<Properties> properties;
9998
private final ImmutableMultimap.Builder<String, String> builder;
10099

@@ -142,7 +141,7 @@ private void putAllTypesFromPackage(String enrichmentType, String eventsPackage)
142141
final Collection<TypeUrl> eventTypes = KnownTypes.getTypesFromPackage(packageName);
143142
for (TypeUrl type : eventTypes) {
144143
final String typeQualifier = type.getTypeName();
145-
if (hasTargetFields(typeQualifier, boundFields)) {
144+
if (hasOneOfTargetFields(typeQualifier, boundFields)) {
146145
builder.put(enrichmentType, typeQualifier);
147146
}
148147
}
@@ -155,13 +154,36 @@ private static Set<String> getBoundFields(String enrichmentType) {
155154
for (FieldDescriptor field : enrichmentDescriptor.getFields()) {
156155
final String extension = field.getOptions()
157156
.getExtension(EventAnnotationsProto.by);
158-
final String fieldName = extension.substring(WILDCARD_TYPE_INDICATOR.length());
157+
final Collection<String> fieldNames = parseFieldNames(extension);
158+
result.addAll(fieldNames);
159+
}
160+
return result;
161+
}
162+
163+
private static Collection<String> parseFieldNames(String qualifiers) {
164+
final Collection<String> result = new LinkedList<>();
165+
final String[] fieldNames = PIPE_SEPARATOR_PATTERN.split(qualifiers);
166+
for (String singleFieldName : fieldNames) {
167+
final String normalizedFieldName = singleFieldName.trim();
168+
if (normalizedFieldName.isEmpty()) {
169+
continue;
170+
}
171+
final String fieldName = getSimpleFieldName(normalizedFieldName);
159172
result.add(fieldName);
160173
}
161174
return result;
162175
}
163176

164-
private static boolean hasTargetFields(String eventType, Collection<String> targetFields) {
177+
private static String getSimpleFieldName(String qualifier) {
178+
int startIndex = qualifier.lastIndexOf(PROTO_PACKAGE_SEPARATOR) + 1;
179+
startIndex = startIndex > 0 // 0 is an invalid value, see line above
180+
? startIndex
181+
: 0;
182+
final String fieldName = qualifier.substring(startIndex);
183+
return fieldName;
184+
}
185+
186+
private static boolean hasOneOfTargetFields(String eventType, Collection<String> targetFields) {
165187
final Descriptor eventDescriptor = KnownTypes.getDescriptorForType(eventType);
166188

167189
final List<FieldDescriptor> fields = eventDescriptor.getFields();
@@ -174,8 +196,12 @@ public String apply(@Nullable FieldDescriptor input) {
174196
return input.getName();
175197
}
176198
});
177-
final boolean result = fieldNames.containsAll(targetFields);
178-
return result;
199+
for (String field : targetFields) {
200+
if (fieldNames.contains(field)) {
201+
return true;
202+
}
203+
}
204+
return false;
179205
}
180206

181207
/**

server/src/main/java/org/spine3/server/event/enrich/ReferenceValidator.java

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import com.google.common.base.Optional;
2424
import com.google.common.collect.ImmutableList;
2525
import com.google.common.collect.ImmutableMultimap;
26+
import com.google.common.collect.LinkedListMultimap;
27+
import com.google.common.collect.Multimap;
2628
import com.google.protobuf.Internal;
2729
import com.google.protobuf.Message;
2830
import org.slf4j.Logger;
@@ -31,10 +33,19 @@
3133
import org.spine3.base.EventContext;
3234
import org.spine3.protobuf.Messages;
3335

36+
import java.util.Collection;
37+
import java.util.Collections;
38+
import java.util.HashSet;
39+
import java.util.LinkedList;
3440
import java.util.List;
41+
import java.util.regex.Pattern;
3542

43+
import static com.google.common.base.Preconditions.checkArgument;
44+
import static com.google.common.base.Preconditions.checkNotNull;
45+
import static com.google.common.base.Preconditions.checkState;
3646
import static com.google.protobuf.Descriptors.Descriptor;
3747
import static com.google.protobuf.Descriptors.FieldDescriptor;
48+
import static com.google.protobuf.Descriptors.FieldDescriptor.Type.MESSAGE;
3849
import static java.lang.String.format;
3950

4051
/**
@@ -51,6 +62,14 @@ class ReferenceValidator {
5162
/** The separator used in Protobuf fully-qualified names. */
5263
private static final String PROTO_FQN_SEPARATOR = ".";
5364

65+
private static final String PIPE_SEPARATOR = "|";
66+
private static final Pattern PATTERN_PIPE_SEPARATOR = Pattern.compile("\\|");
67+
68+
private static final String SPACE = " ";
69+
private static final String EMPTY_STRING = "";
70+
private static final Pattern SPACE_PATTERN = Pattern.compile(SPACE, Pattern.LITERAL);
71+
72+
5473
/** The reference to the event context used in the `by` field option. */
5574
private static final String CONTEXT_REFERENCE = "context";
5675

@@ -74,35 +93,114 @@ class ReferenceValidator {
7493
* @return a {@code ValidationResult} data transfer object, containing the valid fields and functions.
7594
*/
7695
ValidationResult validate() {
77-
final ImmutableList.Builder<EnrichmentFunction<?, ?>> functions = ImmutableList.builder();
78-
final ImmutableMultimap.Builder<FieldDescriptor, FieldDescriptor> fields = ImmutableMultimap.builder();
96+
final List<EnrichmentFunction<?, ?>> functions = new LinkedList<>();
97+
final Multimap<FieldDescriptor, FieldDescriptor> fields = LinkedListMultimap.create();
7998
for (FieldDescriptor enrichmentField : enrichmentDescriptor.getFields()) {
80-
final FieldDescriptor sourceField = findSourceField(enrichmentField);
99+
final Collection<FieldDescriptor> sourceFields = findSourceFields(enrichmentField);
100+
putEnrichmentsByField(functions, fields, enrichmentField, sourceFields);
101+
}
102+
final ImmutableMultimap<FieldDescriptor, FieldDescriptor> sourceToTargetMap = ImmutableMultimap.copyOf(fields);
103+
final ImmutableList<EnrichmentFunction<?, ?>> enrichmentFunctions = ImmutableList.copyOf(functions);
104+
final ValidationResult result = new ValidationResult(enrichmentFunctions, sourceToTargetMap);
105+
return result;
106+
}
107+
108+
private void putEnrichmentsByField(List<EnrichmentFunction<?, ?>> functions,
109+
Multimap<FieldDescriptor, FieldDescriptor> fields,
110+
FieldDescriptor enrichmentField,
111+
Iterable<FieldDescriptor> sourceFields) {
112+
for (FieldDescriptor sourceField : sourceFields) {
81113
final Optional<EnrichmentFunction<?, ?>> function = getEnrichmentFunction(sourceField, enrichmentField);
82114
if (function.isPresent()) {
83115
functions.add(function.get());
84116
fields.put(sourceField, enrichmentField);
85117
}
86118
}
87-
final ImmutableMultimap<FieldDescriptor, FieldDescriptor> fieldMap = fields.build();
88-
final ImmutableList<EnrichmentFunction<?, ?>> functionList = functions.build();
89-
final ValidationResult result = new ValidationResult(functionList, fieldMap);
90-
return result;
91119
}
92120

93121
/** Searches for the event/context field with the name parsed from the enrichment field `by` option. */
94-
private FieldDescriptor findSourceField(FieldDescriptor enrichmentField) {
95-
final String fieldName = enrichmentField.getOptions()
96-
.getExtension(EventAnnotationsProto.by);
97-
checkSourceFieldName(fieldName, enrichmentField);
98-
final Descriptor srcMessage = getSrcMessage(fieldName);
99-
final FieldDescriptor field = findField(fieldName, srcMessage);
100-
if (field == null) {
101-
throw noFieldException(fieldName, srcMessage, enrichmentField);
122+
private Collection<FieldDescriptor> findSourceFields(FieldDescriptor enrichmentField) {
123+
final String byOptionArgument = enrichmentField.getOptions()
124+
.getExtension(EventAnnotationsProto.by);
125+
checkNotNull(byOptionArgument);
126+
final String targetFields = removeSpaces(byOptionArgument);
127+
final int pipeSeparatorIndex = targetFields.indexOf(PIPE_SEPARATOR);
128+
if (pipeSeparatorIndex < 0) {
129+
return Collections.singleton(findSourceFieldByName(targetFields, enrichmentField, true));
130+
} else {
131+
final String[] targetFieldNames = PATTERN_PIPE_SEPARATOR.split(targetFields);
132+
return findSourceFieldsByNames(targetFieldNames, enrichmentField);
133+
}
134+
}
135+
136+
/**
137+
* Searches for the event/context field with the name retrieved from the enrichment field `by` option.
138+
*
139+
* @param name the name of the searched field
140+
* @param enrichmentField the field of the enrichment targeted onto the searched field
141+
* @param strict if {@code true} the field must be found, an exception is thrown otherwise.
142+
* <p>If {@code false} {@code null} will be returned upon an unsuccessful search
143+
* @return {@link FieldDescriptor} for the field with the given name or {@code null} if the field is absent and
144+
* if not in the strict mode
145+
*/
146+
private FieldDescriptor findSourceFieldByName(String name, FieldDescriptor enrichmentField, boolean strict) {
147+
checkSourceFieldName(name, enrichmentField);
148+
final Descriptor srcMessage = getSrcMessage(name);
149+
final FieldDescriptor field = findField(name, srcMessage);
150+
if (field == null && strict) {
151+
throw noFieldException(name, srcMessage, enrichmentField);
102152
}
103153
return field;
104154
}
105155

156+
private static String removeSpaces(String source) {
157+
checkNotNull(source);
158+
final String result = SPACE_PATTERN.matcher(source)
159+
.replaceAll(EMPTY_STRING);
160+
return result;
161+
}
162+
163+
private Collection<FieldDescriptor> findSourceFieldsByNames(String[] names, FieldDescriptor enrichmentField) {
164+
checkArgument(names.length > 0, "Names may not be empty");
165+
checkArgument(names.length > 1,
166+
"Enrichment target field names may not be a singleton array. Use findSourceFieldByName.");
167+
final Collection<FieldDescriptor> result = new HashSet<>(names.length);
168+
169+
FieldDescriptor.Type basicType = null;
170+
Descriptor messageType = null;
171+
for (String name : names) {
172+
final FieldDescriptor field = findSourceFieldByName(name, enrichmentField, false);
173+
if (field == null) {
174+
// We don't know at this stage the type of the event
175+
// The enrichment is to be included anyway, but by other {@code ReferenceValidator} instance
176+
continue;
177+
}
178+
179+
if (basicType == null) { // Get type of the first field
180+
basicType = field.getType();
181+
if (basicType == MESSAGE) {
182+
messageType = field.getMessageType();
183+
}
184+
} else { // Compare the type with each of the next
185+
checkState(basicType == field.getType(), differentTypesErrorMessage(enrichmentField));
186+
if (basicType == MESSAGE) {
187+
checkState(messageType.equals(field.getMessageType()), differentTypesErrorMessage(enrichmentField));
188+
}
189+
}
190+
191+
final boolean noDuplicateFiled = result.add(field);
192+
checkState(
193+
noDuplicateFiled,
194+
"Enrichment target field names may contain no duplicates. Found duplicate field " + name
195+
);
196+
}
197+
return result;
198+
}
199+
200+
private static String differentTypesErrorMessage(FieldDescriptor enrichmentField) {
201+
return format("Enrichment field %s targets fields of different types.", enrichmentField);
202+
}
203+
106204
private static FieldDescriptor findField(String fieldNameFull, Descriptor srcMessage) {
107205
if (fieldNameFull.contains(PROTO_FQN_SEPARATOR)) { // is event field FQN or context field
108206
final int firstCharIndex = fieldNameFull.lastIndexOf(PROTO_FQN_SEPARATOR) + 1;

server/src/test/java/org/spine3/server/event/Given.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,25 @@
2222

2323
import com.google.common.base.Function;
2424
import com.google.protobuf.Any;
25+
import com.google.protobuf.Message;
2526
import com.google.protobuf.Timestamp;
2627
import org.spine3.base.CommandContext;
2728
import org.spine3.base.EventContext;
2829
import org.spine3.base.EventId;
30+
import org.spine3.base.Events;
31+
import org.spine3.base.Identifiers;
2932
import org.spine3.people.PersonName;
33+
import org.spine3.protobuf.AnyPacker;
3034
import org.spine3.server.event.enrich.EventEnricher;
3135
import org.spine3.test.Tests;
3236
import org.spine3.test.event.ProjectCompleted;
3337
import org.spine3.test.event.ProjectCreated;
3438
import org.spine3.test.event.ProjectId;
3539
import org.spine3.test.event.ProjectStarred;
3640
import org.spine3.test.event.ProjectStarted;
41+
import org.spine3.test.event.user.permission.PermissionGrantedEvent;
42+
import org.spine3.test.event.user.permission.PermissionRevokedEvent;
43+
import org.spine3.test.event.user.sharing.SharingRequestApproved;
3744
import org.spine3.time.ZoneOffset;
3845
import org.spine3.users.UserId;
3946

@@ -111,6 +118,27 @@ public static ProjectStarred projectStarred(ProjectId id) {
111118
.setProjectId(id)
112119
.build();
113120
}
121+
122+
public static PermissionGrantedEvent permissionGranted() {
123+
return PermissionGrantedEvent.newBuilder()
124+
.setGranterUid(Identifiers.newUuid())
125+
.setPermissionId("mock-permission")
126+
.setUserUid(Identifiers.newUuid())
127+
.build();
128+
}
129+
130+
public static PermissionRevokedEvent permissionRevoked() {
131+
return PermissionRevokedEvent.newBuilder()
132+
.setPermissionId("old-permission")
133+
.setUserUid(Identifiers.newUuid())
134+
.build();
135+
}
136+
137+
public static SharingRequestApproved sharingRequestApproved() {
138+
return SharingRequestApproved.newBuilder()
139+
.setUserUid(Identifiers.newUuid())
140+
.build();
141+
}
114142
}
115143

116144
public static class Event {
@@ -138,6 +166,31 @@ public static org.spine3.base.Event projectCreated(ProjectId projectId, EventCon
138166
final org.spine3.base.Event event = createEvent(msg, eventContext);
139167
return event;
140168
}
169+
170+
public static org.spine3.base.Event permissionGranted() {
171+
final PermissionGrantedEvent message = EventMessage.permissionGranted();
172+
final org.spine3.base.Event permissionGranted = createGenericEvent(message);
173+
return permissionGranted;
174+
}
175+
176+
public static org.spine3.base.Event permissionRevoked() {
177+
final PermissionRevokedEvent message = EventMessage.permissionRevoked();
178+
final org.spine3.base.Event permissionRevoked = createGenericEvent(message);
179+
return permissionRevoked;
180+
}
181+
182+
public static org.spine3.base.Event sharingRequestApproved() {
183+
final SharingRequestApproved message = EventMessage.sharingRequestApproved();
184+
final org.spine3.base.Event sharingReqquestApproved = createGenericEvent(message);
185+
return sharingReqquestApproved;
186+
}
187+
188+
private static org.spine3.base.Event createGenericEvent(Message eventMessage) {
189+
final Any wrappedMessage = AnyPacker.pack(eventMessage);
190+
final EventContext eventContext = createEventContext();
191+
final org.spine3.base.Event permissionRevoked = Events.createEvent(wrappedMessage, eventContext);
192+
return permissionRevoked;
193+
}
141194
}
142195

143196
public static class Enrichment {

server/src/test/java/org/spine3/server/event/enrich/EventEnricherShould.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ public void enrich_several_events_with_same_enrichment_message_with_wildcard_by(
138138
assertEquals(getProjectName.apply(starredProjectId), subscriber.projectStarredEnrichment.getProjectName());
139139
}
140140

141+
@Test
142+
public void enrich_several_events_bound_by_fields() {
143+
final Event permissionGranted = Given.Event.permissionGranted();
144+
final Event permissionRevoked = Given.Event.permissionRevoked();
145+
final Event sharingRequestApproved = Given.Event.sharingRequestApproved();
146+
147+
assertTrue(enricher.canBeEnriched(permissionGranted));
148+
assertTrue(enricher.canBeEnriched(permissionRevoked));
149+
assertTrue(enricher.canBeEnriched(sharingRequestApproved));
150+
}
151+
141152
@Test
142153
public void enrich_event_if_function_added_at_runtime() {
143154
final Given.Enrichment.GetProjectMaxMemberCount function = new Given.Enrichment.GetProjectMaxMemberCount();

0 commit comments

Comments
 (0)