• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CeON / dataverse / 986

pending completion
986

push

jenkins

GitHub
Closes #2340: Fix NPE when exporting dataset with empty vocabulary field (#2346)

4 of 4 new or added lines in 1 file covered. (100.0%)

21097 of 68968 relevant lines covered (30.59%)

0.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.24
/dataverse-webapp/src/main/java/edu/harvard/iq/dataverse/api/dto/DatasetFieldDTO.java
1
package edu.harvard.iq.dataverse.api.dto;
2

3
import com.fasterxml.jackson.annotation.JsonAutoDetect;
4
import com.fasterxml.jackson.annotation.JsonIgnore;
5
import com.fasterxml.jackson.annotation.JsonInclude;
6
import edu.harvard.iq.dataverse.persistence.dataset.ControlledVocabularyValue;
7
import edu.harvard.iq.dataverse.persistence.dataset.DatasetField;
8
import edu.harvard.iq.dataverse.persistence.dataset.DatasetFieldType;
9
import edu.harvard.iq.dataverse.persistence.dataset.DatasetFieldUtil;
10
import edu.harvard.iq.dataverse.persistence.dataset.DatasetFieldsByType;
11
import edu.harvard.iq.dataverse.persistence.dataset.FieldType;
12
import io.vavr.control.Option;
13

14
import java.util.ArrayList;
15
import java.util.Collections;
16
import java.util.Comparator;
17
import java.util.LinkedHashMap;
18
import java.util.LinkedHashSet;
19
import java.util.List;
20
import java.util.Map;
21
import java.util.Objects;
22
import java.util.Set;
23
import java.util.stream.Collectors;
24

