001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2012 GRANITE DATA SERVICES S.A.S.
004    
005      This file is part of Granite Data Services.
006    
007      Granite Data Services is free software; you can redistribute it and/or modify
008      it under the terms of the GNU Library General Public License as published by
009      the Free Software Foundation; either version 2 of the License, or (at your
010      option) any later version.
011    
012      Granite Data Services is distributed in the hope that it will be useful, but
013      WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014      FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015      for more details.
016    
017      You should have received a copy of the GNU Library General Public License
018      along with this library; if not, see <http://www.gnu.org/licenses/>.
019    */
020    
021    package org.granite.client.tide.data.impl;
022    
023    import java.lang.reflect.Array;
024    import java.util.ArrayList;
025    import java.util.Arrays;
026    import java.util.Collection;
027    import java.util.Date;
028    import java.util.HashMap;
029    import java.util.IdentityHashMap;
030    import java.util.Iterator;
031    import java.util.List;
032    import java.util.Map;
033    import java.util.Map.Entry;
034    import java.util.Set;
035    
036    import org.granite.client.persistence.LazyableCollection;
037    import org.granite.client.tide.PropertyHolder;
038    import org.granite.client.tide.data.Identifiable;
039    import org.granite.client.tide.data.Lazyable;
040    import org.granite.client.tide.data.spi.DataManager;
041    import org.granite.client.tide.data.spi.DataManager.ChangeKind;
042    import org.granite.client.tide.data.spi.DirtyCheckContext;
043    import org.granite.client.tide.data.spi.EntityDescriptor;
044    import org.granite.client.tide.data.spi.ExpressionEvaluator.Value;
045    import org.granite.client.tide.data.spi.MergeContext;
046    import org.granite.client.tide.data.spi.Wrapper;
047    import org.granite.client.tide.server.TrackingContext;
048    import org.granite.client.util.WeakIdentityHashMap;
049    import org.granite.logging.Logger;
050    
051    /**
052     * @author William DRAI
053     */
054    public class DirtyCheckContextImpl implements DirtyCheckContext {
055        
056        private static Logger log = Logger.getLogger("org.granite.client.tide.data.DirtyCheckContextImpl");
057        
058        private DataManager dataManager;
059        private TrackingContext trackingContext;
060        private int dirtyCount = 0;
061        private WeakIdentityHashMap<Object, Map<String, Object>> savedProperties = new WeakIdentityHashMap<Object, Map<String, Object>>();
062        private WeakIdentityHashMap<Object, Object> unsavedEntities = new WeakIdentityHashMap<Object, Object>();
063        
064        
065        public DirtyCheckContextImpl(DataManager dataManager, TrackingContext trackingContext) {
066            this.dataManager = dataManager;
067            this.trackingContext = trackingContext;
068        }
069    
070        @Override
071        public void setTrackingContext(TrackingContext trackingContext) {
072            this.trackingContext = trackingContext;
073        }
074        
075        public boolean isDirty() {
076            return dirtyCount > 0;
077        }
078        
079        public void notifyDirtyChange(boolean oldDirty) {
080            if (isDirty() == oldDirty)
081                return;
082            
083            dataManager.notifyDirtyChange(oldDirty, isDirty());
084        }
085        
086        public boolean notifyEntityDirtyChange(Object entity, boolean oldDirtyEntity) {
087            boolean newDirtyEntity = isEntityChanged(entity);
088            if (newDirtyEntity != oldDirtyEntity)
089                dataManager.notifyEntityDirtyChange(entity, oldDirtyEntity, newDirtyEntity);
090            return newDirtyEntity;
091        }
092        
093        public Map<String, Object> getSavedProperties(Object entity) {
094            return savedProperties.get(entity);
095        }
096        
097        public boolean isSaved(Object entity) {
098            return savedProperties.containsKey(entity);
099        }
100            
101            /**
102             *  Check if the object is marked as new in the context
103             *
104             *  @param object object to check
105             * 
106             *  @return true if the object has been newly attached
107             */ 
108            public boolean isUnsaved(Object object) {
109                    return unsavedEntities.containsKey(object);
110            }
111            
112            public void addUnsaved(Identifiable entity) {
113                    unsavedEntities.put(entity, true);
114            }
115        
116        public void clear(boolean notify) {
117            boolean wasDirty = isDirty();
118            dirtyCount = 0;
119            savedProperties.clear();
120            if (notify)
121                notifyDirtyChange(wasDirty);
122        }
123        
124        /**
125         *  Check if entity property has been changed since last remote call
126         *
127         *  @param entity entity to check
128         *  @param propertyName property to check
129         *  @param value current value to compare with saved value
130         *   
131         *  @return true is value has been changed
132         */ 
133            public boolean isEntityPropertyChanged(Identifiable entity, String propertyName, Object value) {
134            Map<String, Object> source = savedProperties.get(entity);
135            if (source != null)
136                    return source.containsKey(propertyName) && !isSame(source.get(propertyName), value);
137            
138                    return !isSame(dataManager.getProperty(entity, propertyName), value);
139        }
140        
141        
142        public boolean isEntityChanged(Object entity) {
143            return isEntityChanged(entity, null, null, null);
144        }
145        
146        /**
147         *  Check if entity has changed since last save point
148         *
149         *  @param entity entity to check
150         *  @param propName property name
151         *  @param value
152         *   
153         *  @return entity is dirty
154         */ 
155        @SuppressWarnings("unchecked")
156            public boolean isEntityChanged(Object entity, Object embedded, String propName, Object value) {
157            boolean saveTracking = trackingContext.isEnabled();
158            try {
159                trackingContext.setEnabled(false);
160                
161                boolean dirty = false;
162                
163                Map<String, Object> pval = dataManager.getPropertyValues(entity, false, false);
164                
165                EntityDescriptor desc = entity instanceof Identifiable ? dataManager.getEntityDescriptor(entity) : null;
166                Map<String, Object> save = savedProperties.get(entity);
167                String versionPropertyName = desc != null ? desc.getVersionPropertyName() : null;
168                String dirtyPropertyName = desc != null ? desc.getDirtyPropertyName() : null;
169                            
170                            if (embedded == null)
171                                    embedded = entity;
172                
173                for (String p : pval.keySet()) {
174                    if (p.equals(versionPropertyName) || p.equals(dirtyPropertyName))
175                        continue;
176                    
177                    Object val = (entity == embedded && p.equals(propName)) ? value : pval.get(p);
178                    Object saveval = save != null ? save.get(p) : null;
179                    
180                    if (save != null && ((val != null && (ObjectUtil.isSimple(val) || val instanceof byte[]))
181                            || (saveval != null && (ObjectUtil.isSimple(saveval) || saveval instanceof byte[])))) {
182                        dirty = true;
183                        break;
184                    }
185                    else if (save != null && (val instanceof Value || saveval instanceof Value || val instanceof Enum || saveval instanceof Enum)) {
186                        if (saveval != null && ((val == null && saveval != null) || !val.equals(saveval))) {
187                            dirty = true;
188                            break;
189                        }
190                    }
191                    else if (save != null && (val instanceof Identifiable || saveval instanceof Identifiable)) {
192                        if (saveval != null && val != save.get(p)) {
193                            dirty = true;
194                            break;
195                        }
196                    }
197                    else if ((val instanceof List<?> || val instanceof Map<?, ?>) && !(val instanceof LazyableCollection && !((LazyableCollection)val).isInitialized())) {
198                        List<Change> savedArray = (List<Change>)saveval;
199                        if (savedArray != null && !savedArray.isEmpty()) {
200                            dirty = true;
201                            break;
202                        }
203                    }
204                    else if (val != null
205                        && !(val instanceof Identifiable || val instanceof Enum || val instanceof Value || val instanceof byte[]) 
206                        && isEntityChanged(val)) {
207                        dirty = true;
208                        break;
209                    }
210                }
211                return dirty;
212            }
213            finally {
214                trackingContext.setEnabled(saveTracking);
215            }
216        }
217            
218            public boolean isEntityDeepChanged(Object entity) {             
219                    return isEntityDeepChanged(entity, null, new IdentityHashMap<Object, Boolean>());
220            }
221            
222            private boolean isEntityDeepChanged(Object entity, Object embedded, IdentityHashMap<Object, Boolean> cache) {
223                    if (cache == null)
224                            cache = new IdentityHashMap<Object, Boolean>();
225                    if (cache.containsKey(entity))
226                            return false;
227                    cache.put(entity, true);
228                    
229                    boolean saveTracking = trackingContext.isEnabled();
230                    try {
231                            trackingContext.setEnabled(false);
232                            
233                Map<String, Object> pval = dataManager.getPropertyValues(entity, false, false);
234                
235                if (embedded == null)
236                    embedded = entity;
237                
238                EntityDescriptor desc = entity instanceof Identifiable ? dataManager.getEntityDescriptor(entity) : null;
239                Map<String, Object> save = savedProperties.get(entity);
240                String versionPropertyName = desc != null ? desc.getVersionPropertyName() : null;
241                String dirtyPropertyName = desc != null ? desc.getDirtyPropertyName() : null;
242                
243                for (String p : pval.keySet()) {
244                    if (p.equals(versionPropertyName) || p.equals(dirtyPropertyName))
245                        continue;
246                    
247                    Object val = pval.get(p);
248                    Object saveval = save != null ? save.get(p) : null;
249                    
250                    if (save != null && ((val != null && (ObjectUtil.isSimple(val) || val instanceof byte[]))
251                            || (saveval != null && (ObjectUtil.isSimple(saveval) || saveval instanceof byte[])))) {
252                        return true;
253                    }
254                    else if (save != null && (val instanceof Value || saveval instanceof Value || val instanceof Enum || saveval instanceof Enum)) {
255                        if (saveval != null && ((val == null && saveval != null) || !val.equals(saveval))) {
256                            return true;
257                        }
258                    }
259                    else if (save != null && (val instanceof Identifiable || saveval instanceof Identifiable)) {
260                        if (saveval != null && val != save.get(p))
261                            return true;
262                        
263                        if (isEntityDeepChanged(val, null, cache))
264                                                    return true;
265                    }
266                    else if (val instanceof List<?> || val instanceof Map<?, ?>) {
267                             if (val instanceof LazyableCollection && !((LazyableCollection)val).isInitialized())
268                                     return false;
269                             
270                             @SuppressWarnings("unchecked")
271                             List<Change> savedArray = (List<Change>)saveval;
272                             if (savedArray != null && !savedArray.isEmpty())
273                                     return true;
274                             
275                             if (val instanceof List<?>) {
276                                     for (Object elt : (List<?>)val) {
277                                             if (isEntityDeepChanged(elt, null, cache))
278                                                     return true;
279                                     }
280                             }
281                             else if (val instanceof Map<?, ?>) {
282                                                    for (Entry<?, ?> me : ((Map<?, ?>)val).entrySet()) {
283                                                            if (isEntityDeepChanged(me.getKey(), null, cache))
284                                                                    return true;
285                                                            if (isEntityDeepChanged(me.getValue(), null, cache))
286                                                                    return true;
287                                                    }
288                                            }
289                    }
290                    else if (val != null
291                        && !(val instanceof Identifiable || val instanceof Enum || val instanceof Value || val instanceof byte[]) 
292                        && isEntityDeepChanged(val, embedded, cache)) {
293                        return true;
294                    }
295                            }
296                    }
297                    finally {            
298                            trackingContext.setEnabled(saveTracking);
299                    }
300                    
301                    return false;
302            }
303    
304    
305        private boolean isSame(Object val1, Object val2) {
306            if (val1 == null && isEmpty(val2))
307                return true;
308            else if (val2 == null && isEmpty(val1))
309                return true;
310            else if (ObjectUtil.isSimple(val1) && ObjectUtil.isSimple(val2))
311                return val1.equals(val2);
312            else if (val1 instanceof byte[] && val2 instanceof byte[])
313                return Arrays.equals((byte[])val1, (byte[])val2);
314            else if ((val1 instanceof Value && val2 instanceof Value) || (val1 instanceof Enum && val2 instanceof Enum))
315                return val1.equals(val2);
316            
317            Object n = val1 instanceof Wrapper ? ((Wrapper)val1).getWrappedObject() : val1;
318            Object o = val2 instanceof Wrapper ? ((Wrapper)val2).getWrappedObject() : val2;
319            if (n instanceof Identifiable && o instanceof Identifiable)
320                return ((Identifiable)n).getUid() != null && ((Identifiable)n).getUid().equals(((Identifiable)o).getUid());
321            return n == o;
322        }
323    
324        private boolean isSameList(List<Object> save, Collection<?> coll) {
325            if (save.size() != coll.size())
326                    return false;
327    
328            if (coll instanceof List<?>) {
329                    List<?> list = (List<?>)coll;
330                    for (int i = 0; i < save.size(); i++) {
331                            if (!isSame(save.get(i), list.get(i)))
332                                    return false;
333                    }
334            }
335            else {
336                    for (Object e : save) {
337                            if (!coll.contains(e))
338                                    return false;
339                    }
340            }
341            return true;
342        }    
343        
344        private boolean isSameMap(List<Object[]> save, Map<?, ?> map) {
345            if (save.size() != map.size())
346                    return false;
347    
348            for (int i = 0; i < save.size(); i++) {
349                    Object[] entry = save.get(i);
350                    if (!map.containsKey(entry[0]))
351                            return false;
352                    if (!isSame(entry[1], map.get(entry[0])))
353                            return false;
354            }
355            return true;
356        }
357        
358        private boolean isSameExt(Object val1, Object val2) {
359            if (val1 == null && isEmpty(val2))
360                return true;
361            else if (val2 == null && isEmpty(val1))
362                return true;
363            else if (ObjectUtil.isSimple(val1) && ObjectUtil.isSimple(val2))
364                return val1.equals(val2);
365            else if (val1 instanceof byte[] && val2 instanceof byte[])
366                return Arrays.equals((byte[])val1, (byte[])val2);
367            else if ((val1 instanceof Value && val2 instanceof Value) || (val1 instanceof Enum && val2 instanceof Enum))
368                return val1.equals(val2);
369            else if (val1 != null && val1.getClass().isArray() && val2 != null && val2.getClass().isArray()) {
370                if (Array.getLength(val1) != Array.getLength(val2))
371                    return false;
372                for (int idx = 0; idx < Array.getLength(val1); idx++) {
373                    if (!isSameExt(Array.get(val1, idx), Array.get(val2, idx)))
374                        return false;
375                }
376                return true;
377            }
378            else if (val1 instanceof Set<?> && val2 instanceof Set<?>) {
379                            if ((val1 instanceof LazyableCollection && !((LazyableCollection)val1).isInitialized()) 
380                                            || (val2 instanceof LazyableCollection && !((LazyableCollection)val2).isInitialized()))
381                                    return false;
382                Collection<?> coll1 = (Collection<?>)val1;
383                Collection<?> coll2 = (Collection<?>)val2;
384                if (coll1.size() != coll2.size())
385                    return false;
386                for (Object e : coll1) {
387                    boolean found = false;
388                    for (Object f : coll2) {
389                        if (isSameExt(e, f)) {
390                            found = true;
391                            break;
392                        }
393                    }
394                    if (!found)
395                        return false;
396                }
397                for (Object e : coll2) {
398                    boolean found = false;
399                    for (Object f : coll1) {
400                        if (isSameExt(e, f)) {
401                            found = true;
402                            break;
403                        }
404                    }
405                    if (!found)
406                        return false;
407                }
408                return true;
409            }
410            else if (val1 instanceof List<?> && val2 instanceof List<?>) {
411                            if ((val1 instanceof LazyableCollection && !((LazyableCollection)val1).isInitialized()) 
412                                            || (val2 instanceof LazyableCollection && !((LazyableCollection)val2).isInitialized()))
413                                    return false;
414                List<?> list1 = (List<?>)val1;
415                List<?> list2 = (List<?>)val2;
416                if (list1.size() != list2.size())
417                    return false;
418                for (int idx = 0; idx < list1.size(); idx++) {
419                    if (!isSameExt(list1.get(idx), list2.get(idx)))
420                        return false;
421                }
422                return true;
423            }
424            else if (val1 instanceof Map<?, ?> && val2 instanceof Map<?, ?>) {
425                            if ((val1 instanceof LazyableCollection && !((LazyableCollection)val1).isInitialized()) 
426                                            || (val2 instanceof LazyableCollection && !((LazyableCollection)val2).isInitialized()))
427                                    return false;
428                Map<?, ?> map1 = (Map<?, ?>)val1;
429                Map<?, ?> map2 = (Map<?, ?>)val2;
430                if (map1.size() != map2.size())
431                    return false;
432                for (Object e : map1.keySet()) {
433                    Object key = null;
434                    for (Object f : map2.keySet()) {
435                        if (isSameExt(e, f)) {
436                            key = f;
437                            break;
438                        }
439                    }
440                    if (key == null)
441                        return false;
442                    if (!isSameExt(map1.get(e), map2.get(key)))
443                        return false;
444                }
445                for (Object f : map2.keySet()) {
446                    Object key = null;
447                    for (Object e : map1.keySet()) {
448                        if (isSameExt(e, f)) {
449                            key = e;
450                            break;
451                        }
452                    }
453                    if (key == null)
454                        return false;
455                    if (!isSameExt(map1.get(key), map2.get(f)))
456                        return false;
457                }
458                return true;
459            }
460    
461            Object n = val1 instanceof Wrapper ? ((Wrapper)val1).getWrappedObject() : val1;
462            Object o = val2 instanceof Wrapper ? ((Wrapper)val2).getWrappedObject() : val2;
463            if (n instanceof Identifiable && o instanceof Identifiable)
464                return ((Identifiable)n).getUid().equals(((Identifiable)o).getUid());
465            
466            return n == o;
467        }
468    
469        /**
470         *  @private 
471         *  Interceptor for managed entity setters
472         *
473         *  @param entity entity to intercept
474         *  @param propName property name
475         *  @param oldValue old value
476         *  @param newValue new value
477         */ 
478        public void entityPropertyChangeHandler(Object entity, Object object, String propName, Object oldValue, Object newValue) {
479            boolean oldDirty = isDirty();
480            
481            boolean diff = !isSame(oldValue, newValue);
482            
483            if (diff) {
484                boolean oldDirtyEntity = isEntityChanged(entity, object, propName, oldValue);
485                
486                EntityDescriptor desc = dataManager.getEntityDescriptor(entity);
487                Map<String, Object> save = savedProperties.get(object);
488                boolean unsaved = save == null;
489                
490                if (unsaved || (desc.getVersionPropertyName() != null && 
491                    save.get(desc.getVersionPropertyName()) != dataManager.getProperty(entity, desc.getVersionPropertyName()) 
492                        && !(save.get(desc.getVersionPropertyName()) == null && dataManager.getProperty(entity, desc.getVersionPropertyName()) == null))) {
493                    
494                    save = new HashMap<String, Object>();
495                    if (desc.getVersionPropertyName() != null)
496                        save.put(desc.getVersionPropertyName(), dataManager.getProperty(entity, desc.getVersionPropertyName()));
497                    savedProperties.put(object, save);
498                    save.put(propName, oldValue);
499                    if (unsaved)
500                        dirtyCount++;
501                }
502                
503                if (save != null && (desc.getVersionPropertyName() == null 
504                    || save.get(desc.getVersionPropertyName()) == dataManager.getProperty(entity, desc.getVersionPropertyName())
505                    || (save.get(desc.getVersionPropertyName()) == null && dataManager.getProperty(entity, desc.getVersionPropertyName()) == null))) {
506                    
507                    if (!save.containsKey(propName))
508                        save.put(propName, oldValue);
509                    
510                    if (isSame(save.get(propName), newValue)) {
511                        save.remove(propName);
512                        int count = 0;
513                        for (String p : save.keySet()) {
514                            if (!p.equals(desc.getVersionPropertyName()))
515                                count++;
516                        }
517                        if (count == 0) {
518                            savedProperties.remove(object);
519                            dirtyCount--;
520                        }
521                    }
522                }
523                
524                notifyEntityDirtyChange(entity, oldDirtyEntity);
525            }
526            
527            notifyDirtyChange(oldDirty);
528        }
529    
530    
531        /**
532         *  @private 
533         *  Collection event handler to save changes on managed collections
534         *
535         *  @param owner owner entity of the collection
536         *  @param propName property name of the collection
537         *  @param event collection event
538         */ 
539        @SuppressWarnings("unchecked")
540            public void entityCollectionChangeHandler(Object owner, String propName, Collection<?> coll, ChangeKind kind, Integer location, Object[] items) {
541            boolean oldDirty = isDirty();
542            
543            EntityDescriptor desc = dataManager.getEntityDescriptor(owner);
544            boolean oldDirtyEntity = isEntityChanged(owner);
545            
546            Map<String, Object> esave = savedProperties.get(owner);
547            boolean unsaved = esave == null;
548            
549            if (unsaved || (desc.getVersionPropertyName() != null && 
550                (esave.get(desc.getVersionPropertyName()) != dataManager.getProperty(owner, desc.getVersionPropertyName()) 
551                    && !(esave.get(desc.getVersionPropertyName()) == null && dataManager.getProperty(owner, desc.getVersionPropertyName()) == null)))) {
552    
553                esave = new HashMap<String, Object>();
554                if (desc.getVersionPropertyName() != null)
555                    esave.put(desc.getVersionPropertyName(), dataManager.getProperty(owner, desc.getVersionPropertyName()));
556                savedProperties.put(owner, esave);
557                if (unsaved)
558                    dirtyCount++;
559            }
560        
561            List<Object> save = (List<Object>)esave.get(propName);
562            if (save == null) {
563                save = new ArrayList<Object>();
564                esave.put(propName, save);
565                                                    
566                            // Save collection snapshot
567                            for (Object e : coll)
568                                    save.add(e);
569                            
570                            // Adjust with last event
571                            if (kind == ChangeKind.ADD) {
572                                    if (location != null) {
573                                            for (int i = 0; i < items.length; i++)
574                                                    save.remove(location.intValue());
575                                    }
576                                    else {
577                                            for (Object item : items)
578                                                    save.remove(item);
579                                    }
580                            }
581                            else if (kind == ChangeKind.REMOVE) {
582                                    if (location != null) {
583                                            for (int i = 0; i < items.length; i++)
584                                                    save.add(location.intValue()+i, items[i]);
585                                    }
586                                    else {
587                                            for (Object item : items)
588                                                    save.add(item);
589                                    }
590                            }
591                            else if (kind == ChangeKind.REPLACE) {
592                                    if (location != null)
593                                            save.set(location.intValue(), ((Object[])items[0])[0]);
594                                    else {
595                                            save.remove(((Object[])items[0])[1]);
596                                            save.add(((Object[])items[0])[0]);
597                                    }
598                            }
599            }
600                    else {
601                            if (isSameList(save, coll)) {
602                    esave.remove(propName);
603                    int count = 0;
604                    for (Object p : esave.keySet()) {
605                        if (!p.equals(desc.getVersionPropertyName()))
606                            count++;
607                    }
608                    if (count == 0) {
609                        savedProperties.remove(owner);
610                        dirtyCount--;
611                    }
612                            }
613                    }
614            
615            notifyEntityDirtyChange(owner, oldDirtyEntity);
616            
617            notifyDirtyChange(oldDirty);
618        }
619    
620    
621            /**
622             *  @private 
623             *  Collection event handler to save changes on managed maps
624             *
625             *  @param owner owner entity of the collection
626             *  @param propName property name of the collection
627             *  @param event map event
628             */ 
629        @SuppressWarnings("unchecked")
630            public void entityMapChangeHandler(Object owner, String propName, Map<?, ?> map, ChangeKind kind, Object[] items) {
631            boolean oldDirty = isDirty();
632            
633            EntityDescriptor desc = dataManager.getEntityDescriptor(owner);
634            boolean oldDirtyEntity = isEntityChanged(owner);
635            
636            Map<String, Object> esave = savedProperties.get(owner);
637            boolean unsaved = esave == null;
638            
639            if (unsaved || (desc.getVersionPropertyName() != null && 
640                (esave.get(desc.getVersionPropertyName()) != dataManager.getProperty(owner, desc.getVersionPropertyName()) 
641                    && !(esave.get(desc.getVersionPropertyName()) == null && dataManager.getProperty(owner, desc.getVersionPropertyName()) == null)))) {
642    
643                esave = new HashMap<String, Object>();
644                if (desc.getVersionPropertyName() != null)
645                    esave.put(desc.getVersionPropertyName(), dataManager.getProperty(owner, desc.getVersionPropertyName()));
646                savedProperties.put(owner, esave);
647                if (unsaved)
648                    dirtyCount++;
649            }
650            
651            List<Object[]> save = (List<Object[]>)esave.get(propName);
652            if (save == null) {
653                save = new ArrayList<Object[]>();
654                esave.put(propName, save);
655    
656                            // Save map snapshot
657                            for (Entry<?, ?> entry : map.entrySet()) {
658                                    boolean found = false;
659                                    if (kind == ChangeKind.ADD) {
660                                            for (Object item : items) {
661                                                    if (isSame(entry.getKey(), ((Object[])item)[0])) {
662                                                            found = true;
663                                                            break;
664                                                    }
665                                            }
666                                    }
667                                    else if (kind == ChangeKind.REPLACE) {
668                                            for (Object item : items) {
669                                                    if (isSame(entry.getKey(), ((Object[])item)[0])) {
670                                                            save.add(new Object[] { entry.getKey(), ((Object[])item)[1] });
671                                                            found = true;
672                                                            break;
673                                                    }
674                                            }
675                                    }
676                                    if (!found)
677                                            save.add(new Object[] { entry.getKey(), entry.getValue() });
678                            }
679                            
680                            // Add removed element if needed
681                            if (kind == ChangeKind.REMOVE) {
682                                    for (Object item : items)
683                                            save.add(new Object[] { ((Object[])item)[0], ((Object[])item)[1] });
684                            }
685            }
686                    else {
687                            if (isSameMap(save, map)) {
688                    esave.remove(propName);
689                    int count = 0;
690                    for (Object p : esave.keySet()) {
691                        if (!p.equals(desc.getVersionPropertyName()))
692                            count++;
693                    }
694                    if (count == 0) {
695                        savedProperties.remove(owner);
696                        dirtyCount--;
697                    }
698                            }
699                    }
700            
701            notifyEntityDirtyChange(owner, oldDirtyEntity);
702            
703            notifyDirtyChange(oldDirty);
704        }
705    
706    
707        /**
708         *  @private 
709         *  Mark an object merged from the server as not dirty
710         *
711         *  @param object merged object
712         */ 
713        public void markNotDirty(Object object, Identifiable entity) {
714            if (entity != null)
715                    unsavedEntities.remove(entity);
716            
717            if (!savedProperties.containsKey(object))
718                return;
719            
720            boolean oldDirty = isDirty();
721            
722            boolean oldDirtyEntity = false;
723            if (entity == null && object instanceof Identifiable)
724                entity = (Identifiable)object;
725            if (entity != null)
726                oldDirtyEntity = isEntityChanged(entity);
727            
728            savedProperties.remove(object);
729            
730            if (entity != null)
731                notifyEntityDirtyChange(entity, oldDirtyEntity);
732            
733            dirtyCount--;
734    
735            notifyDirtyChange(oldDirty);
736        }
737        
738        
739        /**
740         *  @private 
741         *  Check if dirty properties of an object are the same than those of another entity
742         *  When they are the same, unmark the dirty flag
743         *
744         *  @param entity merged entity
745         *  @param source source entity
746         *  @param owner owner entity for embedded objects
747         *  @return true if the entity is still dirty after comparing with incoming object
748         */ 
749        public boolean checkAndMarkNotDirty(MergeContext mergeContext, Object entity, Object source, Object parent) {
750            if (entity != null)
751                    unsavedEntities.remove(entity);
752            
753            Map<String, Object> save = savedProperties.get(entity);
754            if (save == null)
755                return false;
756            
757                    Object owner = entity instanceof Identifiable ? (Identifiable)entity : parent;
758                    
759            boolean oldDirty = isDirty();
760            boolean oldDirtyEntity = isEntityChanged(owner);
761            
762                    List<String> merged = new ArrayList<String>();
763                    
764            EntityDescriptor desc = owner instanceof Identifiable ? dataManager.getEntityDescriptor(owner) : null;
765            String versionPropertyName = desc != null ? desc.getVersionPropertyName() : null;
766                    
767                    if (source instanceof Identifiable && versionPropertyName != null)
768                            save.put(versionPropertyName, dataManager.getProperty(source, versionPropertyName));
769                    
770                    Map<String, Object> pval = dataManager.getPropertyValues(entity, false, false);
771                    for (String propName : pval.keySet()) {
772                            if (propName.equals(versionPropertyName) || propName.equals("dirty"))
773                                    continue;
774                            
775    //                      if (!source.hasOwnProperty(propName))
776    //                              continue;
777                            
778                            Object localValue = pval.get(propName);
779                            if (localValue instanceof PropertyHolder)
780                                    localValue = ((PropertyHolder)localValue).getObject();
781                            
782                            Object sourceValue = dataManager.getProperty(source, propName);
783                            
784                            if (isSameExt(sourceValue, localValue)) {
785                                    merged.add(propName);
786                                    continue;
787                            }
788                            
789                            if (sourceValue == null || ObjectUtil.isSimple(sourceValue) || sourceValue instanceof Value || sourceValue instanceof Enum) {
790                                    save.put(propName, sourceValue);
791                            }
792                            else if (sourceValue instanceof Identifiable) {
793                                    save.put(propName, mergeContext.getFromCache(sourceValue));
794                            }
795                            else if (sourceValue instanceof Collection<?> && !(sourceValue instanceof LazyableCollection && !((LazyableCollection)sourceValue).isInitialized())) {
796                                    List<Object> snapshot = new ArrayList<Object>((Collection<?>)sourceValue);
797                                    save.put(propName, snapshot);
798                            }
799                            else if (sourceValue instanceof Map<?, ?> && !(sourceValue instanceof LazyableCollection && !((LazyableCollection)sourceValue).isInitialized())) {
800                                    Map<?, ?> map = (Map<?, ?>)sourceValue;
801                                    List<Object[]> snapshot = new ArrayList<Object[]>(map.size());
802                                    for (Entry<?, ?> entry : map.entrySet())
803                                            snapshot.add(new Object[] { entry.getKey(), entry.getValue() });
804                                    save.put(propName, snapshot);
805                            }
806                    }
807            
808            for (String propName : merged)
809                save.remove(propName);
810            
811            int count = 0;
812            for (String propName : save.keySet()) {
813                if (!propName.equals(desc.getVersionPropertyName()))
814                    count++;
815            }
816            if (count == 0) {
817                savedProperties.remove(entity);
818                dirtyCount--;
819            }
820            
821            boolean newDirtyEntity = notifyEntityDirtyChange(owner, oldDirtyEntity);
822            
823            notifyDirtyChange(oldDirty);
824            
825            return newDirtyEntity;
826        }
827            
828            
829            public void fixRemovalsAndPersists(MergeContext mergeContext, List<Object> removals, List<Object> persists) {
830                    boolean oldDirty = dirtyCount > 0;
831                    
832                    for (Object object : savedProperties.keySet()) {
833                            Identifiable owner = null;
834                            if (object instanceof Identifiable)
835                                    owner = (Identifiable)object;
836                            else {
837                                    Object[] ownerEntity = mergeContext.getOwnerEntity(object);
838                                    if (ownerEntity != null && ownerEntity[0] instanceof Identifiable)
839                                            owner = (Identifiable)ownerEntity[0];
840                            }
841                            
842                            EntityDescriptor desc = dataManager.getEntityDescriptor(owner);
843                            
844                            boolean oldDirtyEntity = isEntityChanged(object);
845                            
846                    Map<String, Object> save = savedProperties.get(object);
847                            
848                    Iterator<String> ip = save.keySet().iterator();
849                            while (ip.hasNext()) {
850                                    String p = ip.next();
851                                    Object sn = save.get(p);
852                                    if (!(sn instanceof List<?>))
853                                            continue;
854                                    
855                                    Object value = dataManager.getProperty(object, p);
856                                    if (value instanceof Collection<?>) {
857                                            @SuppressWarnings("unchecked")
858                                            List<Object> snapshot = (List<Object>)sn;
859                                            Collection<?> coll = (Collection<?>)value;
860                                            if (removals != null) {
861                                                    Iterator<Object> isne = snapshot.iterator();
862                                                    while (isne.hasNext()) {
863                                                            Object sne = isne.next();
864                                                            for (Object removal : removals) {
865                                                                    if (sne instanceof Identifiable && ObjectUtil.objectEquals(dataManager, sne, removal))
866                                                                            isne.remove();
867                                                            }
868                                                    }
869                                            }
870                                            if (persists != null) {
871                                                    for (Object persist : persists) {
872                                                            if (coll instanceof List<?>) {
873                                                                    List<?> list = (List<?>)coll;
874                                                                    List<Integer> found = new ArrayList<Integer>();
875                                                                    for (int j = 0; j < list.size(); j++) {
876                                                                            if (ObjectUtil.objectEquals(dataManager, list.get(j), persist))
877                                                                                    found.add(j);
878                                                                    }
879                                                                    for (int j = 0; j < snapshot.size(); j++) {
880                                                                            if (ObjectUtil.objectEquals(dataManager, persist, snapshot.get(j))) {
881                                                                                    snapshot.remove(j);
882                                                                                    j--;
883                                                                            }
884                                                                    }
885                                                                    for (int idx : found)
886                                                                            snapshot.add(idx, persist);
887                                                            }
888                                                            else {
889                                                                    if (coll.contains(persist) && !snapshot.contains(persist))
890                                                                            snapshot.add(persist);
891                                                            }
892                                                    }
893                                            }
894                                            
895                                            if (isSameList(snapshot, coll))
896                                                    ip.remove();
897                                    }
898                                    else if (value instanceof Map<?, ?>) {
899                                            @SuppressWarnings("unchecked")
900                                            List<Object[]> snapshot = (List<Object[]>)sn;
901                                            Map<?, ?> map = (Map<?, ?>)value;
902                                            if (removals != null) {
903                                                    Iterator<Object[]> isne = snapshot.iterator();
904                                                    while (isne.hasNext()) {
905                                                            Object[] sne = isne.next();
906                                                            for (Object removal : removals) {
907                                                                    if (sne[0] instanceof Identifiable && ObjectUtil.objectEquals(dataManager, sne[0], removal))
908                                                                            isne.remove();
909                                                                    else if (sne[1] instanceof Identifiable && ObjectUtil.objectEquals(dataManager, sne[1], removal))
910                                                                            isne.remove();
911                                                            }
912                                                    }
913                                            }
914                                            // TODO: persist ?                              
915    //                                      if (persists != null) {
916    //                                              for (Object persist : persists) {
917    //                                                      boolean foundKey = false;
918    //                                                      List<Object> foundValues = new ArrayList<Object>();
919    //                                                      Iterator<Object[]> isne = snapshot.iterator();
920    //                                                      while (isne.hasNext()) {
921    //                                                              Object[] sne = isne.next();
922    //                                                              if (ObjectUtil.objectEquals(dataManager, sne[0], persist))
923    //                                                                      foundKey = true;
924    //                                                              if (ObjectUtil.objectEquals(dataManager, sne[1], persist)) {
925    //                                                                      foundValues.add(sne[0]);
926    //                                                                      isne.remove();
927    //                                                              }
928    //                                                      }
929    //                                                      if (map.containsKey(persist) && !foundKey)
930    //                                                              snapshot.add(new Object[] { persist, map.get(persist) });
931    //                                                      if (map.containsValue(persist)) {
932    //                                                              for (Entry<?, ?> e : map.entrySet()) {
933    //                                                                      if (e.getValue().equals(persist) && !e.getKey().equals(persist))
934    //                                                                              snapshot.add(new Object[] { e.getKey(), e.getValue() });
935    //                                                              }
936    //                                                      }
937    //                                              }
938    //                                      }
939                                            
940                                            if (isSameMap(snapshot, map))
941                                                    ip.remove();
942                                    }
943                            }
944                            
945                int count = 0;
946                for (Object p : save.keySet()) {
947                    if (!p.equals(desc.getVersionPropertyName()))
948                        count++;
949                }
950                if (count == 0) {
951                    savedProperties.remove(object);
952                    dirtyCount--;
953                }
954                            
955                            notifyEntityDirtyChange(object, oldDirtyEntity);
956                    }
957                    
958                    notifyDirtyChange(oldDirty);
959            }
960        
961        
962        /**
963         *  @private
964         *  Internal implementation of entity reset
965         */ 
966        @SuppressWarnings("unchecked")
967            public void resetEntity(MergeContext mergeContext, Object entity, Identifiable parent, Set<Object> cache) {
968            // Should not try to reset uninitialized entities
969            if (entity instanceof Lazyable && !((Lazyable)entity).isInitialized())
970                return;
971            
972            if (cache.contains(entity))
973                return;
974            cache.add(entity);
975            
976            Map<String, Object> save = savedProperties.get(entity);
977            EntityDescriptor desc = dataManager.getEntityDescriptor(entity);
978            
979            Map<String, Object> pval = dataManager.getPropertyValues(entity, false, false);
980            
981            for (String p : pval.keySet()) {
982                if (p.equals(desc.getVersionPropertyName()))
983                    continue;
984                
985                Object val = dataManager.getProperty(entity, p);
986                if (val instanceof Collection<?> && !(val instanceof LazyableCollection && !((LazyableCollection)val).isInitialized())) {
987                    Collection<Object> coll = (Collection<Object>)val;
988                    List<Object> savedArray = save != null ? (List<Object>)save.get(p) : null;
989                    
990                    if (savedArray != null) {
991                            for (Object obj : coll) {
992                                    if (obj instanceof Identifiable)
993                                            resetEntity(mergeContext, obj, parent, cache);
994                            }
995                            coll.clear();
996                            for (Object e : savedArray)
997                                    coll.add(e);
998                        
999                        // Must be here because collection reset has triggered other useless CollectionEvents
1000                        markNotDirty(val, parent);
1001                    }
1002                    
1003                    for (Object o : coll) {
1004                        if (o instanceof Identifiable)
1005                            resetEntity(mergeContext, o, (Identifiable)o, cache);
1006                    }
1007                }
1008                else if (val instanceof Map<?, ?> && !(val instanceof LazyableCollection && !((LazyableCollection)val).isInitialized())) {
1009                    Map<Object, Object> map = (Map<Object, Object>)val;
1010                    List<Object[]> savedArray = save != null ? (List<Object[]>)save.get(p) : null;
1011                    
1012                    if (savedArray != null) {
1013                            for (Entry<Object, Object> entry : map.entrySet()) {
1014                                    if (entry.getKey() instanceof Identifiable)
1015                                            resetEntity(mergeContext, entry.getKey(), parent, cache);
1016                                    if (entry.getValue() instanceof Identifiable)
1017                                            resetEntity(mergeContext, entry.getValue(), parent, cache);
1018                                }
1019                            map.clear();
1020                            for (Object[] e : savedArray)
1021                                    map.put(e[0], e[1]);
1022                        
1023                        // Must be here because collection reset has triggered other useless CollectionEvents
1024                        markNotDirty(val, parent);
1025                    }
1026                    
1027                    for (Entry<Object, Object> me : map.entrySet()) {
1028                        if (me.getKey() instanceof Identifiable)
1029                            resetEntity(mergeContext, me.getKey(), (Identifiable)me.getKey(), cache);
1030                        if (me.getValue() instanceof Identifiable)
1031                            resetEntity(mergeContext, me.getValue(), (Identifiable)me.getValue(), cache);
1032                    }
1033                }
1034                else if (save != null && (ObjectUtil.isSimple(val) || ObjectUtil.isSimple(save.get(p)))) {
1035                    if (save.containsKey(p))
1036                        dataManager.setInternalProperty(entity, p, save.get(p));
1037                }
1038                else if (save != null && (val instanceof Enum || save.get(p) instanceof Enum || val instanceof Value || save.get(p) instanceof Value)) {
1039                    if (save.containsKey(p))
1040                        dataManager.setInternalProperty(entity, p, save.get(p));
1041                } 
1042                else if (save != null && save.containsKey(p)) {
1043                    if (!ObjectUtil.objectEquals(dataManager, val, save.get(p)))
1044                        dataManager.setInternalProperty(entity, p, save.get(p));
1045                }
1046                else if (val instanceof Identifiable)
1047                    resetEntity(mergeContext, val, (Identifiable)val, cache);
1048                else if (val != null && parent != null && !ObjectUtil.isSimple(val))
1049                    resetEntity(mergeContext, val, parent, cache);
1050            }
1051            
1052            // Must be here because entity reset may have triggered useless new saved properties
1053            markNotDirty(entity, null);
1054        }
1055        
1056        
1057        /**
1058         *  @private
1059         *  Internal implementation of entity reset all
1060         */ 
1061        public void resetAllEntities(MergeContext mergeContext, Set<Object> cache) {
1062            boolean found = false;
1063            do {
1064                found = false;
1065                for (Object entity : savedProperties.keySet()) {
1066                    if (entity instanceof Identifiable) {
1067                        found = true;
1068                        resetEntity(mergeContext, entity, (Identifiable)entity, cache);
1069                        break;
1070                    }
1071                }
1072            }
1073            while (found);
1074            
1075            if (dirtyCount > 0)
1076                log.error("Incomplete reset of context, could be a bug");
1077        }
1078        
1079        
1080        /**
1081         *  @private 
1082         *  Check if a value is empty
1083         *
1084         *  @return value is empty
1085         */ 
1086        public boolean isEmpty(Object val) {
1087            if (val == null)
1088                return true;
1089            else if (val instanceof String)
1090                return val.equals("");
1091            else if (val.getClass().isArray())
1092                return Array.getLength(val) == 0;
1093            else if (val instanceof Date)
1094                return ((Date)val).getTime() == 0L;
1095            else if (val instanceof List<?>)
1096                return ((List<?>)val).size() == 0;
1097            else if (val instanceof Map<?, ?>)
1098                return ((Map<?, ?>)val).size() == 0;
1099            return false; 
1100        }
1101        
1102    
1103        public static class Change {
1104            
1105            private ChangeKind kind;
1106            private int location;
1107            private Object[] items;
1108            
1109            public Change(ChangeKind kind, int location, Object[] items) {
1110                this.kind = kind;
1111                this.location = location;
1112                this.items = items;
1113            }
1114            
1115            public ChangeKind getKind() {
1116                return kind;
1117            }
1118            
1119            public int getLocation() {
1120                return location;
1121            }
1122            
1123            public Object[] getItems() {
1124                return items;
1125            }
1126            
1127            public void moveLocation(int offset) {
1128                location += offset;
1129            }
1130        }
1131    }