1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.rice.krad.data.util;
17
18 import com.google.common.collect.Sets;
19 import org.apache.commons.collections.CollectionUtils;
20 import org.apache.commons.lang.ArrayUtils;
21 import org.apache.commons.lang.StringUtils;
22 import org.apache.commons.lang3.reflect.FieldUtils;
23 import org.kuali.rice.krad.data.DataObjectService;
24 import org.kuali.rice.krad.data.DataObjectWrapper;
25 import org.kuali.rice.krad.data.metadata.DataObjectAttributeRelationship;
26 import org.kuali.rice.krad.data.metadata.DataObjectCollection;
27 import org.kuali.rice.krad.data.metadata.DataObjectMetadata;
28 import org.kuali.rice.krad.data.metadata.DataObjectRelationship;
29 import org.kuali.rice.krad.data.metadata.MetadataChild;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32 import org.springframework.beans.PropertyAccessor;
33 import org.springframework.beans.PropertyAccessorUtils;
34 import org.springframework.core.annotation.AnnotationUtils;
35
36 import java.lang.reflect.Field;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Set;
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 public class ReferenceLinker {
75
76 private static final Logger LOG = LoggerFactory.getLogger(ReferenceLinker.class);
77
78 private DataObjectService dataObjectService;
79
80
81
82
83
84
85 public DataObjectService getDataObjectService() {
86 return dataObjectService;
87 }
88
89
90
91
92
93
94
95
96
97
98 public void setDataObjectService(DataObjectService dataObjectService) {
99 this.dataObjectService = dataObjectService;
100 }
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 public void linkChanges(Object rootObject, Set<String> changedPropertyPaths) {
120 if (rootObject == null || CollectionUtils.isEmpty(changedPropertyPaths)) {
121 return;
122 }
123 Class<?> type = rootObject.getClass();
124 if (!dataObjectService.supports(type)) {
125 LOG.info("Object supplied for linking is not a supported data object type: " + type);
126 return;
127 }
128 if (LOG.isInfoEnabled()) {
129 LOG.info("Performing linking on instance of " + type + " with the following changed property paths: " +
130 Arrays.toString(changedPropertyPaths.toArray()));
131 }
132 Map<String, Set<String>> decomposedPaths = decomposePropertyPaths(changedPropertyPaths);
133 linkChangesInternal(rootObject, decomposedPaths, new HashSet<Object>());
134 }
135
136
137
138
139
140
141
142
143
144
145
146 protected void linkChangesInternal(Object object, Map<String, Set<String>> changedPropertyPaths,
147 Set<Object> linked) {
148 if (object == null || linked.contains(object) || !dataObjectService.supports(object.getClass()) ||
149 changedPropertyPaths.isEmpty()) {
150 return;
151 }
152 linked.add(object);
153 DataObjectWrapper<?> wrapped = dataObjectService.wrap(object);
154
155
156 linkRelationshipChanges(wrapped, changedPropertyPaths, linked);
157 linkCollectionChanges(wrapped, changedPropertyPaths, linked);
158 cascadeLinkingAnnotations(wrapped, changedPropertyPaths, linked);
159 }
160
161
162
163
164
165
166
167
168 protected void linkRelationshipChanges(DataObjectWrapper<?> wrapped, Map<String, Set<String>> decomposedPaths,
169 Set<Object> linked) {
170 List<DataObjectRelationship> relationships = wrapped.getMetadata().getRelationships();
171 for (DataObjectRelationship relationship : relationships) {
172 String relationshipName = relationship.getName();
173
174
175 if (relationship.isSavedWithParent() && decomposedPaths.containsKey(relationshipName)) {
176 Object value = wrapped.getPropertyValue(relationshipName);
177 Map<String, Set<String>> nextPropertyPaths =
178 decomposePropertyPaths(decomposedPaths.get(relationshipName));
179 linkChangesInternal(value, nextPropertyPaths, linked);
180 }
181
182
183
184 List<DataObjectAttributeRelationship> attributeRelationships = relationship.getAttributeRelationships();
185 boolean modifiedAttributeRelationship = false;
186 for (DataObjectAttributeRelationship attributeRelationship : attributeRelationships) {
187 if (decomposedPaths.containsKey(attributeRelationship.getParentAttributeName())) {
188 modifiedAttributeRelationship = true;
189 break;
190 }
191 }
192 if (modifiedAttributeRelationship) {
193
194 wrapped.fetchRelationship(relationshipName, true, true);
195 } else if (decomposedPaths.containsKey(relationshipName)) {
196
197 Class<?> targetType = relationship.getRelatedType();
198 DataObjectMetadata targetMetadata =
199 dataObjectService.getMetadataRepository().getMetadata(targetType);
200 Set<String> modifiedPropertyPaths = decomposedPaths.get(relationshipName);
201 if (isPrimaryKeyModified(targetMetadata, modifiedPropertyPaths)) {
202
203
204 wrapped.fetchRelationship(relationshipName, false, false);
205 } else {
206
207
208
209 wrapped.linkForeignKeys(relationshipName, false);
210 }
211 }
212 }
213 }
214
215
216
217
218
219
220
221
222 protected void linkCollectionChanges(DataObjectWrapper<?> wrapped, Map<String, Set<String>> decomposedPaths,
223 Set<Object> linked) {
224 List<DataObjectCollection> collections = wrapped.getMetadata().getCollections();
225 for (DataObjectCollection collectionMetadata : collections) {
226
227 if (collectionMetadata.isSavedWithParent()) {
228 Set<Integer> modifiedIndicies = extractModifiedIndicies(collectionMetadata, decomposedPaths);
229 if (!modifiedIndicies.isEmpty()) {
230 Object collectionValue = wrapped.getPropertyValue(collectionMetadata.getName());
231 if (collectionValue instanceof Iterable<?>) {
232 int index = 0;
233
234 for (Object element : (Iterable<?>)collectionValue) {
235
236
237 if (modifiedIndicies.contains(Integer.valueOf(Integer.MAX_VALUE)) ||
238 modifiedIndicies.contains(Integer.valueOf(index))) {
239
240 String pathKey = collectionMetadata.getName() + "[" + index + "]";
241 if (decomposedPaths.containsKey(pathKey)) {
242 Map<String, Set<String>> nextPropertyPaths =
243 decomposePropertyPaths(decomposedPaths.get(pathKey));
244 linkChangesInternal(element, nextPropertyPaths, linked);
245 }
246 if (dataObjectService.supports(element.getClass())) {
247 DataObjectWrapper<?> elementWrapper = dataObjectService.wrap(element);
248 linkBiDirectionalCollection(wrapped, elementWrapper, collectionMetadata);
249 }
250 }
251 index++;
252 }
253 }
254 }
255 }
256 }
257 }
258
259
260
261
262
263
264
265
266
267 protected void linkBiDirectionalCollection(DataObjectWrapper<?> parentWrapper,
268 DataObjectWrapper<?> elementWrapper, DataObjectCollection collectionMetadata) {
269 MetadataChild inverseRelationship = collectionMetadata.getInverseRelationship();
270 if (inverseRelationship != null) {
271
272 elementWrapper.setPropertyValue(inverseRelationship.getName(), parentWrapper.getWrappedInstance());
273
274
275 elementWrapper.linkForeignKeys(inverseRelationship.getName(), false);
276 }
277 }
278
279
280
281
282
283
284
285
286
287
288
289
290 private Set<Integer> extractModifiedIndicies(DataObjectCollection collectionMetadata,
291 Map<String, Set<String>> decomposedPaths) {
292 String relationshipName = collectionMetadata.getName();
293 Set<Integer> modifiedIndicies = Sets.newHashSet();
294
295 if (decomposedPaths.containsKey(relationshipName)) {
296 modifiedIndicies.add(Integer.valueOf(Integer.MAX_VALUE));
297 }
298 for (String propertyName : decomposedPaths.keySet()) {
299 if (relationshipName.equals(PropertyAccessorUtils.getPropertyName(relationshipName))) {
300 Integer index = extractIndex(propertyName);
301 if (index != null) {
302 modifiedIndicies.add(index);
303 }
304 }
305 }
306 return modifiedIndicies;
307 }
308
309
310
311
312
313
314
315
316
317
318
319 private Integer extractIndex(String propertyName) {
320 int firstIndex = propertyName.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
321 int lastIndex = propertyName.lastIndexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR);
322 if (firstIndex != -1 && lastIndex != -1) {
323 String indexValue = propertyName.substring(firstIndex + 1, lastIndex);
324 try {
325 int index = Integer.parseInt(indexValue);
326 return Integer.valueOf(index);
327 } catch (NumberFormatException e) {
328
329 }
330 }
331 return null;
332 }
333
334
335
336
337
338
339 protected boolean isPrimaryKeyModified(DataObjectMetadata metadata, Set<String> modifiedPropertyPaths) {
340 Set<String> primaryKeyAttributeNames = new HashSet<String>(metadata.getPrimaryKeyAttributeNames());
341 for (String modifiedPropertyPath : modifiedPropertyPaths) {
342 if (primaryKeyAttributeNames.contains(modifiedPropertyPath)) {
343 return true;
344 }
345 }
346 return false;
347 }
348
349
350
351
352
353
354
355
356
357
358
359
360 protected void cascadeLinkingAnnotations(DataObjectWrapper<?> wrapped, Map<String, Set<String>> decomposedPaths,
361 Set<Object> linked) {
362 Field[] fields = FieldUtils.getAllFields(wrapped.getWrappedClass());
363 Map<String, Field> modifiedFieldMap = new HashMap<String, Field>();
364 for (Field field : fields) {
365 if (decomposedPaths.containsKey(field.getName())) {
366 modifiedFieldMap.put(field.getName(), field);
367 }
368 }
369 for (String modifiedFieldName : modifiedFieldMap.keySet()) {
370 Field modifiedField = modifiedFieldMap.get(modifiedFieldName);
371 Link link = modifiedField.getAnnotation(Link.class);
372 if (link == null) {
373
374 link = AnnotationUtils.findAnnotation(modifiedField.getType(), Link.class);
375 }
376 if (link != null && link.cascade()) {
377 List<String> linkingPaths = assembleLinkingPaths(link);
378 for (String linkingPath : linkingPaths) {
379 Map<String, Set<String>> decomposedLinkingPath =
380 decomposePropertyPaths(decomposedPaths.get(modifiedFieldName), linkingPath);
381 String valuePath = modifiedFieldName;
382 if (StringUtils.isNotBlank(linkingPath)) {
383 valuePath = valuePath + "." + link.path();
384 }
385 Object linkRootObject = wrapped.getPropertyValueNullSafe(valuePath);
386 linkChangesInternal(linkRootObject, decomposedLinkingPath, linked);
387 }
388 }
389 }
390 }
391
392
393
394
395
396
397
398 protected List<String> assembleLinkingPaths(Link link) {
399 List<String> linkingPaths = new ArrayList<String>();
400 if (ArrayUtils.isEmpty(link.path())) {
401 linkingPaths.add("");
402 } else {
403 for (String path : link.path()) {
404 linkingPaths.add(path);
405 }
406 }
407 return linkingPaths;
408 }
409
410
411
412
413
414
415
416 protected Map<String, Set<String>> decomposePropertyPaths(Set<String> changedPropertyPaths) {
417 return decomposePropertyPaths(changedPropertyPaths, "");
418 }
419
420
421
422
423
424
425
426
427 protected Map<String, Set<String>> decomposePropertyPaths(Set<String> changedPropertyPaths, String prefix) {
428
429 Set<String> processedProperties = new HashSet<String>();
430 if (StringUtils.isNotBlank(prefix) && changedPropertyPaths != null) {
431 for (String changedPropertyPath : changedPropertyPaths) {
432 if (changedPropertyPath.startsWith(prefix)) {
433 processedProperties.add(changedPropertyPath.substring(prefix.length() + 1));
434 }
435 }
436 } else {
437 processedProperties = changedPropertyPaths;
438 }
439 Map<String, Set<String>> decomposedPropertyPaths = new HashMap<String, Set<String>>();
440 for (String changedPropertyPath : processedProperties) {
441 int index = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(changedPropertyPath);
442 if (index == -1) {
443 decomposedPropertyPaths.put(changedPropertyPath, new HashSet<String>());
444 } else {
445 String pathEntry = changedPropertyPath.substring(0, index);
446 Set<String> remainingPaths = decomposedPropertyPaths.get(pathEntry);
447 if (remainingPaths == null) {
448 remainingPaths = new HashSet<String>();
449 decomposedPropertyPaths.put(pathEntry, remainingPaths);
450 }
451 String remainingPath = changedPropertyPath.substring(index + 1);
452 remainingPaths.add(remainingPath);
453 }
454 }
455 return decomposedPropertyPaths;
456 }
457
458 }