25
@JsonInclude(JsonInclude.Include.NON_EMPTY)
26
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
27
public class DatasetFieldDTO {
28
    public static final String PRIMITIVE = "primitive";
29
    public static final String VOCABULARY = "controlledVocabulary";
30
    public static final String COMPOUND = "compound";
31

32
    private String typeName;
33
    private Boolean multiple;
34
    private String typeClass;
35

36
    /**
37
     * This property can hold one of following types:
38
     * <ul>
39
     *     <li> {@code String} – for primitive/vocabulary value of non-multiple field
40
     *     <li> {@code List<String>} – for multiple primitive/vocabulary values
41
     *     <li> {@code Map<String, DatasetFieldDTO>} – for non-multiple compound field
42
     *     <li> {@code List<Map<String, DatasetFieldDTO>>} – for multiple compound field
43
     * </ul>
44
     */
45
    private Object value;
46

47
    @JsonIgnore
48
    private boolean emailType;
49

50
    // -------------------- CONSTRUCTORS --------------------
51

52
    public DatasetFieldDTO() { }
1✔
53

54
    public DatasetFieldDTO(String typeName, Boolean multiple, String typeClass, Object value) {
1✔
55
        this.typeName = typeName;
1✔
56
        this.multiple = multiple;
1✔
57
        this.typeClass = typeClass;
1✔
58
        this.value = value;
1✔
59
    }
1✔
60

61
    // -------------------- GETTERS --------------------
62

63
    public String getTypeName() {
64
        return typeName;
1✔
65
    }
66

67
    public Boolean getMultiple() {
68
        return multiple;
1✔
69
    }
70

71
    public String getTypeClass() {
72
        return typeClass;
1✔
73
    }
74

75
    public Object getValue() {
76
        return value;
1✔
77
    }
78

79
    @JsonIgnore
80
    public boolean isEmailType() {
81
        return emailType;
1✔
82
    }
83

84
    // -------------------- LOGIC --------------------
85

86
    public String getSinglePrimitive() {
87
        return value == null ? "" : (String) value;
1✔
88
    }
89

90
    public String getSingleVocabulary() {
91
        return getSinglePrimitive();
1✔
92
    }
93

94
    /**
95
     * Removes all email-type subfields from compound field.
96
     * @return true if after email fields removal there are no other subfields
97
     *  left and the field should be removed. False otherwise.
98
     */
99
    public boolean clearEmailSubfields() {
100
        if (!COMPOUND.equals(typeClass) || value == null) {
1✔
101
            return false;
1✔
102
        }
103
        if (Map.class.isAssignableFrom(value.getClass())) {
1✔
104
            return clearEmailSubfields(value).isEmpty();
1✔
105
        }
106
        List<?> valueList = (List<?>) value;
1✔
107
        for (Object element : valueList) {
1✔
108
            if (!Map.class.isAssignableFrom(element.getClass())) {
1✔
109
                break;
×
110
            } else {
111
                clearEmailSubfields(element);
1✔
112
            }
113
        }
1✔
114
        valueList.removeIf(f -> Map.class.isAssignableFrom(f.getClass()) && ((Map<?, ?>) f).isEmpty());
1✔
115
        return valueList.isEmpty();
1✔
116
    }
117

118
    public Set<DatasetFieldDTO> getSingleCompound() {
119
        return value != null
1✔
120
                ? new LinkedHashSet<>(((Map<String, DatasetFieldDTO>) value).values())
1✔
121
                : Collections.emptySet();
×
122
    }
123

124
    public List<String> getMultiplePrimitive() {
125
        return value != null
1✔
126
                ? (List<String>) value : Collections.emptyList();
×
127
    }
128

129
    public List<String> getMultipleVocabulary() {
130
        return getMultiplePrimitive();
1✔
131
    }
132

133
    public List<Set<DatasetFieldDTO>> getMultipleCompound() {
134
        if (value == null) {
1✔
135
            return Collections.emptyList();
×
136
        }
137
        List<Map<String, DatasetFieldDTO>> fieldList = (List<Map<String, DatasetFieldDTO>>) value;
1✔
138
        return fieldList.stream()
1✔
139
                .map(v -> new LinkedHashSet<>(v.values()))
1✔
140
                .collect(Collectors.toList());
1✔
141
    }
142

143
    // -------------------- PRIVATE --------------------
144

145
    private Map<String, DatasetFieldDTO> clearEmailSubfields(Object value) {
146
        Map<String, DatasetFieldDTO> subfieldsMap = (Map<String, DatasetFieldDTO>) value;
1✔
147
        subfieldsMap.values().removeIf(DatasetFieldDTO::isEmailType);
1✔
148
        return subfieldsMap;
1✔
149
    }
150

151
    // -------------------- SETTERS --------------------
152

153
    public void setTypeName(String typeName) {
154
        this.typeName = typeName;
1✔
155
    }
1✔
156

157
    public void setMultiple(Boolean multiple) {
158
        this.multiple = multiple;
1✔
159
    }
1✔
160

161
    public void setTypeClass(String typeClass) {
162
        this.typeClass = typeClass;
1✔
163
    }
1✔
164

165
    public void setValue(Object value) {
166
        this.value = value;
1✔
167
    }
1✔
168

169
    public void setEmailType(boolean emailType) {
170
        this.emailType = emailType;
1✔
171
    }
1✔
172

173
    // -------------------- hashCode & equals --------------------
174

175
    @Override
176
    public int hashCode() {
177
        return Objects.hash(typeName, multiple, typeClass, value);
1✔
178
    }
179

180
    @Override
181
    public boolean equals(Object other) {
182
        if (this == other) {
1✔
183
            return true;
×
184
        }
185
        if (other == null || getClass() != other.getClass()) {
1✔
186
            return false;
×
187
        }
188
        DatasetFieldDTO that = (DatasetFieldDTO) other;
1✔
189
        return Objects.equals(typeName, that.typeName) &&
1✔
190
                Objects.equals(multiple, that.multiple) &&
1✔
191
                Objects.equals(typeClass, that.typeClass) &&
1✔
192
                Objects.equals(value, that.value);
1✔
193
    }
194

195
    // -------------------- INNER CLASSES --------------------
196

197
    public static class Creator {
1✔
198

199
        // -------------------- LOGIC --------------------
200

201
        public List<DatasetFieldDTO> create(List<DatasetField> datasetFields) {
202
            datasetFields.sort(Comparator.comparing(DatasetField::getDatasetFieldTypeDisplayOrder));
1✔
203
            datasetFields.forEach(f -> f.getDatasetFieldsChildren()
1✔
204
                    .sort(Comparator.comparing(DatasetField::getDatasetFieldTypeDisplayOrder)));
1✔
205
            List<DatasetFieldDTO> fields = new ArrayList<>();
1✔
206
            for (DatasetFieldsByType fieldsByType : DatasetFieldUtil.groupByType(datasetFields)) {
1✔
207
                DatasetFieldType fieldType = fieldsByType.getDatasetFieldType();
1✔
208
                DatasetFieldDTO field = createForType(fieldType);
1✔
209
                List<DatasetField> fieldsOfType = fieldsByType.getDatasetFields();
1✔
210
                List<?> values = Collections.emptyList();
1✔
211
                if (fieldType.isControlledVocabulary()) {
1✔
212
                    values = fieldsOfType.stream()
1✔
213
                            .flatMap(f -> f.getControlledVocabularyValues().stream())
1✔
214
                            .sorted(ControlledVocabularyValue.DisplayOrder)
1✔
215
                            .map(ControlledVocabularyValue::getStrValue)
1✔
216
                            .collect(Collectors.toList());
1✔
217
                } else if (fieldType.isPrimitive()) {
1✔
218
                    values = fieldsOfType.stream()
1✔
219
                            .map(DatasetField::getFieldValue)
1✔
220
                            .filter(Option::isDefined)
1✔
221
                            .map(Option::get)
1✔
222
                            .collect(Collectors.toList());
1✔
223
                } else if (fieldType.isCompound()) {
1✔
224
                    values = fieldsOfType.stream()
1✔
225
                            .map(this::extractChildren)
1✔
226
                            .collect(Collectors.toList());
1✔
227
                }
228
                if (values.isEmpty()) {
1✔
229
                    continue;
×
230
                }
231
                field.setValue(extractValue(fieldType, values));
1✔
232
                fields.add(field);
1✔
233
            }
1✔
234
            return fields;
1✔
235
        }
236

237
        // -------------------- PRIVATE --------------------
238

239
        private DatasetFieldDTO createForType(DatasetFieldType fieldType) {
240
            DatasetFieldDTO field = new DatasetFieldDTO();
1✔
241
            field.setTypeName(fieldType.getName());
1✔
242
            field.setMultiple(fieldType.isAllowMultiples());
1✔
243
            field.setTypeClass(fieldType.isControlledVocabulary()
1✔
244
                    ? VOCABULARY
245
                    : fieldType.isCompound()
1✔
246
                    ? COMPOUND : PRIMITIVE);
247
            field.setEmailType(fieldType.getFieldType() == FieldType.EMAIL);
1✔
248
            return field;
1✔
249
        }
250

251
        private Object extractChildren(DatasetField datasetField) {
252
            Map<String, DatasetFieldDTO> children = new LinkedHashMap<>();
1✔
253
            for(DatasetField child : datasetField.getDatasetFieldsChildren()) {
1✔
254
                DatasetFieldType fieldType = child.getDatasetFieldType();
1✔
255
                DatasetFieldDTO field = createForType(fieldType);
1✔
256
                if (fieldType.isControlledVocabulary()) {
1✔
257
                    List<String> values = child.getControlledVocabularyValues().stream()
×
258
                            .sorted(Comparator.comparing(ControlledVocabularyValue::getDisplayOrder))
×
259
                            .map(ControlledVocabularyValue::getStrValue)
×
260
                            .collect(Collectors.toList());
×
261
                    if (values.isEmpty()) {
×
262
                        continue;
×
263
                    }
264
                    field.setValue(extractValue(fieldType, values));
×
265
                } else if (fieldType.isPrimitive()) {
1✔
266
                    field.setValue(child.getFieldValue().getOrElse((String) null));
1✔
267
                }
268
                children.put(field.getTypeName(), field);
1✔
269
            }
1✔
270
            return children;
1✔
271
        }
272

273
        private <T> Object extractValue(DatasetFieldType fieldType, List<T> values) {
274
            return fieldType.isAllowMultiples() || values.size() > 1
1✔
275
                    ? values : values.get(0);
1✔
276
        }
277
    }
278
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc