Skip to content

Commit 23d177b

Browse files
awellesspvillard31
authored andcommitted
NIFI-14491 Support other types in CreateBoxFileMetadataInstance
Signed-off-by: Pierre Villard <[email protected]> This closes #9891.
1 parent a3249e9 commit 23d177b

File tree

6 files changed

+179
-11
lines changed

6 files changed

+179
-11
lines changed

nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/CreateBoxFileMetadataInstance.java

+50-6
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,20 @@
3737
import org.apache.nifi.processor.Relationship;
3838
import org.apache.nifi.processor.exception.ProcessException;
3939
import org.apache.nifi.processor.util.StandardValidators;
40+
import org.apache.nifi.processors.box.utils.BoxDate;
4041
import org.apache.nifi.serialization.RecordReader;
4142
import org.apache.nifi.serialization.RecordReaderFactory;
4243
import org.apache.nifi.serialization.record.Record;
44+
import org.apache.nifi.serialization.record.RecordField;
45+
import org.apache.nifi.serialization.record.RecordFieldType;
4346

4447
import java.io.InputStream;
48+
import java.time.LocalDate;
4549
import java.util.ArrayList;
50+
import java.util.Arrays;
4651
import java.util.List;
4752
import java.util.Map;
53+
import java.util.Objects;
4854
import java.util.Set;
4955

5056
import static java.lang.String.valueOf;
@@ -223,20 +229,58 @@ private void processRecord(Record record, Metadata metadata, List<String> errors
223229
return;
224230
}
225231

226-
List<String> fieldNames = record.getSchema().getFieldNames();
232+
final List<RecordField> fields = record.getSchema().getFields();
227233

