2323import com .google .common .base .Optional ;
2424import com .google .common .collect .ImmutableList ;
2525import com .google .common .collect .ImmutableMultimap ;
26+ import com .google .common .collect .LinkedListMultimap ;
27+ import com .google .common .collect .Multimap ;
2628import com .google .protobuf .Internal ;
2729import com .google .protobuf .Message ;
2830import org .slf4j .Logger ;
3133import org .spine3 .base .EventContext ;
3234import 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 ;
3440import 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 ;
3646import static com .google .protobuf .Descriptors .Descriptor ;
3747import static com .google .protobuf .Descriptors .FieldDescriptor ;
48+ import static com .google .protobuf .Descriptors .FieldDescriptor .Type .MESSAGE ;
3849import 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 ;
0 commit comments