/** * GRANITE DATA SERVICES * Copyright (C) 2006-2015 GRANITE DATA SERVICES S.A.S. * * This file is part of the Granite Data Services Platform. * * *** * * Community License: GPL 3.0 * * This file is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * This file is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * *** * * Available Commercial License: GraniteDS SLA 1.0 * * This is the appropriate option if you are creating proprietary * applications and you are not prepared to distribute and share the * source code of your application under the GPL v3 license. * * Please visit http://www.granitedataservices.com/license for more * details. */ package org.granite.client.javafx.tide; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanPropertyBase; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.beans.value.WeakChangeListener; import javafx.beans.value.WritableValue; import javafx.collections.ListChangeListener; import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.collections.ObservableSet; import javafx.collections.SetChangeListener; import org.granite.client.javafx.persistence.collection.FXPersistentCollections; import org.granite.client.javafx.persistence.collection.ObservablePersistentBag; import org.granite.client.javafx.persistence.collection.ObservablePersistentList; import org.granite.client.javafx.persistence.collection.ObservablePersistentMap; import org.granite.client.javafx.persistence.collection.ObservablePersistentSet; import org.granite.client.javafx.persistence.collection.ObservablePersistentSortedMap; import org.granite.client.javafx.persistence.collection.ObservablePersistentSortedSet; import org.granite.client.tide.data.EntityManager; import org.granite.client.tide.data.PersistenceManager; import org.granite.client.tide.data.impl.AbstractDataManager; import org.granite.client.util.WeakIdentityHashMap; import org.granite.logging.Logger; import org.granite.util.TypeUtil; /** * @author William DRAI */ public class JavaFXDataManager extends AbstractDataManager { @SuppressWarnings("unused") private static final Logger log = Logger.getLogger(JavaFXDataManager.class); private TrackingHandler trackingHandler; public void setTrackingHandler(TrackingHandler trackingHandler) { this.trackingHandler = trackingHandler; } @SuppressWarnings("unchecked") public <T> T newInstance(Object source, Class<T> cast) throws IllegalAccessException, InstantiationException { if (ObservablePersistentList.class.isInstance(source)) return (T)FXPersistentCollections.observablePersistentList(); else if (ObservablePersistentBag.class.isInstance(source)) return (T)FXPersistentCollections.observablePersistentBag(); else if (ObservablePersistentSortedSet.class.isInstance(source)) return (T)FXPersistentCollections.observablePersistentSortedSet(); else if (ObservablePersistentSet.class.isInstance(source)) return (T)FXPersistentCollections.observablePersistentSet(); else if (ObservablePersistentSortedMap.class.isInstance(source)) return (T)FXPersistentCollections.observablePersistentSortedMap(); else if (ObservablePersistentMap.class.isInstance(source)) return (T)FXPersistentCollections.observablePersistentMap(); return TypeUtil.newInstance(source.getClass(), cast); } public BooleanProperty dirty = new ReadOnlyBooleanWrapper(this, "dirty", false); public ReadOnlyBooleanProperty dirtyProperty() { return dirty; } public boolean isDirty() { return dirty.get(); } private Map<Object, ObservableDirtyEntity> dirtyEntityCache = new WeakIdentityHashMap<Object, ObservableDirtyEntity>(); public ObservableDirtyEntity dirtyEntity(Object entity) { ObservableDirtyEntity dirtyEntity = dirtyEntityCache.get(entity); if (dirtyEntity == null) { dirtyEntity = new ObservableDirtyEntity(entity); dirtyEntityCache.put(entity, dirtyEntity); } return dirtyEntity; } private Map<Object, ObservableDeepDirtyEntity> deepDirtyEntityCache = new WeakIdentityHashMap<Object, ObservableDeepDirtyEntity>(); public ObservableDeepDirtyEntity deepDirtyEntity(Object entity) { ObservableDeepDirtyEntity deepDirtyEntity = deepDirtyEntityCache.get(entity); if (deepDirtyEntity == null) { deepDirtyEntity = new ObservableDeepDirtyEntity(entity); deepDirtyEntityCache.put(entity, deepDirtyEntity); } return deepDirtyEntity; } private class ObservableDirtyEntity extends ReadOnlyBooleanPropertyBase { private Object entity; private EntityManager entityManager; private ChangeListener<Object> changeListener = new WeakChangeListener<Object>(new ChangeListener<Object>() { @Override public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) { ObservableDirtyEntity.this.entity = newValue; ObservableDirtyEntity.this.entityManager = PersistenceManager.getEntityManager(newValue); fireValueChangedEvent(); } }); public ObservableDirtyEntity(Object entity) { if (entity instanceof ObservableValue) { ObservableValue<?> value = (ObservableValue<?>)entity; this.entity = value.getValue(); value.addListener(changeListener); } else this.entity = entity; this.entityManager = PersistenceManager.getEntityManager(this.entity); } @Override public Object getBean() { return JavaFXDataManager.this; } @Override public String getName() { return "deepDirtyEntity"; } @Override public boolean get() { if (entity == null) return false; if (entityManager == null) { // In case entity was not managed on initial binding entityManager = PersistenceManager.getEntityManager(entity); if (entityManager == null) return false; } return entityManager.isDirtyEntity(entity); } protected void fireValueChangedEvent() { super.fireValueChangedEvent(); } } private class ObservableDeepDirtyEntity extends ReadOnlyBooleanPropertyBase { private Object entity; private EntityManager entityManager; private ChangeListener<Object> changeListener = new WeakChangeListener<Object>(new ChangeListener<Object>() { @Override public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) { ObservableDeepDirtyEntity.this.entity = newValue; ObservableDeepDirtyEntity.this.entityManager = PersistenceManager.getEntityManager(newValue); fireValueChangedEvent(); } }); public ObservableDeepDirtyEntity(Object entity) { if (entity instanceof ObservableValue) { ObservableValue<?> value = (ObservableValue<?>)entity; this.entity = value.getValue(); value.addListener(changeListener); } else this.entity = entity; this.entityManager = PersistenceManager.getEntityManager(this.entity); } @Override public Object getBean() { return JavaFXDataManager.this; } @Override public String getName() { return "deepDirtyEntity"; } @Override public boolean get() { if (entity == null) return false; if (entityManager == null) { // In case entity was not managed on initial binding entityManager = PersistenceManager.getEntityManager(entity); if (entityManager == null) return false; } return entityManager.isDeepDirtyEntity(entity); } protected void fireValueChangedEvent() { super.fireValueChangedEvent(); } } public class EntityPropertyChangeListener<E> implements ChangeListener<E> { @Override public void changed(ObservableValue<? extends E> property, E oldValue, E newValue) { if (property instanceof ReadOnlyProperty<?>) { if (((ReadOnlyProperty<?>)property).getBean() == null) throw new IllegalStateException("Property bean must be defined"); if (((ReadOnlyProperty<?>)property).getName() == null || ((ReadOnlyProperty<?>)property).getName().trim().length() == 0) throw new IllegalStateException("Property name must be defined"); trackingHandler.entityPropertyChangeHandler(((ReadOnlyProperty<?>)property).getBean(), ((ReadOnlyProperty<?>)property).getName(), oldValue, newValue); } } } public class EntityListChangeListener<E> implements ListChangeListener<E> { @Override public void onChanged(Change<? extends E> change) { while (change.next()) { if (change.wasReplaced()) { Object[] replaced = new Object[change.getAddedSize()]; for (int i = 0; i < change.getAddedSize(); i++) replaced[i] = new Object[] { change.getRemoved().get(i), change.getAddedSubList().get(i) }; trackingHandler.entityCollectionChangeHandler(ChangeKind.REPLACE, change.getList(), change.getFrom(), replaced); return; } if (change.wasRemoved()) trackingHandler.entityCollectionChangeHandler(ChangeKind.REMOVE, change.getList(), change.getFrom(), change.getRemoved().toArray()); if (change.wasAdded()) trackingHandler.entityCollectionChangeHandler(ChangeKind.ADD, change.getList(), change.getFrom(), change.getAddedSubList().toArray()); // else if (change.wasReplaced()) // trackingHandler.entityCollectionChangeHandler("replace", change.getList(), change.getFrom(), change.) } } } public class EntitySetChangeListener<E> implements SetChangeListener<E> { @Override public void onChanged(Change<? extends E> change) { if (change.wasRemoved()) trackingHandler.entityCollectionChangeHandler(ChangeKind.REMOVE, change.getSet(), null, new Object[] { change.getElementRemoved() }); if (change.wasAdded()) trackingHandler.entityCollectionChangeHandler(ChangeKind.ADD, change.getSet(), null, new Object[] { change.getElementAdded() }); } } public class DefaultListChangeListener<E> implements ListChangeListener<E> { @Override public void onChanged(ListChangeListener.Change<? extends E> change) { while (change.next()) { if (change.wasReplaced()) { Object[] replaced = new Object[change.getAddedSize()]; for (int i = 0; i < change.getAddedSize(); i++) replaced[i] = new Object[] { change.getRemoved().get(i), change.getAddedSubList().get(i) }; trackingHandler.entityCollectionChangeHandler(ChangeKind.REPLACE, change.getList(), change.getFrom(), replaced); return; } if (change.wasRemoved()) trackingHandler.collectionChangeHandler(ChangeKind.REMOVE, change.getList(), change.getFrom(), change.getRemoved().toArray()); if (change.wasAdded()) trackingHandler.collectionChangeHandler(ChangeKind.ADD, change.getList(), change.getFrom(), change.getAddedSubList().toArray()); } } } public class DefaultSetChangeListener<E> implements SetChangeListener<E> { @Override public void onChanged(SetChangeListener.Change<? extends E> change) { if (change.wasRemoved()) trackingHandler.collectionChangeHandler(ChangeKind.REMOVE, change.getSet(), null, new Object[] { change.getElementRemoved() }); if (change.wasAdded()) trackingHandler.collectionChangeHandler(ChangeKind.ADD, change.getSet(), null, new Object[] { change.getElementAdded() }); } } public class EntityMapChangeListener<K, V> implements MapChangeListener<K, V> { @Override public void onChanged(MapChangeListener.Change<? extends K, ? extends V> change) { if (change.wasAdded() && change.wasRemoved()) trackingHandler.entityMapChangeHandler(ChangeKind.REPLACE, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueRemoved(), change.getValueAdded() }}); else if (change.wasRemoved()) trackingHandler.entityMapChangeHandler(ChangeKind.REMOVE, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueRemoved() }}); else if (change.wasAdded()) trackingHandler.entityMapChangeHandler(ChangeKind.ADD, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueAdded() }}); } } public class DefaultMapChangeListener<K, V> implements MapChangeListener<K, V> { @Override public void onChanged(MapChangeListener.Change<? extends K, ? extends V> change) { if (change.wasAdded() && change.wasRemoved()) trackingHandler.mapChangeHandler(ChangeKind.REPLACE, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueRemoved(), change.getValueAdded() }}); else if (change.wasRemoved()) trackingHandler.mapChangeHandler(ChangeKind.REMOVE, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueRemoved() }}); else if (change.wasAdded()) trackingHandler.mapChangeHandler(ChangeKind.ADD, change.getMap(), 0, new Object[] { new Object[] { change.getKey(), change.getValueAdded() }}); } } private ListChangeListener<Object> listChangeListener = new DefaultListChangeListener<Object>(); private SetChangeListener<Object> setChangeListener = new DefaultSetChangeListener<Object>(); private MapChangeListener<Object, Object> mapChangeListener = new DefaultMapChangeListener<Object, Object>(); private ChangeListener<Object> entityPropertyChangeListener = new EntityPropertyChangeListener<Object>(); private ListChangeListener<Object> entityListChangeListener = new EntityListChangeListener<Object>(); private SetChangeListener<Object> entitySetChangeListener = new EntitySetChangeListener<Object>(); private MapChangeListener<Object, Object> entityMapChangeListener = new EntityMapChangeListener<Object, Object>(); private WeakIdentityHashMap<Object, TrackingType> trackingListeners = new WeakIdentityHashMap<Object, TrackingType>(); @Override public void startTracking(Object previous, Object parent) { if (previous == null || trackingListeners.containsKey(previous)) return; if (previous instanceof ObservableList<?>) { if (parent != null) { ((ObservableList<?>)previous).addListener(entityListChangeListener); trackingListeners.put(previous, TrackingType.ENTITY_LIST); } else { ((ObservableList<?>)previous).addListener(listChangeListener); trackingListeners.put(previous, TrackingType.LIST); } } else if (previous instanceof ObservableSet<?>) { if (parent != null) { ((ObservableSet<?>)previous).addListener(entitySetChangeListener); trackingListeners.put(previous, TrackingType.ENTITY_SET); } else { ((ObservableSet<?>)previous).addListener(setChangeListener); trackingListeners.put(previous, TrackingType.SET); } } else if (previous instanceof ObservableMap<?, ?>) { if (parent != null) { ((ObservableMap<?, ?>)previous).addListener(entityMapChangeListener); trackingListeners.put(previous, TrackingType.ENTITY_MAP); } else { ((ObservableMap<?, ?>)previous).addListener(mapChangeListener); trackingListeners.put(previous, TrackingType.MAP); } } else if (parent != null || isEntity(previous)) { List<ObservableValue<?>> properties = instrospectProperties(previous); for (ObservableValue<?> property : properties) { if (property instanceof WritableValue<?>) property.addListener(entityPropertyChangeListener); } trackingListeners.put(previous, TrackingType.ENTITY_PROPERTY); } } @Override public void stopTracking(Object previous, Object parent) { if (previous == null || !trackingListeners.containsKey(previous)) return; if (previous instanceof ObservableList<?>) { if (parent != null) ((ObservableList<?>)previous).removeListener(entityListChangeListener); else ((ObservableList<?>)previous).removeListener(listChangeListener); } else if (previous instanceof ObservableSet<?>) { if (parent != null) ((ObservableSet<?>)previous).removeListener(entitySetChangeListener); else ((ObservableSet<?>)previous).removeListener(setChangeListener); } else if (previous instanceof ObservableMap<?, ?>) { if (parent != null) ((ObservableMap<?, ?>)previous).removeListener(entityMapChangeListener); else ((ObservableMap<?, ?>)previous).removeListener(mapChangeListener); } else if (parent != null || isEntity(previous)) { List<ObservableValue<?>> properties = instrospectProperties(previous); for (ObservableValue<?> property : properties) { if (property instanceof WritableValue<?>) property.removeListener(entityPropertyChangeListener); } } trackingListeners.remove(previous); } @Override public void clear() { dirty.set(false); deepDirtyEntityCache.clear(); Iterator<Object> ikey = trackingListeners.keySet().iterator(); while (ikey.hasNext()) { Object obj = ikey.next(); TrackingType type = trackingListeners.get(obj); if (type == null) // gc'ed during iteration ?? continue; switch (type) { case LIST: ((ObservableList<?>)obj).removeListener(listChangeListener); break; case SET: ((ObservableSet<?>)obj).removeListener(setChangeListener); break; case MAP: ((ObservableMap<?, ?>)obj).removeListener(mapChangeListener); break; case ENTITY_PROPERTY: for (ObservableValue<?> property : instrospectProperties(obj)) { if (property instanceof WritableValue<?>) property.removeListener(entityPropertyChangeListener); } break; case ENTITY_LIST: ((ObservableList<?>)obj).removeListener(entityListChangeListener); break; case ENTITY_SET: ((ObservableSet<?>)obj).removeListener(entitySetChangeListener); break; case ENTITY_MAP: ((ObservableMap<?, ?>)obj).removeListener(entityMapChangeListener); break; } } } private List<ObservableValue<?>> instrospectProperties(Object obj) { List<ObservableValue<?>> properties = new ArrayList<ObservableValue<?>>(); for (Method m : obj.getClass().getMethods()) { if (m.getParameterTypes().length != 0 || !m.getName().endsWith("Property") || !ObservableValue.class.isAssignableFrom(m.getReturnType())) continue; try { ObservableValue<?> property = (ObservableValue<?>)m.invoke(obj); properties.add(property); } catch (Exception e) { throw new RuntimeException("Could not get property " + m.getName() + " on " + obj, e); } } return properties; } @Override public void notifyDirtyChange(boolean oldDirty, boolean dirty) { this.dirty.set(dirty); } @Override public void notifyEntityDirtyChange(Object entity, boolean oldDirtyEntity, boolean newDirtyEntity) { ObservableDirtyEntity dirtyEntity = dirtyEntityCache.get(entity); if (dirtyEntity != null) dirtyEntity.fireValueChangedEvent(); for (ObservableDeepDirtyEntity deepDirtyEntity : deepDirtyEntityCache.values()) deepDirtyEntity.fireValueChangedEvent(); } }