228-
if (fieldNames.isEmpty()) {
234+
if (fields.isEmpty()) {
229235
errors.add("Record has no fields");
230236
return;
231237
}
232238

233-
for (String fieldName : fieldNames) {
234-
Object valueObj = record.getValue(fieldName);
235-
String value = valueObj != null ? valueObj.toString() : null;
236-
metadata.add("/" + fieldName, value);
239+
for (final RecordField field : fields) {
240+
addValueToMetadata(metadata, record, field);
237241
}
238242
}
239243

244+
private void addValueToMetadata(final Metadata metadata, final Record record, final RecordField field) {
245+
if (record.getValue(field) == null) {
246+
return;
247+
}
248+
249+
final RecordFieldType fieldType = field.getDataType().getFieldType();
250+
final String fieldName = field.getFieldName();
251+
final String path = "/" + fieldName;
252+
253+
if (isNumber(fieldType)) {
254+
metadata.add(path, record.getAsDouble(fieldName));
255+
} else if (isDate(fieldType)) {
256+
final LocalDate date = record.getAsLocalDate(fieldName, null);
257+
metadata.add(path, BoxDate.of(date).format());
258+
} else if (isArray(fieldType)) {
259+
final List<String> values = Arrays.stream(record.getAsArray(fieldName))
260+
.filter(Objects::nonNull)
261+
.map(Object::toString)
262+
.toList();
263+
264+
metadata.add(path, values);
265+
} else {
266+
metadata.add(path, record.getAsString(fieldName));
267+
}
268+
}
269+
270+
private boolean isNumber(final RecordFieldType fieldType) {
271+
final boolean isInteger = RecordFieldType.BIGINT.equals(fieldType) || RecordFieldType.BIGINT.isWiderThan(fieldType);
272+
final boolean isFloat = RecordFieldType.DECIMAL.equals(fieldType) || RecordFieldType.DECIMAL.isWiderThan(fieldType);
273+
return isInteger || isFloat;
274+
}
275+
276+
private boolean isDate(final RecordFieldType fieldType) {
277+
return RecordFieldType.DATE.equals(fieldType);
278+
}
279+
280+
private boolean isArray(final RecordFieldType fieldType) {
281+
return RecordFieldType.ARRAY.equals(fieldType);
282+
}
283+
240284
/**
241285
* Returns a BoxFile object for the given file ID.
242286
*

nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/main/java/org/apache/nifi/processors/box/UpdateBoxFileMetadataInstance.java

+16-2
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@
3737
import org.apache.nifi.processor.Relationship;
3838
import org.apache.nifi.processor.exception.ProcessException;
3939
import org.apache.nifi.processor.util.StandardValidators;
40+
import org.apache.nifi.processors.box.utils.BoxDate;
4041
import org.apache.nifi.serialization.RecordReader;
4142
import org.apache.nifi.serialization.RecordReaderFactory;
4243
import org.apache.nifi.serialization.record.Record;
44+
import org.apache.nifi.serialization.record.RecordField;
45+
import org.apache.nifi.serialization.record.RecordFieldType;
4346

4447
import java.io.InputStream;
48+
import java.time.LocalDate;
4549
import java.util.HashMap;
4650
import java.util.List;
4751
import java.util.Map;
@@ -216,8 +220,16 @@ private Map<String, Object> readDesiredState(final ProcessSession session,
216220

217221
final Record record = recordReader.nextRecord();
218222
if (record != null) {
219-
for (String fieldName : record.getSchema().getFieldNames()) {
220-
desiredState.put(fieldName, record.getValue(fieldName));
223+
final List<RecordField> fields = record.getSchema().getFields();
224+
for (final RecordField field : fields) {
225+
final String fieldName = field.getFieldName();
226+
final RecordFieldType type = field.getDataType().getFieldType();
227+
228+
final Object value = RecordFieldType.DATE.equals(type)
229+
? record.getAsLocalDate(fieldName, null) // Ensuring dates are read as LocalDate.
230+
: record.getValue(field);
231+
232+
desiredState.put(fieldName, value);
221233
}
222234
}
223235
}
@@ -269,13 +281,15 @@ private void updateField(final Metadata metadata,
269281
switch (value) {
270282
case Number n -> metadata.replace(propertyPath, n.doubleValue());
271283
case List<?> l -> metadata.replace(propertyPath, convertListToStringList(l, propertyPath));
284+
case LocalDate d -> metadata.replace(propertyPath, BoxDate.of(d).format());
272285
default -> metadata.replace(propertyPath, value.toString());
273286
}
274287
} else {
275288
// Add new field
276289
switch (value) {
277290
case Number n -> metadata.add(propertyPath, n.doubleValue());
278291
case List<?> l -> metadata.add(propertyPath, convertListToStringList(l, propertyPath));
292+
case LocalDate d -> metadata.add(propertyPath, BoxDate.of(d).format());
279293
default -> metadata.add(propertyPath, value.toString());
280294
}
281295
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.processors.box.utils;
18+
19+
import java.time.LocalDate;
20+
import java.time.format.DateTimeFormatter;
21+
22+
/**
23+
* A wrapper class for formatting {@link LocalDate} to a string that is accepted by Box Metadata API.
24+
*/
25+
public final class BoxDate {
26+
27+
// The time part is always set to 0. https://developer.box.com/guides/metadata/fields/date/
28+
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T00:00:00.000Z'");
29+
30+
private final LocalDate date;
31+
32+
private BoxDate(final LocalDate date) {
33+
this.date = date;
34+
}
35+
36+
public static BoxDate of(final LocalDate date) {
37+
return new BoxDate(date);
38+
}
39+
40+
public String format() {
41+
return DATE_FORMATTER.format(date);
42+
}
43+
}

nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/CreateBoxFileMetadataInstanceTest.java

+26-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@
3131
import org.mockito.Mock;
3232
import org.mockito.junit.jupiter.MockitoExtension;
3333

34+
import java.text.ParseException;
35+
import java.time.Instant;
36+
import java.time.LocalDate;
37+
import java.util.Date;
38+
import java.util.List;
39+
40+
import static java.time.ZoneOffset.UTC;
3441
import static org.junit.jupiter.api.Assertions.assertEquals;
3542
import static org.mockito.ArgumentMatchers.any;
3643
import static org.mockito.Mockito.doThrow;
@@ -75,14 +82,20 @@ private void configureJsonRecordReader(TestRunner runner) throws InitializationE
7582
}
7683

