-
Notifications
You must be signed in to change notification settings - Fork 5
/
Factory.java
244 lines (220 loc) · 9.21 KB
/
Factory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/*
* © 2021. TU Dortmund University,
* Institute of Energy Systems, Energy Efficiency and Energy Economics,
* Research group Distribution grid planning and operation
*/
package edu.ie3.datamodel.io.factory;
import edu.ie3.datamodel.exceptions.FactoryException;
import edu.ie3.datamodel.io.source.SourceValidator;
import edu.ie3.datamodel.utils.Try;
import edu.ie3.datamodel.utils.Try.*;
import edu.ie3.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract factory class, that is able to transfer specific "flat" information in to actual model
* class instances.
*
* @param <C> Type of the intended target class.
* @param <D> Type of the "flat" information.
* @param <R> Type of the intended return type (might differ slightly from target class (cf. {@link
* edu.ie3.datamodel.io.factory.timeseries.TimeBasedValueFactory})).
*/
public abstract class Factory<C, D extends FactoryData, R> implements SourceValidator<C> {
public static final Logger log = LoggerFactory.getLogger(Factory.class);
private final List<Class<? extends C>> supportedClasses;
@SafeVarargs
protected Factory(Class<? extends C>... supportedClasses) {
this.supportedClasses = Arrays.asList(supportedClasses);
}
public List<Class<? extends C>> getSupportedClasses() {
return supportedClasses;
}
/**
* Builds entity with data from given EntityData object after doing all kinds of checks on the
* data
*
* @param data EntityData (or subclass) containing the data
* @return An entity wrapped in a {@link Success} if successful, or an exception wrapped in a
* {@link Failure}
*/
public Try<R, FactoryException> get(D data) {
isSupportedClass(data.getTargetClass());
try {
// build the model
return Success.of(buildModel(data));
} catch (FactoryException | IllegalArgumentException e) {
return Failure.of(
new FactoryException(
"An error occurred when creating instance of "
+ data.getTargetClass().getSimpleName()
+ ".class.",
e));
}
}
/**
* Builds entity with data from given EntityData object after doing all kinds of checks on the
* data
*
* @param data EntityData (or subclass) containing the data wrapped in a {@link Try}
* @return An entity wrapped in a {@link Success} if successful, or an exception wrapped in a
* {@link Failure}
*/
public Try<R, FactoryException> get(Try<D, ?> data) {
return data.transformF(FactoryException::new).flatMap(this::get);
}
/**
* Builds model with data from given {@link FactoryData} object. Throws {@link FactoryException}
* if something goes wrong.
*
* @param data {@link FactoryData} (or subclass) containing the data
* @return model created from data
* @throws FactoryException if the model cannot be build
*/
protected abstract R buildModel(D data);
/**
* Checks, if the specific given class can be handled by this factory.
*
* @param desiredClass Class that should be built
*/
private void isSupportedClass(Class<?> desiredClass) {
if (!supportedClasses.contains(desiredClass))
throw new FactoryException(
"Cannot process "
+ desiredClass.getSimpleName()
+ ".class with this factory!\nThis factory can only process the following classes:\n - "
+ supportedClasses.stream()
.map(Class::getSimpleName)
.collect(Collectors.joining("\n - ")));
}
/**
* Returns list of sets of attribute names that the entity requires to be built. At least one of
* these sets needs to be delivered for entity creation to be successful.
*
* @param entityClass class that can be used to specify the fields that are returned
* @return list of possible attribute sets
*/
protected abstract List<Set<String>> getFields(Class<?> entityClass);
/**
* Method to find and return additional fields that were found in a source. This method will
* return the additional fields that were found in the valid set with the least additional fields.
*
* @param actualFields found in the source
* @param validFieldSets that contains at least all fields found in the source
* @return a set of additional fields
*/
protected Set<String> getAdditionalFields(
Set<String> actualFields, List<Set<String>> validFieldSets) {
// checking for additional fields
// and returning the set with the least additional fields
return validFieldSets.stream()
.map(
s -> {
Set<String> set = new HashSet<>(actualFields);
set.removeAll(s);
return set;
})
.min(Comparator.comparing(Collection::size))
.orElse(Collections.emptySet());
}
/**
* Method for validating the found fields. The found fields needs to fully contain at least one of
* the sets returned by {@link #getFields(Class)}. If the found fields don't contain all necessary
* fields, an {@link FactoryException} with a detail message is thrown. If the found fields
* contain more fields than necessary, these fields are ignored.
*
* @param actualFields that were found
* @param entityClass of the build data
*/
public void validate(Set<String> actualFields, Class<? extends C> entityClass) {
List<Set<String>> fieldSets = getFields(entityClass);
Set<String> harmonizedFoundFields = toCamelCase(actualFields);
// comparing the found fields to a list of possible fields (allows additional fields)
// if not all fields were found in a set, this set is filtered out
// all other fields are saved as a list
// allows snake, camel and mixed cases
List<Set<String>> validFieldSets =
fieldSets.stream().filter(harmonizedFoundFields::containsAll).toList();
if (validFieldSets.isEmpty()) {
// build the exception string with extensive debug information
String providedKeysString = "[" + String.join(", ", actualFields) + "]";
String possibleOptions = getFieldsString(fieldSets).toString();
throw new FactoryException(
"The provided fields "
+ providedKeysString
+ " are invalid for instance of '"
+ entityClass.getSimpleName()
+ "'. \nThe following fields (without complex objects e.g. nodes, operators, ...) to be passed to a constructor of '"
+ entityClass.getSimpleName()
+ "' are possible (NOT case-sensitive!):\n"
+ possibleOptions);
} else {
Set<String> additionalFields = getAdditionalFields(harmonizedFoundFields, validFieldSets);
if (!additionalFields.isEmpty()) {
log.debug(
"The following additional fields were found for entity class of '{}': {}",
entityClass.getSimpleName(),
additionalFields);
}
}
}
protected static StringBuilder getFieldsString(List<Set<String>> fieldSets) {
StringBuilder possibleOptions = new StringBuilder();
for (int i = 0; i < fieldSets.size(); i++) {
Set<String> fieldSet = fieldSets.get(i);
String option =
i
+ ": ["
+ String.join(", ", fieldSet)
+ "] or ["
+ String.join(", ", toSnakeCase(fieldSet))
+ "]\n";
possibleOptions.append(option);
}
return possibleOptions;
}
/**
* Creates a new set of attribute names from given list of attributes. This method should always
* be used when returning attribute sets, i.e. through {@link #getFields(Class)}.
*
* @param attributes attribute names
* @return new set exactly containing attribute names
*/
protected TreeSet<String> newSet(String... attributes) {
TreeSet<String> set = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
set.addAll(Arrays.asList(attributes));
return set;
}
/**
* Expands a set of attributes with further attributes. This method should always be used when
* returning attribute sets, i.e. through getting the needed fields. The set maintains a
* lexicographic order, that is case-insensitive.
*
* @param attributeSet set of attributes to expand
* @param more attribute names to expand given set with
* @return new set exactly containing given attribute set plus additional attributes
*/
protected TreeSet<String> expandSet(Set<String> attributeSet, String... more) {
TreeSet<String> newSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
newSet.addAll(attributeSet);
newSet.addAll(Arrays.asList(more));
return newSet;
}
protected static Set<String> toSnakeCase(Set<String> set) {
TreeSet<String> newSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
newSet.addAll(set.stream().map(StringUtils::camelCaseToSnakeCase).toList());
return newSet;
}
protected static Set<String> toCamelCase(Set<String> set) {
TreeSet<String> newSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
newSet.addAll(set.stream().map(StringUtils::snakeCaseToCamelCase).toList());
return newSet;
}
protected static Set<String> toLowerCase(Set<String> set) {
TreeSet<String> newSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
newSet.addAll(set.stream().map(String::toLowerCase).toList());
return newSet;
}
}