1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package ch.elca.el4j.services.persistence.generic.dao;
18
19 import java.lang.reflect.AccessibleObject;
20 import java.lang.reflect.Array;
21 import java.lang.reflect.Field;
22 import java.lang.reflect.Method;
23 import java.lang.reflect.Modifier;
24 import java.security.AccessController;
25 import java.security.PrivilegedAction;
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.HashMap;
29 import java.util.IdentityHashMap;
30 import java.util.List;
31 import java.util.Map;
32
33 import org.aopalliance.intercept.MethodInvocation;
34 import org.apache.commons.collections.map.AbstractReferenceMap;
35 import org.apache.commons.collections.map.ReferenceMap;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38 import org.springframework.aop.IntroductionInterceptor;
39 import org.springframework.aop.support.IntroductionInfoSupport;
40
41 import ch.elca.el4j.services.persistence.generic.dao.DaoChangeNotifier.NewEntityState;
42 import ch.elca.el4j.services.persistence.generic.dao.IdentityFixerMergePolicy.UpdatePolicy;
43 import ch.elca.el4j.services.persistence.generic.dao.annotations.ReturnsUnchangedParameter;
44 import ch.elca.el4j.services.persistence.generic.dao.impl.DefaultDaoChangeNotifier;
45 import ch.elca.el4j.util.codingsupport.AopHelper;
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 public abstract class AbstractIdentityFixer {
115
116
117
118 protected static final Object ANONYMOUS = new Object();
119
120
121 static Map<Class<?>, List<Field>> s_cachedFields
122 = new HashMap<Class<?>, List<Field>>();
123
124
125 static Logger s_logger = LoggerFactory.getLogger(AbstractIdentityFixer.class);
126
127
128 static ObjectIdentifier s_oi = new ObjectIdentifier();
129
130
131
132 int m_traceIndentation = 0;
133
134
135 DaoChangeNotifier m_changeNotifier;
136
137
138
139
140
141 Map<Object, Object> m_representatives;
142
143
144
145
146 IdentityHashMap<Collection<?>, Collection<?>> m_collectionMapping
147 = new IdentityHashMap<Collection<?>, Collection<?>>();
148
149
150
151
152
153 IdentityHashMap<Collection<?>, Collection<?>> m_reverseCollectionMapping
154 = new IdentityHashMap<Collection<?>, Collection<?>>();
155
156
157
158
159
160
161 HashMap<IdentityFixerCollectionField, Collection<?>> m_collectionsToBeReplaced
162 = new HashMap<IdentityFixerCollectionField, Collection<?>>();
163
164
165
166
167
168 public AbstractIdentityFixer() {
169 this(new DefaultDaoChangeNotifier());
170 }
171
172
173
174
175
176 @SuppressWarnings("unchecked")
177 public AbstractIdentityFixer(DaoChangeNotifier changeNotifier) {
178 m_changeNotifier = changeNotifier;
179 m_representatives = new ReferenceMap(
180 AbstractReferenceMap.HARD, AbstractReferenceMap.WEAK);
181 }
182
183
184
185
186
187
188 public DaoChangeNotifier getChangeNotifier() {
189 return m_changeNotifier;
190 }
191
192
193
194
195
196
197
198 protected static List<Field> instanceFields(Class<?> c) {
199 List<Field> fs = new ArrayList<Field>();
200 for (Class<?> sc = c; sc != null; sc = sc.getSuperclass()) {
201 for (Field f : sc.getDeclaredFields()) {
202 if (!Modifier.isStatic(f.getModifiers())) {
203 fs.add(f);
204 }
205 }
206 }
207 return fs;
208 }
209
210
211
212
213
214
215
216
217
218
219 protected static List<AccessibleObject>
220 instanceAccessibleObjects(Class<?> c) {
221 List<AccessibleObject> fs = new ArrayList<AccessibleObject>();
222
223
224 for (Class<?> sc = c; sc != null; sc = sc.getSuperclass()) {
225
226
227 for (Field f : sc.getDeclaredFields()) {
228 if (!Modifier.isStatic(f.getModifiers())) {
229 fs.add(f);
230 }
231 }
232
233
234 for (Method m : sc.getDeclaredMethods()) {
235 if (!Modifier.isStatic(m.getModifiers())) {
236 fs.add(m);
237 }
238 }
239 }
240 return fs;
241 }
242
243
244
245
246
247
248
249
250
251 private static List<Field> fields(Class<?> c) {
252 List<Field> fields = s_cachedFields.get(c);
253 if (fields == null) {
254 fields = instanceFields(c);
255 s_cachedFields.put(c, fields);
256
257 for (Field f : fields) {
258 if (!Modifier.isPublic(f.getModifiers())) {
259 final Field[] FIELDS
260 = fields.toArray(new Field[fields.size()]);
261 AccessController.doPrivileged(
262 new PrivilegedAction<Object>() {
263 public Object run() {
264 Field.setAccessible(FIELDS, true);
265 return null;
266 }
267 }
268 );
269 }
270 }
271 }
272 return fields;
273 }
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298 @Deprecated
299 @SuppressWarnings("unchecked")
300 protected <T> T merge(T anchor, T updated,
301 boolean isIdentical,
302 IdentityHashMap<Object, Object> reached,
303 List<Object> objectsToUpdate, IdentityHashMap<Object, Object> hintMapping) {
304
305
306 T prepUpdated = (T) prepareObject(updated);
307
308 if (immutableValue(prepUpdated)) {
309 trace("", prepUpdated, " is an immutable value");
310 return prepUpdated;
311 }
312
313 T attached = (T) reached.get(prepUpdated);
314 if (attached != null) {
315 trace("", prepUpdated, " was already merged to ", attached);
316 return attached;
317 }
318
319 boolean isNew = true;
320
321
322 Object id = id(prepUpdated);
323 if (isIdentical) {
324
325 attached = anchor;
326 assert id(anchor) == null
327 || id(anchor) == ANONYMOUS
328 || id(anchor).equals(id);
329 isNew = false;
330 } else {
331 if (id == ANONYMOUS) {
332 attached = anchor;
333 isNew = attached == null;
334 } else {
335
336 if (id != null) {
337 attached = (T) m_representatives.get(id);
338 isNew = (attached != null) ? false : true;
339 }
340
341 if (attached == prepUpdated) {
342 reached.put(prepUpdated, attached);
343 return attached;
344 }
345 }
346
347
348
349 if (attached == null) {
350 attached = prepUpdated;
351 }
352 }
353 assert attached != null;
354 if (id != ANONYMOUS && id != null) {
355 m_representatives.put(id, attached);
356 }
357
358
359 reached.put(prepUpdated, attached);
360
361 trace("merging ", prepUpdated, " to ", attached);
362 m_traceIndentation++;
363 if (prepUpdated.getClass().isArray()) {
364
365 int l = Array.getLength(prepUpdated);
366 assert Array.getLength(attached) == Array.getLength(prepUpdated);
367 for (int i = 0; i < l; i++) {
368 Array.set(
369 attached, i,
370 merge(
371 attached != null ? Array.get(attached, i) : null,
372 Array.get(prepUpdated, i),
373 attached != null && isIdentical,
374 reached,
375 objectsToUpdate,
376 hintMapping
377 )
378 );
379 }
380 } else if (attached instanceof Collection) {
381 Collection attachedCollection = (Collection) attached;
382 Collection updatedCollection = (Collection) prepUpdated;
383
384 List mergedEntries = new ArrayList(attachedCollection.size());
385
386 for (Object updatedObject : updatedCollection) {
387 Object anchorObject = null;
388 if (hintMapping != null) {
389 anchorObject = hintMapping.get(updatedObject);
390 }
391 mergedEntries.add(merge(anchorObject,
392 updatedObject, anchorObject != null && isIdentical, reached, objectsToUpdate, hintMapping));
393 }
394
395
396
397 if (objectsToUpdate == null || objectsToUpdate.contains(attachedCollection) || isNew) {
398
399
400
401 updatedCollection.clear();
402 updatedCollection.addAll(mergedEntries);
403 } else {
404
405 updatedCollection.clear();
406 updatedCollection.addAll(attachedCollection);
407 }
408 attached = (T) updatedCollection;
409 } else {
410 boolean isUpdateNeeded = objectsToUpdate == null || objectsToUpdate.contains(attached) || isNew;
411 for (Field f : fields(prepUpdated.getClass())) {
412 try {
413 Object fieldValue = f.get(prepUpdated);
414 Object fieldValue2 = (attached != null) ? f.get(attached) : null;
415 boolean collectionUpdate = fieldValue2 != null && fieldValue2 instanceof Collection
416 && id(fieldValue2) == ANONYMOUS;
417 if (objectsToUpdate != null && isUpdateNeeded && collectionUpdate) {
418 objectsToUpdate.add(fieldValue2);
419 }
420 Object merged = merge(
421 (isUpdateNeeded || collectionUpdate) ? fieldValue2 : null,
422 fieldValue,
423 fieldValue2 != null && isIdentical,
424 reached,
425 objectsToUpdate,
426 hintMapping
427 );
428
429
430
431
432
433 if (isUpdateNeeded || collectionUpdate) {
434
435 f.set(attached, merged);
436 }
437 } catch (IllegalAccessException e) { assert false : e; }
438 }
439 }
440 m_traceIndentation--;
441
442
443 NewEntityState e = new NewEntityState();
444 e.setChangee(attached);
445 m_changeNotifier.announce(e);
446
447 return attached;
448 }
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473 @SuppressWarnings("unchecked")
474 protected <T> T merge(T anchor, T updated, IdentityFixerMergePolicy policy, boolean isIdentical,
475 IdentityHashMap<Object, Object> reached, HashMap<Object, Object> locked) {
476
477
478 T referenceHolder = null;
479 T valueHolder = updated;
480 boolean identical = isIdentical;
481
482 if (policy.needsPreparation()) {
483
484 valueHolder = (T) prepareObject(valueHolder);
485 }
486
487
488 if (immutableValue(valueHolder)) {
489 trace("", valueHolder, " is an immutable value");
490 return valueHolder;
491 }
492
493
494 T savedState = (T) reached.get(valueHolder);
495 if (savedState != null) {
496 trace("", valueHolder, " was already merged to ", savedState);
497 return savedState;
498 }
499
500 if (identical) {
501
502 if (id(anchor) != null && m_representatives.get(id(anchor)) != null) {
503
504 s_logger.debug("Anchor was given for a representative already present, anchor is discarded!");
505 identical = false;
506 assert anchor == m_representatives.get(id(anchor));
507 }
508 }
509
510 boolean isNew = true;
511
512
513 Object valueHolderId = id(valueHolder);
514
515 if (identical) {
516
517 referenceHolder = anchor;
518 assert id(referenceHolder) == null
519 || id(referenceHolder) == ANONYMOUS
520 || id(referenceHolder).equals(valueHolderId);
521 isNew = false;
522 } else {
523 if (valueHolderId == ANONYMOUS) {
524
525 referenceHolder = anchor;
526 isNew = (referenceHolder == null);
527 } else {
528
529 if (valueHolderId != null) {
530 referenceHolder = (T) m_representatives.get(valueHolderId);
531 isNew = (referenceHolder == null);
532 }
533
534
535
536 if (referenceHolder == valueHolder && reached.containsValue(referenceHolder)) {
537 reached.put(valueHolder, referenceHolder);
538 return referenceHolder;
539 }
540 }
541
542
543 if (referenceHolder == null) {
544 referenceHolder = valueHolder;
545 }
546 }
547 assert referenceHolder != null;
548
549 if (valueHolderId != null && valueHolderId != ANONYMOUS) {
550
551 if (locked.get(valueHolderId) != null && locked.get(valueHolderId) != valueHolder) {
552 return referenceHolder;
553 }
554 if (isNew || identical) {
555 m_representatives.put(valueHolderId, referenceHolder);
556 }
557 }
558
559
560 reached.put(valueHolder, referenceHolder);
561
562 trace("merging ", valueHolder, " to ", referenceHolder);
563 m_traceIndentation++;
564 if (valueHolder.getClass().isArray()) {
565
566 assert Array.getLength(referenceHolder) == Array.getLength(valueHolder);
567 for (int i = 0; i < Array.getLength(valueHolder); i++) {
568 Array.set(
569 referenceHolder, i,
570 merge(
571 Array.get(referenceHolder, i),
572 Array.get(valueHolder, i),
573 policy,
574 identical,
575 reached,
576 locked
577 )
578 );
579 }
580 } else if (valueHolder instanceof Collection) {
581 Collection savedCollection = (Collection) referenceHolder;
582 Collection updateCollection = (Collection) valueHolder;
583
584 List mergedEntries;
585
586
587 boolean isInPolicy = false;
588 try {
589 isInPolicy = policy.getObjectsToUpdate().contains(savedCollection);
590 } catch (Exception e) {
591 isInPolicy = false;
592 }
593 if (policy.getUpdatePolicy() == UpdatePolicy.UPDATE_ALL
594 || (policy.getUpdatePolicy() == UpdatePolicy.UPDATE_CHOSEN
595 && isInPolicy)
596 || isNew || immutableValue(savedCollection)) {
597
598
599 mergedEntries = new ArrayList(updateCollection.size());
600 for (Object updatedObject : updateCollection) {
601 Object anchorObject = null;
602 if (policy.getCollectionEntryMapping() != null) {
603 anchorObject = policy.getCollectionEntryMapping().get(updatedObject);
604 }
605 mergedEntries.add(merge(anchorObject, updatedObject, policy,
606 anchorObject != null && identical, reached, locked));
607 }
608 if (needsAdditionalProcessing(updateCollection)) {
609
610 Collection<?> restoreCollection = m_reverseCollectionMapping.get(savedCollection);
611 if (restoreCollection != null) {
612 m_reverseCollectionMapping.remove(savedCollection);
613 referenceHolder = (T) restoreCollection;
614 savedCollection = restoreCollection;
615
616 }
617 if (savedCollection != updateCollection) {
618
619 m_collectionMapping.put(savedCollection, updateCollection);
620 }
621
622 }
623 if (immutableValue(savedCollection)) {
624
625 referenceHolder = (T) updateCollection;
626 savedCollection = updateCollection;
627 }
628 savedCollection.clear();
629 savedCollection.addAll(mergedEntries);
630
631 } else {
632 mergedEntries = new ArrayList(savedCollection.size());
633 for (Object updatedObject : savedCollection) {
634 Object anchorObject = null;
635 if (policy.getCollectionEntryMapping() != null) {
636 anchorObject = policy.getCollectionEntryMapping().get(updatedObject);
637 }
638 mergedEntries.add(merge(anchorObject, updatedObject, policy,
639 anchorObject != null && identical, reached, locked));
640 }
641 savedCollection.clear();
642 savedCollection.addAll(mergedEntries);
643 }
644
645 } else {
646 boolean isUpdateNeeded = policy.getUpdatePolicy() == UpdatePolicy.UPDATE_ALL
647 || (policy.getUpdatePolicy() == UpdatePolicy.UPDATE_CHOSEN
648 && policy.getObjectsToUpdate().contains(referenceHolder))
649 || isNew;
650 for (Field f : fields(valueHolder.getClass())) {
651 try {
652 Object fieldValueNew = f.get(valueHolder);
653 Object fieldValueOld = f.get(referenceHolder);
654 boolean collectionUpdate = fieldValueOld != null && fieldValueOld instanceof Collection
655 && id(fieldValueOld) == ANONYMOUS;
656 if (policy.getUpdatePolicy() == UpdatePolicy.UPDATE_CHOSEN && collectionUpdate) {
657 policy.getObjectsToUpdate().add(fieldValueOld);
658 }
659 boolean updateAnyway = false;
660 if (collectionUpdate && needsAdditionalProcessing(fieldValueNew)) {
661
662 IdentityFixerCollectionField idcf = new IdentityFixerCollectionField(referenceHolder, f);
663 Collection<?> replaceCollection = m_collectionsToBeReplaced.get(idcf);
664 if (replaceCollection != null) {
665 updateAnyway = true;
666 fieldValueOld = replaceCollection;
667 m_collectionsToBeReplaced.remove(idcf);
668 }
669 }
670 if (collectionUpdate && immutableValue(fieldValueOld)) {
671 updateAnyway = true;
672 }
673 Object merged = merge(
674 fieldValueOld,
675 fieldValueNew,
676 policy,
677 fieldValueOld != null && identical,
678 reached,
679 locked
680 );
681
682
683
684
685
686 if (isUpdateNeeded || updateAnyway) {
687
688 f.set(referenceHolder, merged);
689 }
690 } catch (IllegalAccessException e) { assert false : e; }
691 }
692 }
693 m_traceIndentation--;
694
695
696 NewEntityState e = new NewEntityState();
697 e.setChangee(referenceHolder);
698 m_changeNotifier.announce(e);
699
700 return referenceHolder;
701 }
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718 public <T> T merge(T anchor, T updated) {
719 HashMap<Object, Object> locked = new HashMap<Object, Object>();
720 if (updated instanceof Collection) {
721 for (Object o : (Collection<?>) updated) {
722 Object id = id(o);
723 if (id != null && id != ANONYMOUS) {
724 locked.put(id, o);
725 }
726 }
727 }
728 T result = merge(
729 anchor,
730 updated,
731 IdentityFixerMergePolicy.reloadAllPolicy(),
732 anchor != null,
733 new IdentityHashMap<Object, Object>(),
734 locked
735 );
736 return result;
737 }
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759 public <T> T merge(T anchor, T updated, IdentityFixerMergePolicy policy) {
760 HashMap<Object, Object> locked = new HashMap<Object, Object>();
761 if (updated instanceof Collection) {
762 for (Object o : (Collection<?>) updated) {
763 Object id = id(o);
764 if (id != null && id != ANONYMOUS) {
765 locked.put(id, o);
766 }
767 }
768 }
769 T result = merge(
770 anchor,
771 updated,
772 policy,
773 anchor != null,
774 new IdentityHashMap<Object, Object>(),
775 locked
776 );
777 return result;
778 }
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795 @SuppressWarnings("unchecked")
796 protected Object reverseMerge(Object object, IdentityHashMap<Object, Object> reached, boolean mergeRecursive) {
797
798 if (immutableValue(object)) {
799 return object;
800 }
801
802 if (reached.get(object) == object) {
803 return object;
804 }
805
806 if (mergeRecursive) {
807 reached.put(object, object);
808 }
809
810
811 for (Field f : fields(object.getClass())) {
812 try {
813 Object fieldValue = f.get(object);
814 if (fieldValue instanceof Collection) {
815 Collection fieldCollection = (Collection) fieldValue;
816 Collection mappedCollection = m_collectionMapping.get(fieldCollection);
817 if (mappedCollection != null && fieldCollection != mappedCollection) {
818
819 f.set(object, mappedCollection);
820 List<Object> tmpList = new ArrayList<Object>(fieldCollection);
821 mappedCollection.clear();
822 mappedCollection.addAll(tmpList);
823 m_reverseCollectionMapping.put(mappedCollection, fieldCollection);
824 } else {
825
826 m_collectionsToBeReplaced.put(new IdentityFixerCollectionField(object, f), fieldCollection);
827 }
828 if (mergeRecursive) {
829 for (Object o : fieldCollection) {
830 reverseMerge(o, reached, mergeRecursive);
831 }
832 }
833 } else if (mergeRecursive && fieldValue != null) {
834 if (fieldValue.getClass().isArray()) {
835 int l = Array.getLength(fieldValue);
836 for (int i = 0; i < l; i++) {
837 reverseMerge(Array.get(fieldValue, i), reached, mergeRecursive);
838 }
839 } else {
840 reverseMerge(fieldValue, reached, mergeRecursive);
841 }
842 }
843 } catch (IllegalAccessException e) { assert false : e; }
844 }
845
846
847 return object;
848 }
849
850
851
852
853
854
855
856
857
858
859 public Object reverseMerge(Object object, boolean mergeRecursive) {
860 return reverseMerge(object,
861 new IdentityHashMap<Object, Object>(),
862 mergeRecursive
863 );
864 }
865
866
867
868
869
870
871
872
873
874 public List<Object> reverseMerge(List<Object> objects) {
875 List<Object> returnList = new ArrayList<Object>(objects.size());
876 for (Object o : objects) {
877 returnList.add(reverseMerge(o, false));
878 }
879 return returnList;
880 }
881
882
883
884
885
886 public boolean isRepresentative(Object object) {
887 return m_representatives.containsValue(object);
888 }
889
890
891
892
893 public Collection<?> getRepresentatives() {
894 return m_representatives.values();
895 }
896
897
898
899
900
901 public void removeRepresentative(Object object) {
902 Object id = id(object);
903 if (m_representatives.get(id) != null && m_representatives.get(id).equals(object)) {
904
905 m_collectionsToBeReplaced.remove(id);
906 m_representatives.remove(id);
907 }
908 }
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929 protected abstract Object id(Object o);
930
931
932
933
934
935
936
937
938
939
940
941
942 protected abstract boolean immutableValue(Object o);
943
944
945
946
947
948
949
950
951
952 protected abstract Object prepareObject(Object o);
953
954
955
956
957
958
959 protected boolean needsAdditionalProcessing(Object o) {
960 return false;
961 }
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977 public class GenericInterceptor
978 extends IntroductionInfoSupport
979 implements IntroductionInterceptor {
980
981
982
983
984
985 @SuppressWarnings("unchecked")
986 public GenericInterceptor(Class<?> fixedInterface) {
987 publishedInterfaces.add(fixedInterface);
988 }
989
990
991 @SuppressWarnings("unchecked")
992 public Object invoke(MethodInvocation invocation) throws Throwable {
993 ReturnsUnchangedParameter rp
994 = invocation.getMethod().getAnnotation(
995 ReturnsUnchangedParameter.class
996 );
997 if (rp != null) {
998 Object arg = invocation.getArguments()[rp.value()];
999 if (arg instanceof List) {
1000 reverseMerge((List) arg);
1001 } else {
1002 reverseMerge(arg, true);
1003 }
1004 Object result = invocation.proceed();
1005 return merge(arg, result);
1006 } else {
1007 Object result = invocation.proceed();
1008 return merge(null, result);
1009 }
1010 }
1011
1012
1013
1014
1015
1016
1017
1018 public Object decorate(Object o) {
1019 return AopHelper.addAdvice(o, this);
1020 }
1021 }
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035 void trace(Object... os) {
1036 if (s_logger.isDebugEnabled()) {
1037 StringBuilder sb = new StringBuilder();
1038 for (int i = 0; i < m_traceIndentation; i++) {
1039 sb.append(" ");
1040 }
1041 boolean isLiteral = false;
1042 for (Object o : os) {
1043 isLiteral = o instanceof String && !isLiteral;
1044 if (isLiteral) {
1045 sb.append(o);
1046 } else {
1047 s_oi.format(o, sb);
1048 }
1049 }
1050 s_logger.debug(sb.toString());
1051 }
1052 }
1053
1054
1055
1056
1057
1058 private static class ObjectIdentifier {
1059
1060 int m_maxid = 0;
1061
1062
1063 IdentityHashMap<Object, Integer> m_seen
1064 = new IdentityHashMap<Object, Integer>();
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076 public StringBuilder format(Object obj, StringBuilder toAppendTo) {
1077 if (obj == null) {
1078 return toAppendTo.append("null");
1079 } else {
1080 Integer id = m_seen.get(obj);
1081 if (id == null) {
1082 id = m_maxid++;
1083 m_seen.put(obj, id);
1084 }
1085 return toAppendTo.append(obj.getClass().getSimpleName())
1086 .append(id.toString());
1087 }
1088 }
1089 }
1090 }