001/**
002 *   GRANITE DATA SERVICES
003 *   Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004 *
005 *   This file is part of the Granite Data Services Platform.
006 *
007 *   Granite Data Services is free software; you can redistribute it and/or
008 *   modify it under the terms of the GNU Lesser General Public
009 *   License as published by the Free Software Foundation; either
010 *   version 2.1 of the License, or (at your option) any later version.
011 *
012 *   Granite Data Services is distributed in the hope that it will be useful,
013 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
014 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
015 *   General Public License for more details.
016 *
017 *   You should have received a copy of the GNU Lesser General Public
018 *   License along with this library; if not, write to the Free Software
019 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
020 *   USA, or see <http://www.gnu.org/licenses/>.
021 */
022package org.granite.tide.data;
023
024import java.io.Serializable;
025import java.lang.reflect.Array;
026import java.lang.reflect.Field;
027import java.lang.reflect.InvocationTargetException;
028import java.lang.reflect.ParameterizedType;
029import java.lang.reflect.Type;
030import java.util.Collection;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Iterator;
034import java.util.List;
035import java.util.Map;
036import java.util.Map.Entry;
037import java.util.Set;
038
039import org.granite.context.GraniteContext;
040import org.granite.logging.Logger;
041import org.granite.messaging.amf.io.convert.Converters;
042import org.granite.messaging.amf.io.util.ClassGetter;
043import org.granite.messaging.service.ServiceException;
044import org.granite.util.Introspector;
045import org.granite.util.PropertyDescriptor;
046import org.granite.util.TypeUtil;
047import org.granite.util.Entity;
048
049
050/**
051 *      Utility class that applies a ChangeSet on a persistence context
052 *      @author William DRAI
053 *
054 */
055public class ChangeSetApplier {
056        
057        private static final Logger log = Logger.getLogger(ChangeSetApplier.class);
058
059    private TidePersistenceAdapter persistenceAdapter;
060
061    public ChangeSetApplier(TidePersistenceAdapter persistenceAdapter) {
062        this.persistenceAdapter = persistenceAdapter;
063    }
064
065    protected long getVersion(Entity e) {
066        if (!e.isVersioned())
067            throw new IllegalStateException("Cannot apply change set on non versioned entity " + e.getName());
068        
069        Number version = (Number)e.getVersion();
070        if (version == null)
071                throw new IllegalStateException("Cannot apply changes on non persistent entity " + e.getName() + ":" + e.getIdentifier());
072        
073        return version.longValue();
074    }
075
076
077    protected Object mergeObject(Object entity, Set<Object> cache) {
078        ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter();
079        Converters converters = GraniteContext.getCurrentInstance().getGraniteConfig().getConverters();
080        
081        if (entity != null && !classGetter.isInitialized(null, null, entity)) {
082            // cache.contains() cannot be called on un unintialized proxy because hashCode will fail !!
083            Class<?> cls = classGetter.getClass(entity);
084            Entity e = new Entity(cls);
085            return persistenceAdapter.find(cls, (Serializable)converters.convert(classGetter.getIdentifier(entity), e.getIdentifierType()));
086        }
087        
088        if (cache.contains(entity))
089            return entity;
090        
091        if (entity instanceof ChangeRef) {
092            ChangeRef ref = (ChangeRef)entity;
093            try {
094                Class<?> entityClass = TypeUtil.forName(ref.getClassName());
095                Entity e = new Entity(entityClass);                
096                Serializable refId = (Serializable)converters.convert(ref.getId(), e.getIdentifierType());
097                
098                // Lookup in current merge cache, if not found then lookup from persistence adapter
099                for (Object cached : cache) {
100                        if (cached.getClass().equals(entityClass) && refId.equals(e.getIdentifier(cached)))
101                                return cached;
102                }
103                
104                return persistenceAdapter.find(entityClass, refId);
105            }
106            catch (ClassNotFoundException cnfe) {
107                throw new ServiceException("Could not find class " + ref.getClassName(), cnfe);
108            }
109        }
110
111        cache.add(entity);
112
113        if (entity != null && classGetter.isEntity(entity) && classGetter.isInitialized(null, null, entity)) {
114            // If the entity has an id, we should get the managed instance
115            Entity e = new Entity(entity);
116            Object id = e.getIdentifier();
117            if (id != null)
118                return persistenceAdapter.find(entity.getClass(), (Serializable)id);
119
120            cache.add(entity);
121            
122            // If there is no id, traverse the object graph to merge associated objects
123            List<Object[]> fieldValues = classGetter.getFieldValues(entity);
124            for (Object[] fieldValue : fieldValues) {
125                Object value = fieldValue[1];
126                Field field = (Field)fieldValue[0];
127                if (value == null)
128                    continue;
129                
130                if (!classGetter.isInitialized(entity, field.getName(), value)) {                       
131                        if (!classGetter.getClass(value).isAnnotationPresent(javax.persistence.Entity.class))
132                                continue;
133                
134                    try {
135                        // Lookup in cache
136                        Serializable valueId = classGetter.getIdentifier(value);
137                        Object newValue = null;
138                        Entity ve = new Entity(field.getType());
139                        for (Object cached : cache) {
140                                if (field.getType().isInstance(cached) && valueId.equals(ve.getIdentifier(cached))) {
141                                        newValue = cached;
142                                        break;
143                                }
144                        }                       
145                        if (newValue == null)
146                                newValue = persistenceAdapter.find(field.getType(), valueId);
147                        
148                        field.set(entity, newValue);
149                    }
150                    catch (IllegalAccessException e1) {
151                        throw new ServiceException("Could not set entity field value on " + value.getClass() + "." + field.getName());
152                    }
153                }
154                
155                if (value instanceof Collection<?>) {
156                    @SuppressWarnings("unchecked")
157                                        Collection<Object> coll = (Collection<Object>)value;
158                    Iterator<Object> icoll = coll.iterator();
159                    Set<Object> addedElements = new HashSet<Object>();
160                    int idx = 0;
161                    while (icoll.hasNext()) {
162                        Object element = icoll.next();
163                        if (element != null) {
164                                Object newElement = mergeObject(element, cache);
165                                if (newElement != element) {
166                                    if (coll instanceof List<?>)
167                                        ((List<Object>)coll).set(idx, newElement);
168                                    else {
169                                        icoll.remove();
170                                        addedElements.add(newElement);
171                                    }
172                                }
173                        }
174                        idx++;
175                    }
176                    if (!(coll instanceof List<?>))
177                        coll.addAll(addedElements);
178                }
179                else if (value.getClass().isArray()) {
180                    for (int idx = 0; idx < Array.getLength(value); idx++) {
181                        Object element = Array.get(value, idx);
182                        if (element == null)
183                            continue;
184                        Object newElement = mergeObject(element, cache);
185                        if (newElement != element)
186                            Array.set(value, idx, newElement);
187                    }
188                }
189                else if (value instanceof Map<?, ?>) {
190                    @SuppressWarnings("unchecked")
191                                        Map<Object, Object> map = (Map<Object, Object>)value;
192                    Iterator<Entry<Object, Object>> ime = map.entrySet().iterator();
193                    Map<Object, Object> addedElements = new HashMap<Object, Object>();
194                    while (ime.hasNext()) {
195                        Entry<Object, Object> me = ime.next();
196                        Object val = me.getValue();
197                        if (val != null) {
198                            Object newVal = mergeObject(val, cache);
199                            if (newVal != val)
200                                me.setValue(newVal);
201                        }
202                        Object key = me.getKey();
203                        if (key != null) {
204                            Object newKey = mergeObject(key, cache);
205                            if (newKey != key) {
206                                ime.remove();
207                                addedElements.put(newKey, me.getValue());
208                            }
209                        }
210                    }
211                    map.putAll(addedElements);
212                }
213                else if (classGetter.isEntity(value)) {
214                    Object newValue = mergeObject(value, cache);
215                    if (newValue != value) {
216                        try {
217                            field.set(entity, newValue);
218                        }
219                        catch (IllegalAccessException e1) {
220                            throw new ServiceException("Could not set entity field value on " + value.getClass() + "." + field.getName());
221                        }
222                    }
223                }
224            }
225        }
226
227        return entity;
228    }
229
230        public Object[] applyChanges(ChangeSet changeSet) {
231                Set<Object> cache = new HashSet<Object>();
232                Object[] appliedChanges = new Object[changeSet.getChanges().length];
233                for (int i = 0; i < changeSet.getChanges().length; i++)
234                        appliedChanges[i] = applyChange(changeSet.getChanges()[i], cache);
235                
236                return appliedChanges;
237        }
238        
239        @SuppressWarnings("unchecked")
240        private Object applyChange(Change change, Set<Object> cache) {
241                Converters converters = GraniteContext.getCurrentInstance().getGraniteConfig().getConverters();
242                
243                Object appliedChange = null;
244                try {
245                        Class<?> entityClass = TypeUtil.forName(change.getClassName());
246                        if (change.getId() != null) {
247                Entity e = new Entity(entityClass);
248                Type identifierType = e.getIdentifierType();
249                                Object entity = persistenceAdapter.find(entityClass, (Serializable)converters.convert(change.getId(), identifierType));
250                                if (entity == null) {
251                                        log.debug("Entity not found, maybe has already been deleted by cascading");
252                                        return null;
253                                }
254                                
255                                e = new Entity(entity);
256                Long version = getVersion(e);
257                                if ((change.getVersion() != null && change.getVersion().longValue() < version) || (change.getVersion() == null && version != null))
258                                        persistenceAdapter.throwOptimisticLockException(entity);
259                                
260                                appliedChange = entity;
261                                
262                                for (Entry<String, Object> me : change.getChanges().entrySet()) {
263                                        try {
264                                                PropertyDescriptor[] propertyDescriptors = Introspector.getPropertyDescriptors(entityClass);
265                                                PropertyDescriptor propertyDescriptor = null;
266                                                for (PropertyDescriptor pd : propertyDescriptors) {
267                                                        if (pd.getName().equals(me.getKey())) {
268                                                                propertyDescriptor = pd;
269                                                                break;
270                                                        }
271                                                }
272                                                if (propertyDescriptor == null) {
273                                                        log.warn("Could not find property " + me.getKey() + " on class " + change.getClassName());
274                                                }
275                                                else {
276                                                        if (me.getValue() instanceof CollectionChanges) {
277                                                                Object collection = propertyDescriptor.getReadMethod().invoke(entity);
278                                                                
279                                                                CollectionChanges collectionChanges = (CollectionChanges)me.getValue();
280                                                                for (CollectionChange collectionChange : collectionChanges.getChanges()) {
281                                                                        if (collectionChange.getType() == 1) {
282                                                                                if (collection instanceof Set<?>) {
283                                                                                        Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
284                                                                                        Type elementType = Object.class;
285                                                                                        if (collectionType instanceof ParameterizedType)
286                                                                                                elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
287                                                                                        
288                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
289                                                                                        ((Set<Object>)collection).add(value);
290                                                                                }
291                                                                                else if (collection instanceof List<?>) {
292                                                                                        Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
293                                                                                        Type elementType = Object.class;
294                                                                                        if (collectionType instanceof ParameterizedType)
295                                                                                                elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
296                                                                                        
297                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
298                                                                                        ((List<Object>)collection).add((Integer)collectionChange.getKey(), value);
299                                                                                }
300                                                                                else if (collection instanceof Map<?, ?>) {
301                                                                                        Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
302                                                                                        Type keyType = Object.class;
303                                                                                        Type valueType = Object.class;
304                                                                                        if (mapType instanceof ParameterizedType) {
305                                                                                                keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
306                                                                                                valueType = ((ParameterizedType)mapType).getActualTypeArguments()[1];
307                                                                                        }
308                                                                                        
309                                                                                        Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
310                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), valueType);
311                                                                                        ((Map<Object, Object>)collection).put(key, value);
312                                                                                }
313                                                                        }
314                                                                        else if (collectionChange.getType() == -1) {
315                                                                                if (collection instanceof Set<?>) {
316                                                                                        Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
317                                                                                        Type elementType = Object.class;
318                                                                                        if (collectionType instanceof ParameterizedType)
319                                                                                                elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
320                                                                                        
321                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
322                                                                                        Object removed = ((Set<Object>)collection).remove(value);
323                                                                                        cache.add(removed);
324                                                                                }
325                                                                                else if (collection instanceof List<?>) {
326                                                                                        int index = (Integer)collectionChange.getKey();
327                                                                                        Object removed = ((List<Object>)collection).remove(index);
328                                                                                        cache.add(removed);
329                                                                                }
330                                                                                else if (collection instanceof Map<?, ?>) {
331                                                                                        Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
332                                                                                        Type keyType = Object.class;
333                                                                                        if (mapType instanceof ParameterizedType)
334                                                                                                keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
335                                                                                        
336                                                                                        Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
337                                                                                        Object removed = ((Map<Object, Object>)collection).remove(key);
338                                                                                        cache.add(removed);
339                                                                                }
340                                                                        }
341                                                                        else if (collectionChange.getType() == 0) {
342                                                                                if (collection instanceof Set<?>) {
343                                                                                        // This should not happen with a Set !!
344                                                                                        throw new IllegalStateException("Cannot replace an indexed element on a Set, don't use setItemAt on an ArrayCollection representing a Set !");
345                                                                                }
346                                                                                else if (collection instanceof List<?>) {
347                                                                                        int index = (Integer)collectionChange.getKey();
348                                                                                        Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
349                                                                                        Type elementType = Object.class;
350                                                                                        if (collectionType instanceof ParameterizedType)
351                                                                                                elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
352                                                                                        
353                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
354                                                                                        ((List<Object>)collection).set(index, value);
355                                                                                }
356                                                                                else if (collection instanceof Map<?, ?>) {
357                                                                                        Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
358                                                                                        Type keyType = Object.class;
359                                                                                        Type valueType = Object.class;
360                                                                                        if (mapType instanceof ParameterizedType) {
361                                                                                                keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
362                                                                                                valueType = ((ParameterizedType)mapType).getActualTypeArguments()[1];
363                                                                                        }
364                                                                                        
365                                                                                        Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
366                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), valueType);
367                                                                                        ((Map<Object, Object>)collection).put(key, value);
368                                                                                }
369                                                                        }
370                                                                }
371                                                        }
372                                                        else {
373                                                                if (propertyDescriptor.getWriteMethod() != null) {
374                                                                        Object value = mergeObject(me.getValue(), cache);
375                                                                        value = converters.convert(value, propertyDescriptor.getWriteMethod().getGenericParameterTypes()[0]);
376                                                                        propertyDescriptor.getWriteMethod().invoke(entity, value);
377                                                                }
378                                                                else
379                                                                        log.warn("Property " + me.getKey() + " on class " + change.getClassName() + " is not writeable");
380                                                        }
381                                                }
382                                        }
383                                        catch (InvocationTargetException ite) {
384                                                throw new ServiceException("Could not set property " + me.getKey(), ite.getTargetException());
385                                        }
386                                        catch (IllegalAccessException iae) {
387                                                throw new ServiceException("Could not set property " + me.getKey(), iae);
388                                        }
389                                }
390                        }
391                        
392                        return appliedChange;
393                }
394                catch (ClassNotFoundException cnfe) {
395                        throw new ServiceException("Could not find class " + change.getClassName(), cnfe);
396                }
397        }
398
399}