7784
@Test
78-
void testSuccessfulMetadataCreation() {
85+
void testSuccessfulMetadataCreation() throws ParseException {
7986
final String inputJson = """
8087
{
8188
"audience": "internal",
8289
"documentType": "Q1 plans",
8390
"competitiveDocument": "no",
8491
"status": "active",
85-
"author": "Jones"
92+
"author": "Jones",
93+
"int": 1,
94+
"double": 1.234,
95+
"almostTenToThePowerOfThirty": 1000000000000000000000000000123,
96+
"array": [ "one", "two", "three" ],
97+
"intArray": [ 1, 2, 3 ],
98+
"date": "2025-01-01"
8699
}""";
87100

88101
testRunner.enqueue(inputJson);
@@ -97,6 +110,12 @@ void testSuccessfulMetadataCreation() {
97110
assertEquals("no", capturedMetadata.getValue("/competitiveDocument").asString());
98111
assertEquals("active", capturedMetadata.getValue("/status").asString());
99112
assertEquals("Jones", capturedMetadata.getValue("/author").asString());
113+
assertEquals(1, capturedMetadata.getValue("/int").asInt());
114+
assertEquals(1.234, capturedMetadata.getDouble("/double"));
115+
assertEquals(1e30, capturedMetadata.getDouble("/almostTenToThePowerOfThirty")); // Precision loss is accepted.
116+
assertEquals(List.of("one", "two", "three"), capturedMetadata.getMultiSelect("/array"));
117+
assertEquals(List.of("1", "2", "3"), capturedMetadata.getMultiSelect("/intArray"));
118+
assertEquals(createLegacyDate(2025, 1, 1), capturedMetadata.getDate("/date"));
100119

101120
testRunner.assertAllFlowFilesTransferred(CreateBoxFileMetadataInstance.REL_SUCCESS, 1);
102121
final MockFlowFile flowFile = testRunner.getFlowFilesForRelationship(CreateBoxFileMetadataInstance.REL_SUCCESS).getFirst();
@@ -157,4 +176,9 @@ void testTemplateNotFound() {
157176
flowFile.assertAttributeEquals(BoxFileAttributes.ERROR_MESSAGE, "API Error [404]");
158177
}
159178

179+
private static Date createLegacyDate(int year, int month, int day) {
180+
final LocalDate date = LocalDate.of(year, month, day);
181+
final Instant instant = date.atStartOfDay(UTC).toInstant();
182+
return Date.from(instant);
183+
}
160184
}

nifi-extension-bundles/nifi-box-bundle/nifi-box-processors/src/test/java/org/apache/nifi/processors/box/UpdateBoxFileMetadataInstanceTest.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ void setUp() throws Exception {
9191
private void configureJsonRecordReader(TestRunner runner) throws InitializationException {
9292
final JsonTreeReader readerService = new JsonTreeReader();
9393
runner.addControllerService("json-reader", readerService);
94+
runner.setProperty(readerService, "Date Format", "yyyy-MM-dd");
9495
runner.enableControllerService(readerService);
9596
}
9697

@@ -290,7 +291,8 @@ void testAddingDifferentDataTypes() {
290291
"doubleField": 42.5,
291292
"booleanField": true,
292293
"listField": ["item1", "item2", "item3"],
293-
"emptyListField": []
294+
"emptyListField": [],
295+
"date": "2025-01-01"
294296
}""";
295297

296298
testRunner.enqueue(inputJson);
@@ -301,6 +303,7 @@ void testAddingDifferentDataTypes() {
301303
verify(mockMetadata).add("/numberField", 42.0); // Numbers are stored as doubles
302304
verify(mockMetadata).add("/doubleField", 42.5);
303305
verify(mockMetadata).add("/booleanField", "true"); // Booleans are stored as strings
306+
verify(mockMetadata).add("/date", "2025-01-01T00:00:00.000Z"); // Dates have a specific format.
304307
// We need to use doAnswer/when to capture and verify list fields being added, but this is simpler
305308

306309
verify(mockBoxFile).updateMetadata(any(Metadata.class));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.processors.box.utils;
18+
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.CsvSource;
21+
22+
import java.time.LocalDate;
23+
24+
import static org.junit.jupiter.api.Assertions.assertEquals;
25+
26+
class BoxDateTest {
27+
28+
@ParameterizedTest
29+
@CsvSource({
30+
"2025-01-01, 2025-01-01T00:00:00.000Z",
31+
"2025-02-25, 2025-02-25T00:00:00.000Z",
32+
"2025-11-10, 2025-11-10T00:00:00.000Z",
33+
})
34+
void format(final LocalDate date, final String expected) {
35+
final BoxDate boxDate = BoxDate.of(date);
36+
final String formatted = boxDate.format();
37+
38+
assertEquals(expected, formatted);
39+
}
40+
}

0 commit comments

Comments
 (0)