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.javafx;
022    
023    import java.beans.Introspector;
024    import java.lang.annotation.ElementType;
025    import java.lang.reflect.Method;
026    import java.util.ArrayList;
027    import java.util.Collection;
028    import java.util.Collections;
029    import java.util.Iterator;
030    import java.util.LinkedHashMap;
031    import java.util.List;
032    import java.util.Map;
033    import java.util.Set;
034    
035    import javafx.beans.property.BooleanProperty;
036    import javafx.beans.property.Property;
037    import javafx.beans.property.ReadOnlyBooleanProperty;
038    import javafx.beans.property.ReadOnlyBooleanPropertyBase;
039    import javafx.beans.property.ReadOnlyBooleanWrapper;
040    import javafx.beans.property.ReadOnlyProperty;
041    import javafx.beans.value.ChangeListener;
042    import javafx.beans.value.ObservableValue;
043    import javafx.beans.value.WeakChangeListener;
044    import javafx.beans.value.WritableValue;
045    import javafx.collections.ListChangeListener;
046    import javafx.collections.MapChangeListener;
047    import javafx.collections.ObservableList;
048    import javafx.collections.ObservableMap;
049    import javafx.event.Event;
050    
051    import javax.validation.ConstraintViolation;
052    import javax.validation.Path;
053    import javax.validation.Path.Node;
054    import javax.validation.TraversableResolver;
055    
056    import org.granite.client.persistence.LazyableCollection;
057    import org.granite.client.tide.collections.ManagedPersistentCollection;
058    import org.granite.client.tide.collections.ManagedPersistentMap;
059    import org.granite.client.tide.collections.javafx.JavaFXManagedPersistentCollection;
060    import org.granite.client.tide.collections.javafx.JavaFXManagedPersistentMap;
061    import org.granite.client.tide.data.EntityManager;
062    import org.granite.client.tide.data.Identifiable;
063    import org.granite.client.tide.data.Lazyable;
064    import org.granite.client.tide.data.PersistenceManager;
065    import org.granite.client.tide.data.Transient;
066    import org.granite.client.tide.data.impl.AbstractDataManager;
067    import org.granite.client.tide.data.spi.EntityDescriptor;
068    import org.granite.client.util.WeakIdentityHashMap;
069    import org.granite.client.util.javafx.DataNotifier;
070    import org.granite.client.validation.javafx.ConstraintViolationEvent;
071    import org.granite.logging.Logger;
072    
073    /**
074     * @author William DRAI
075     */
076    public class JavaFXDataManager extends AbstractDataManager {
077            
078            private static final Logger log = Logger.getLogger(JavaFXDataManager.class);
079        
080        private TrackingHandler trackingHandler;
081        
082        public void setTrackingHandler(TrackingHandler trackingHandler) {
083            this.trackingHandler = trackingHandler;
084        }
085        
086        public BooleanProperty dirty = new ReadOnlyBooleanWrapper(this, "dirty", false);
087        
088        public ReadOnlyBooleanProperty dirtyProperty() {
089            return dirty;
090        }
091        
092        public boolean isDirty() {
093            return dirty.get();
094        }
095        
096        private Map<Object, ObservableDeepDirtyEntity> deepDirtyEntityCache = new WeakIdentityHashMap<Object, ObservableDeepDirtyEntity>();
097        
098        public ObservableDeepDirtyEntity deepDirtyEntity(Object entity) {
099            ObservableDeepDirtyEntity deepDirtyEntity = deepDirtyEntityCache.get(entity);
100            if (deepDirtyEntity == null) {
101                    deepDirtyEntity = new ObservableDeepDirtyEntity(entity);
102                    deepDirtyEntityCache.put(entity, deepDirtyEntity);
103            }
104            return deepDirtyEntity;
105        }
106        
107        private class ObservableDeepDirtyEntity extends ReadOnlyBooleanPropertyBase {
108            
109            private Object entity;
110            private EntityManager entityManager;
111            private ChangeListener<Object> changeListener = new WeakChangeListener<Object>(new ChangeListener<Object>() {
112                            @Override
113                            public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
114                                    ObservableDeepDirtyEntity.this.entity = newValue;
115                                    ObservableDeepDirtyEntity.this.entityManager = PersistenceManager.getEntityManager(newValue);
116                                    fireValueChangedEvent();
117                            }               
118            });
119            
120            public ObservableDeepDirtyEntity(Object entity) {
121                    if (entity instanceof ObservableValue) {
122                            ObservableValue<?> value = (ObservableValue<?>)entity;
123                            this.entity = value.getValue();
124                            value.addListener(changeListener);
125                    }
126                    else
127                            this.entity = entity;
128                    this.entityManager = PersistenceManager.getEntityManager(this.entity);
129            }
130            
131                    @Override
132                    public Object getBean() {
133                            return JavaFXDataManager.this;
134                    }
135                    
136                    @Override
137                    public String getName() {
138                            return "deepDirtyEntity";
139                    }
140                    
141                    @Override
142                    public boolean get() {
143                            if (entityManager == null || entity == null)
144                                    return false;
145                            return entityManager.isDeepDirtyEntity(entity);
146                    }
147    
148                    protected void fireValueChangedEvent() {
149                            super.fireValueChangedEvent();
150                    }
151        }
152        
153        
154        @Override
155        public Object getProperty(Object object, String propertyName) {
156            try {
157                Method m = object.getClass().getMethod("get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1));
158                return m.invoke(object);
159            }
160            catch (NoSuchMethodException e) {
161                try {
162                    Method m = object.getClass().getMethod("is" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1));
163                    return m.invoke(object);
164                }
165                catch (Exception f) {
166                    throw new RuntimeException("Could not get property " + propertyName + " on object " + object, f);
167                }
168            }
169            catch (Exception e) {
170                throw new RuntimeException("Could not get property " + propertyName + " on object " + object, e);
171            }
172        }
173        
174        @Override
175        public void setProperty(Object object, String propertyName, Object oldValue, Object newValue) {
176            try {
177                Method[] methods = object.getClass().getMethods();
178                String setter = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
179                for (Method m : methods) {
180                    if (m.getName().equals(setter) && m.getParameterTypes().length == 1 && m.getParameterTypes()[0].isInstance(newValue)) {
181                        m.invoke(object, newValue);
182                        break;
183                    }
184                }
185            }
186            catch (Exception e) {
187                throw new RuntimeException("Could not get property " + propertyName + " on object " + object, e);
188            }
189        }
190    
191        @Override
192        public void setInternalProperty(Object object, String propertyName, Object value) {
193            Method[] methods = object.getClass().getMethods();
194            String property = propertyName + "Property";
195            String setter = "set" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
196            boolean found = false;
197            for (Method m : methods) {
198                    if (m.getName().equals(property) && m.getParameterTypes().length == 0 && ObservableValue.class.isAssignableFrom(m.getReturnType())) {
199                    try {
200                            @SuppressWarnings("unchecked")
201                            Property<Object> p = (Property<Object>)m.invoke(object);
202                            p.setValue(value);
203                                    found = true;
204                    }
205                    catch (Exception e) {
206                        throw new RuntimeException("Could not setValue on property " + propertyName + " on object " + object, e);
207                    }
208                    }
209            }
210            if (!found) {
211                for (Method m : methods) {
212                    if (m.getName().equals(setter) && m.getParameterTypes().length == 1 && m.getParameterTypes()[0].isInstance(value)) {
213                            try {
214                                    m.invoke(object, value);
215                                    found = true;
216                                    break;
217                            }
218                            catch (Exception e) {
219                            throw new RuntimeException("Could not call setter on property " + propertyName + " on object " + object, e);
220                            }
221                    }
222                }
223            }
224            if (!found)
225                    log.warn("No property found for object " + object + " name " + propertyName);
226        }
227    
228        @SuppressWarnings("unchecked")
229        @Override
230        public Map<String, Object> getPropertyValues(Object object, boolean includeReadOnly, boolean includeTransient) {
231            return getPropertyValues(object, Collections.EMPTY_LIST, includeReadOnly, includeTransient);
232        }
233    
234        @Override
235        public Map<String, Object> getPropertyValues(Object object, List<String> excludedProperties, boolean includeReadOnly, boolean includeTransient) {
236            EntityDescriptor desc = getEntityDescriptor(object);
237            
238            Map<String, Object> values = new LinkedHashMap<String, Object>();
239            for (Method m : object.getClass().getMethods()) {
240                if (!((m.getName().endsWith("Property") && ObservableValue.class.isAssignableFrom(m.getReturnType()) 
241                        || (m.getName().startsWith("get") && (Collection.class.isAssignableFrom(m.getReturnType()) || Map.class.isAssignableFrom(m.getReturnType()))))))
242                    continue;
243                
244                if (!includeTransient && m.isAnnotationPresent(Transient.class))
245                    continue;
246                
247                String pname = m.getName().startsWith("get") 
248                    ? Introspector.decapitalize(m.getName().substring(3))
249                    : m.getName().substring(0, m.getName().length()-8);
250                    
251                if (desc.getDirtyPropertyName() != null && desc.getDirtyPropertyName().equals(pname))
252                    continue;
253                
254                try {
255                    if (excludedProperties.contains(pname))
256                        continue;
257                    
258                    if (m.getName().endsWith("Property")) {
259                        @SuppressWarnings("unchecked")
260                        ReadOnlyProperty<Object> p = (ReadOnlyProperty<Object>)m.invoke(object);
261                        
262                        if (!includeReadOnly && !(p instanceof WritableValue))
263                            continue;
264                        
265                        values.put(p.getName(), p.getValue());
266                    }
267                    else {
268                        Object value = m.invoke(object);
269                        
270                        values.put(pname, value);
271                    }
272                }
273                catch (Exception e) {
274                    throw new RuntimeException("Could not get property " + m + " on object " + object, e);
275                }
276            }
277            return values;
278        }
279    
280        @Override
281        public ManagedPersistentCollection<Object> newPersistentCollection(Identifiable parent, String propertyName, LazyableCollection nextList) {
282            return new JavaFXManagedPersistentCollection<Object>(parent, propertyName, nextList);
283        }
284    
285        @Override
286        public ManagedPersistentMap<Object, Object> newPersistentMap(Identifiable parent, String propertyName, LazyableCollection nextMap) {
287            return new JavaFXManagedPersistentMap<Object, Object>(parent, propertyName, nextMap);
288        }
289    
290        
291        public class EntityPropertyChangeListener<E> implements ChangeListener<E> {
292            @Override
293            public void changed(ObservableValue<? extends E> property, E oldValue, E newValue) {
294                if (property instanceof Property<?>) {
295                    if (((Property<?>)property).getBean() == null)
296                        throw new IllegalStateException("Property bean must be defined");
297                    if (((Property<?>)property).getName() == null || ((Property<?>)property).getName().trim().length() == 0)
298                        throw new IllegalStateException("Property name must be defined");
299                    trackingHandler.entityPropertyChangeHandler(((Property<?>)property).getBean(), ((Property<?>)property).getName(), oldValue, newValue);
300                }
301            }
302        }
303        
304        public class EntityListChangeListener<E> implements ListChangeListener<E> {
305            @Override
306            public void onChanged(Change<? extends E> change) {
307                while (change.next()) {
308                    if (change.wasReplaced()) {
309                            Object[] replaced = new Object[change.getAddedSize()];
310                            for (int i = 0; i < change.getAddedSize(); i++)
311                                    replaced[i] = new Object[] { change.getRemoved().get(i), change.getAddedSubList().get(i) };
312                        trackingHandler.entityCollectionChangeHandler(ChangeKind.REPLACE, change.getList(), change.getFrom(), replaced);
313                            return;
314                    }
315                    if (change.wasRemoved())
316                        trackingHandler.entityCollectionChangeHandler(ChangeKind.REMOVE, change.getList(), change.getFrom(), change.getRemoved().toArray());
317                    if (change.wasAdded())
318                        trackingHandler.entityCollectionChangeHandler(ChangeKind.ADD, change.getList(), change.getFrom(), change.getAddedSubList().toArray());
319    //                else if (change.wasReplaced())
320    //                    trackingHandler.entityCollectionChangeHandler("replace", change.getList(), change.getFrom(), change.)
321                }
322            }
323        }
324        
325        public class DefaultListChangeListener<E> implements ListChangeListener<E> {
326            @Override
327            public void onChanged(ListChangeListener.Change<? extends E> change) {
328                while (change.next()) {
329                    if (change.wasReplaced()) {
330                            Object[] replaced = new Object[change.getAddedSize()];
331                            for (int i = 0; i < change.getAddedSize(); i++)
332                                    replaced[i] = new Object[] { change.getRemoved().get(i), change.getAddedSubList().get(i) };
333                        trackingHandler.entityCollectionChangeHandler(ChangeKind.REPLACE, change.getList(), change.getFrom(), replaced);
334                            return;
335                    }
336                    if (change.wasRemoved())
337                        trackingHandler.collectionChangeHandler(ChangeKind.REMOVE, change.getList(), change.getFrom(), change.getRemoved().toArray());
338                    if (change.wasAdded())
339                        trackingHandler.collectionChangeHandler(ChangeKind.ADD, change.getList(), change.getFrom(), change.getAddedSubList().toArray());
340                }
341            }
342        }
343        
344        public class EntityMapChangeListener<K, V> implements MapChangeListener<K, V> {
345            @Override
346            public void onChanged(MapChangeListener.Change<? extends K, ? extends V> change) {
347                if (change.wasAdded() && change.wasRemoved())
348                    trackingHandler.entityMapChangeHandler(ChangeKind.REPLACE, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueRemoved(), change.getValueAdded() }});
349                else if (change.wasRemoved())
350                    trackingHandler.entityMapChangeHandler(ChangeKind.REMOVE, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueRemoved() }});
351                else if (change.wasAdded())
352                    trackingHandler.entityMapChangeHandler(ChangeKind.ADD, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueAdded() }});
353            }
354        }
355    
356        public class DefaultMapChangeListener<K, V> implements MapChangeListener<K, V> {
357            @Override
358            public void onChanged(MapChangeListener.Change<? extends K, ? extends V> change) {
359                if (change.wasAdded() && change.wasRemoved())
360                    trackingHandler.mapChangeHandler(ChangeKind.REPLACE, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueRemoved(), change.getValueAdded() }});
361                else if (change.wasRemoved())
362                    trackingHandler.mapChangeHandler(ChangeKind.REMOVE, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueRemoved() }});
363                else if (change.wasAdded())
364                    trackingHandler.mapChangeHandler(ChangeKind.ADD, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueAdded() }});
365            }
366        }
367     
368        private ListChangeListener<Object> listChangeListener = new DefaultListChangeListener<Object>();
369        private MapChangeListener<Object, Object> mapChangeListener = new DefaultMapChangeListener<Object, Object>();
370        private ChangeListener<Object> entityPropertyChangeListener = new EntityPropertyChangeListener<Object>();
371        private ListChangeListener<Object> entityListChangeListener = new EntityListChangeListener<Object>();
372        private MapChangeListener<Object, Object> entityMapChangeListener = new EntityMapChangeListener<Object, Object>();
373        
374        private WeakIdentityHashMap<Object, TrackingType> trackingListeners = new WeakIdentityHashMap<Object, TrackingType>();
375        
376        
377        @Override
378        public void startTracking(Object previous, Object parent) {
379            if (previous == null || trackingListeners.containsKey(previous))
380                return;
381            
382            if (previous instanceof ObservableList<?>) {
383                if (parent != null) {
384                    ((ObservableList<?>)previous).addListener(entityListChangeListener);
385                    trackingListeners.put(previous, TrackingType.ENTITY_COLLECTION);
386                }
387                else {
388                    ((ObservableList<?>)previous).addListener(listChangeListener);
389                    trackingListeners.put(previous, TrackingType.COLLECTION);
390                }
391            }
392            else if (previous instanceof ObservableMap<?, ?>) {
393                if (parent != null) {
394                    ((ObservableMap<?, ?>)previous).addListener(entityMapChangeListener);
395                    trackingListeners.put(previous, TrackingType.ENTITY_MAP);
396                }
397                else {
398                    ((ObservableMap<?, ?>)previous).addListener(mapChangeListener);
399                    trackingListeners.put(previous, TrackingType.MAP);
400                }
401            }
402            else if (parent != null || previous instanceof Identifiable) {
403                for (ObservableValue<?> property : instrospectProperties(previous))
404                    property.addListener(entityPropertyChangeListener);
405                trackingListeners.put(previous, TrackingType.ENTITY_PROPERTY);
406            }
407        }
408    
409        @Override
410        public void stopTracking(Object previous, Object parent) {
411            if (previous == null || !trackingListeners.containsKey(previous))
412                return;
413            
414            if (previous instanceof ObservableList<?>) {
415                if (parent != null)
416                    ((ObservableList<?>)previous).removeListener(entityListChangeListener);
417                else
418                    ((ObservableList<?>)previous).removeListener(listChangeListener);
419            }
420            else if (previous instanceof ObservableMap<?, ?>) {
421                if (parent != null)
422                    ((ObservableMap<?, ?>)previous).removeListener(entityMapChangeListener);
423                else
424                    ((ObservableMap<?, ?>)previous).removeListener(mapChangeListener);
425            }
426            else if (parent != null || previous instanceof Identifiable) {
427                for (ObservableValue<?> property : instrospectProperties(previous))
428                    property.removeListener(entityPropertyChangeListener);
429            }
430            
431            trackingListeners.remove(previous);
432        }
433    
434        @Override
435        public void clear() {
436            dirty.set(false);
437            
438            deepDirtyEntityCache.clear();
439            
440            Iterator<Object> ikey = trackingListeners.keySet().iterator();
441            while (ikey.hasNext()) {
442                Object obj = ikey.next();
443                TrackingType type = trackingListeners.get(obj);
444                switch (type) {
445                case COLLECTION:
446                    ((ObservableList<?>)obj).removeListener(listChangeListener);
447                    break;
448                case MAP:
449                    ((ObservableMap<?, ?>)obj).removeListener(mapChangeListener);
450                    break;
451                case ENTITY_PROPERTY:
452                    for (ObservableValue<?> property : instrospectProperties(obj))
453                        property.removeListener(entityPropertyChangeListener);
454                    break;
455                case ENTITY_COLLECTION:
456                    ((ObservableList<?>)obj).removeListener(listChangeListener);
457                    break;
458                case ENTITY_MAP:
459                    ((ObservableMap<?, ?>)obj).removeListener(mapChangeListener);
460                    break;
461                }
462            }
463        }
464        
465        private List<ObservableValue<?>> instrospectProperties(Object obj) {
466            List<ObservableValue<?>> properties = new ArrayList<ObservableValue<?>>();
467            EntityDescriptor desc = getEntityDescriptor(obj);
468            for (Method m : obj.getClass().getMethods()) {
469                if (m.getParameterTypes().length != 0 || !m.getName().endsWith("Property") || !ObservableValue.class.isAssignableFrom(m.getReturnType()))
470                    continue;
471                if (desc != null && m.getName().substring(0, m.getName().length()-8).equals(desc.getDirtyPropertyName()))
472                    continue;
473                
474                try {
475                    ObservableValue<?> property = (ObservableValue<?>)m.invoke(obj);
476                    properties.add(property);
477                }
478                catch (Exception e) {
479                    throw new RuntimeException("Could not get property " + m.getName() + " on " + obj, e);
480                }                
481            }
482            return properties;
483        }
484    
485        @Override
486        public void notifyDirtyChange(boolean oldDirty, boolean dirty) {
487            this.dirty.set(dirty);
488        }
489    
490        @Override
491        public void notifyEntityDirtyChange(Object entity, boolean oldDirtyEntity, boolean newDirtyEntity) {
492            EntityDescriptor desc = getEntityDescriptor(entity);
493            if (desc.getDirtyPropertyName() != null) {
494                Method m;
495                try {
496                    m = entity.getClass().getMethod(desc.getDirtyPropertyName() + "Property");
497                    BooleanProperty dirty = (BooleanProperty)m.invoke(entity);
498                    dirty.set(newDirtyEntity);
499                }
500                catch (Exception e) {
501                    throw new RuntimeException("Could not get dirty property on entity " + entity, e);
502                }
503            }
504            
505            for (ObservableDeepDirtyEntity deepDirtyEntity : deepDirtyEntityCache.values())
506                    deepDirtyEntity.fireValueChangedEvent();
507        }
508        
509        
510        public void notifyConstraintViolations(Object entity, Set<ConstraintViolation<?>> violations) {
511                    if (!(entity instanceof DataNotifier))
512                            return;
513                    ConstraintViolationEvent event = new ConstraintViolationEvent(ConstraintViolationEvent.CONSTRAINT_VIOLATION, violations);
514                    Event.fireEvent((DataNotifier)entity, event);
515        }
516        
517        
518        private TraversableResolver traversableResolver = new TraversableResolverImpl();
519        
520        @Override
521        public TraversableResolver getTraversableResolver() {
522            return traversableResolver;
523        }
524    
525        public class TraversableResolverImpl implements TraversableResolver {
526            
527            public boolean isReachable(Object bean, Node propertyPath, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
528                    if (bean == null || propertyPath.getName() == null || ElementType.TYPE.equals(elementType))
529                            return true;
530                    Object value = getProperty(bean, propertyPath.getName());
531                    if (value instanceof LazyableCollection)
532                            return ((LazyableCollection)value).isInitialized();
533                    if (value instanceof Lazyable)
534                            return ((Lazyable)value).isInitialized();
535                    return true;
536            }
537            
538            public boolean isCascadable(Object bean, Node propertyPath, Class<?> rootBeanType, Path pathToTraversableObject, ElementType elementType) {
539                    return true;
540            }
541    
542        }
543    
544    }