1
+ package com .netflix .hollow .api .producer .validation ;
2
+
3
+ import com .netflix .hollow .api .producer .HollowProducer ;
4
+ import com .netflix .hollow .core .index .key .PrimaryKey ;
5
+ import com .netflix .hollow .core .read .HollowReadFieldUtils ;
6
+ import com .netflix .hollow .core .read .engine .HollowReadStateEngine ;
7
+ import com .netflix .hollow .core .read .engine .object .HollowObjectTypeReadState ;
8
+ import com .netflix .hollow .core .schema .HollowObjectSchema ;
9
+ import com .netflix .hollow .core .schema .HollowSchema ;
10
+ import com .netflix .hollow .core .write .objectmapper .HollowPrimaryKey ;
11
+ import com .netflix .hollow .core .write .objectmapper .HollowTypeMapper ;
12
+
13
+ import java .util .Arrays ;
14
+ import java .util .BitSet ;
15
+ import java .util .HashMap ;
16
+ import java .util .Map ;
17
+ import java .util .Objects ;
18
+ import java .util .stream .Collectors ;
19
+
20
+ import static com .netflix .hollow .core .HollowConstants .ORDINAL_NONE ;
21
+
22
+ /**
23
+ * A validator that fails if any records of a given type have null primary key fields.
24
+ * <p>
25
+ * The primary key may be declared on a data model class using the {@link HollowPrimaryKey} annotation, may be
26
+ * declared more directly using {@link PrimaryKey} with {@link HollowObjectSchema}, or declared explicitly when
27
+ * instantiating this validator.
28
+ */
29
+ public class NullPrimaryKeyFieldValidator implements ValidatorListener {
30
+ private static final String NAME = NullPrimaryKeyFieldValidator .class .getName ();
31
+ private static final String NULL_PRIMARY_KEYS_FOUND_ERROR_MSG_FORMAT =
32
+ "Null primary key fields found for type %s. Primary Key in schema is %s. "
33
+ + "Null records: [%s]" ;
34
+ private static final String NO_PRIMARY_KEY_ERROR_MSG_FORMAT =
35
+ "NullPrimaryKeyFieldValidator defined but unable to find primary key for data type %s. "
36
+ + "Please check schema definition." ;
37
+
38
+ private static final String NO_SCHEMA_FOUND_MSG_FORMAT =
39
+ "NullPrimaryKeyFieldValidator defined for data type %s but schema not found. "
40
+ + "Please check that the HollowProducer is initialized with "
41
+ + "the data type's schema (see initializeDataModel)" ;
42
+ private static final String NOT_AN_OBJECT_ERROR_MSG_FORMAT =
43
+ "NullPrimaryKeyFieldValidator is defined but schema type of %s is not Object. "
44
+ + "This validation cannot be done." ;
45
+
46
+ private static final String FIELD_PATH_NAME = "FieldPaths" ;
47
+ private static final String DATA_TYPE_NAME = "Typename" ;
48
+
49
+ private final String dataTypeName ;
50
+
51
+ /**
52
+ * Creates a validator to detect records with null primary key fields for the type
53
+ * that corresponds to the given data type class annotated with {@link HollowPrimaryKey}.
54
+ *
55
+ * @param dataType the data type class
56
+ * @throws IllegalArgumentException if the data type class is not annotated with {@link HollowPrimaryKey}
57
+ */
58
+ public NullPrimaryKeyFieldValidator (Class <?> dataType ) {
59
+ Objects .requireNonNull (dataType );
60
+
61
+ if (!dataType .isAnnotationPresent (HollowPrimaryKey .class )) {
62
+ throw new IllegalArgumentException ("The data class " +
63
+ dataType .getName () +
64
+ " must be annotated with @HollowPrimaryKey" );
65
+ }
66
+
67
+ this .dataTypeName = HollowTypeMapper .getDefaultTypeName (dataType );
68
+ }
69
+
70
+ /**
71
+ * Creates a validator to detect records with null primary keys of the type
72
+ * that corresponds to the given data type class annotated with {@link HollowPrimaryKey}.
73
+ * <p>
74
+ * The validator will fail, when {@link #onValidate validating}, if a schema with a
75
+ * primary key definition does not exist for the given data type name.
76
+ *
77
+ * @param dataTypeName the data type name
78
+ */
79
+ public NullPrimaryKeyFieldValidator (String dataTypeName ) {
80
+ this .dataTypeName = Objects .requireNonNull (dataTypeName );
81
+ }
82
+
83
+ @ Override
84
+ public String getName () {
85
+ return NAME + "_" + dataTypeName ;
86
+ }
87
+
88
+ @ Override
89
+ public ValidationResult onValidate (HollowProducer .ReadState readState ) {
90
+ ValidationResult .ValidationResultBuilder vrb = ValidationResult .from (this );
91
+ vrb .detail (DATA_TYPE_NAME , dataTypeName );
92
+
93
+ PrimaryKey primaryKey ;
94
+ HollowSchema schema = readState .getStateEngine ().getSchema (dataTypeName );
95
+ if (schema == null ) {
96
+ return vrb .failed (String .format (NO_SCHEMA_FOUND_MSG_FORMAT , dataTypeName ));
97
+ }
98
+ if (schema .getSchemaType () != HollowSchema .SchemaType .OBJECT ) {
99
+ return vrb .failed (String .format (NOT_AN_OBJECT_ERROR_MSG_FORMAT , dataTypeName ));
100
+ }
101
+
102
+ HollowObjectSchema oSchema = (HollowObjectSchema ) schema ;
103
+ primaryKey = oSchema .getPrimaryKey ();
104
+ if (primaryKey == null ) {
105
+ return vrb .failed (String .format (NO_PRIMARY_KEY_ERROR_MSG_FORMAT , dataTypeName ));
106
+ }
107
+
108
+ String fieldPaths = Arrays .toString (primaryKey .getFieldPaths ());
109
+ vrb .detail (FIELD_PATH_NAME , fieldPaths );
110
+
111
+ Map <Integer , Object []> ordinalToNullPkey = getNullPrimaryKeyValues (readState , primaryKey );
112
+ if (!ordinalToNullPkey .isEmpty ()) {
113
+ return vrb .failed (String .format (NULL_PRIMARY_KEYS_FOUND_ERROR_MSG_FORMAT ,
114
+ dataTypeName , fieldPaths ,
115
+ nullKeysToString (ordinalToNullPkey )));
116
+ }
117
+
118
+ return vrb .passed (getName () + "no records with null primary key fields" );
119
+ }
120
+
121
+ private Map <Integer , Object []> getNullPrimaryKeyValues (HollowProducer .ReadState readState , PrimaryKey primaryKey ) {
122
+ HollowReadStateEngine stateEngine = readState .getStateEngine ();
123
+ HollowObjectTypeReadState typeState = (HollowObjectTypeReadState ) stateEngine .getTypeState (dataTypeName );
124
+
125
+ int [][] fieldPathIndexes = new int [primaryKey .getFieldPaths ().length ][];
126
+ for (int i = 0 ; i < fieldPathIndexes .length ; i ++) {
127
+ fieldPathIndexes [i ] = primaryKey .getFieldPathIndex (stateEngine , i );
128
+ }
129
+
130
+ BitSet ordinals = typeState .getPopulatedOrdinals ();
131
+ int ordinal = ordinals .nextSetBit (0 );
132
+ Map <Integer , Object []> ordinalToNullPkey = new HashMap <>();
133
+ while (ordinal != ORDINAL_NONE ) {
134
+ Object [] primaryKeyValues = getPrimaryKeyValue (typeState , fieldPathIndexes , ordinal );
135
+ if (Arrays .stream (primaryKeyValues ).anyMatch (Objects ::isNull )) {
136
+ ordinalToNullPkey .put (ordinal , primaryKeyValues );
137
+ }
138
+ ordinal = ordinals .nextSetBit (ordinal + 1 );
139
+ }
140
+ return ordinalToNullPkey ;
141
+ }
142
+
143
+ private Object [] getPrimaryKeyValue (HollowObjectTypeReadState typeState , int [][] fieldPathIndexes , int ordinal ) {
144
+ Object [] results = new Object [fieldPathIndexes .length ];
145
+ for (int i = 0 ; i < fieldPathIndexes .length ; i ++) {
146
+ results [i ] = getPrimaryKeyFieldValue (typeState , fieldPathIndexes [i ], ordinal );
147
+ }
148
+
149
+ return results ;
150
+ }
151
+
152
+ private Object getPrimaryKeyFieldValue (HollowObjectTypeReadState typeState , int [] fieldPathIndexes , int ordinal ) {
153
+ HollowObjectSchema schema = typeState .getSchema ();
154
+ int lastFieldPath = fieldPathIndexes .length - 1 ;
155
+ for (int i = 0 ; i < lastFieldPath ; i ++) {
156
+ if (ordinal == ORDINAL_NONE ) {
157
+ // The ordinal must have referenced a null record.
158
+ return null ;
159
+ }
160
+ ordinal = typeState .readOrdinal (ordinal , fieldPathIndexes [i ]);
161
+ typeState = (HollowObjectTypeReadState ) schema .getReferencedTypeState (fieldPathIndexes [i ]);
162
+ schema = typeState .getSchema ();
163
+ }
164
+
165
+ if (ordinal == ORDINAL_NONE ) {
166
+ // The ordinal must have referenced a record with a null value for this field.
167
+ return null ;
168
+ }
169
+
170
+ return HollowReadFieldUtils .fieldValueObject (typeState , ordinal , fieldPathIndexes [lastFieldPath ]);
171
+ }
172
+
173
+ private String nullKeysToString (Map <Integer , Object []> nullPrimaryKeyValues ) {
174
+ return nullPrimaryKeyValues .entrySet ().stream ()
175
+ .map (entry -> {
176
+ return "(ordinal=" + entry .getKey () + ", key=" + Arrays .toString (entry .getValue ()) + ")" ;
177
+ })
178
+ .collect (Collectors .joining (", " ));
179
+ }
180
+ }
0 commit comments