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 }