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        if (entity == null)
079            return null;
080
081        ClassGetter classGetter = GraniteContext.getCurrentInstance().getGraniteConfig().getClassGetter();
082        Converters converters = GraniteContext.getCurrentInstance().getGraniteConfig().getConverters();
083        
084        if (entity != null && !classGetter.isInitialized(null, null, entity)) {
085            // cache.contains() cannot be called on un unintialized proxy because hashCode will fail !!
086            Class<?> cls = classGetter.getClass(entity);
087            Entity e = new Entity(cls);
088            return persistenceAdapter.find(cls, (Serializable)converters.convert(classGetter.getIdentifier(entity), e.getIdentifierType()));
089        }
090        
091        if (cache.contains(entity))
092            return entity;
093        
094        if (entity instanceof ChangeRef) {
095            ChangeRef ref = (ChangeRef)entity;
096            try {
097                Class<?> entityClass = TypeUtil.forName(ref.getClassName());
098                Entity e = new Entity(entityClass);                
099                Serializable refId = (Serializable)converters.convert(ref.getId(), e.getIdentifierType());
100                
101                // Lookup in current merge cache, if not found then lookup from persistence adapter
102                for (Object cached : cache) {
103                        if (cached.getClass().equals(entityClass) && refId.equals(e.getIdentifier(cached)))
104                                return cached;
105                }
106                
107                return persistenceAdapter.find(entityClass, refId);
108            }
109            catch (ClassNotFoundException cnfe) {
110                throw new ServiceException("Could not find class " + ref.getClassName(), cnfe);
111            }
112        }
113
114        cache.add(entity);
115
116        if (entity != null && classGetter.isEntity(entity) && classGetter.isInitialized(null, null, entity)) {
117            // If the entity has an id, we should get the managed instance
118            Entity e = new Entity(entity);
119            Object id = e.getIdentifier();
120            if (id != null)
121                return persistenceAdapter.find(entity.getClass(), (Serializable)id);
122
123            cache.add(entity);
124            
125            // If there is no id, traverse the object graph to merge associated objects
126            List<Object[]> fieldValues = classGetter.getFieldValues(entity);
127            for (Object[] fieldValue : fieldValues) {
128                Object value = fieldValue[1];
129                Field field = (Field)fieldValue[0];
130                if (value == null)
131                    continue;
132                
133                if (!classGetter.isInitialized(entity, field.getName(), value)) {                       
134                        if (!classGetter.getClass(value).isAnnotationPresent(javax.persistence.Entity.class))
135                                continue;
136                
137                    try {
138                        // Lookup in cache
139                        Serializable valueId = classGetter.getIdentifier(value);
140                        Object newValue = null;
141                        Entity ve = new Entity(field.getType());
142                        for (Object cached : cache) {
143                                if (field.getType().isInstance(cached) && valueId.equals(ve.getIdentifier(cached))) {
144                                        newValue = cached;
145                                        break;
146                                }
147                        }                       
148                        if (newValue == null)
149                                newValue = persistenceAdapter.find(field.getType(), valueId);
150                        
151                        field.set(entity, newValue);
152                    }
153                    catch (IllegalAccessException e1) {
154                        throw new ServiceException("Could not set entity field value on " + value.getClass() + "." + field.getName());
155                    }
156                }
157                
158                if (value instanceof Collection<?>) {
159                    @SuppressWarnings("unchecked")
160                                        Collection<Object> coll = (Collection<Object>)value;
161                    Iterator<Object> icoll = coll.iterator();
162                    Set<Object> addedElements = new HashSet<Object>();
163                    int idx = 0;
164                    while (icoll.hasNext()) {
165                        Object element = icoll.next();
166                        if (element != null) {
167                                Object newElement = mergeObject(element, cache);
168                                if (newElement != element) {
169                                    if (coll instanceof List<?>)
170                                        ((List<Object>)coll).set(idx, newElement);
171                                    else {
172                                        icoll.remove();
173                                        addedElements.add(newElement);
174                                    }
175                                }
176                        }
177                        idx++;
178                    }
179                    if (!(coll instanceof List<?>))
180                        coll.addAll(addedElements);
181                }
182                else if (value.getClass().isArray()) {
183                    for (int idx = 0; idx < Array.getLength(value); idx++) {
184                        Object element = Array.get(value, idx);
185                        if (element == null)
186                            continue;
187                        Object newElement = mergeObject(element, cache);
188                        if (newElement != element)
189                            Array.set(value, idx, newElement);
190                    }
191                }
192                else if (value instanceof Map<?, ?>) {
193                    @SuppressWarnings("unchecked")
194                                        Map<Object, Object> map = (Map<Object, Object>)value;
195                    Iterator<Entry<Object, Object>> ime = map.entrySet().iterator();
196                    Map<Object, Object> addedElements = new HashMap<Object, Object>();
197                    while (ime.hasNext()) {
198                        Entry<Object, Object> me = ime.next();
199                        Object val = me.getValue();
200                        if (val != null) {
201                            Object newVal = mergeObject(val, cache);
202                            if (newVal != val)
203                                me.setValue(newVal);
204                        }
205                        Object key = me.getKey();
206                        if (key != null) {
207                            Object newKey = mergeObject(key, cache);
208                            if (newKey != key) {
209                                ime.remove();
210                                addedElements.put(newKey, me.getValue());
211                            }
212                        }
213                    }
214                    map.putAll(addedElements);
215                }
216                else if (classGetter.isEntity(value)) {
217                    Object newValue = mergeObject(value, cache);
218                    if (newValue != value) {
219                        try {
220                            field.set(entity, newValue);
221                        }
222                        catch (IllegalAccessException e1) {
223                            throw new ServiceException("Could not set entity field value on " + value.getClass() + "." + field.getName());
224                        }
225                    }
226                }
227            }
228        }
229
230        return entity;
231    }
232
233        public Object[] applyChanges(ChangeSet changeSet) {
234                Set<Object> cache = new HashSet<Object>();
235                Object[] appliedChanges = new Object[changeSet.getChanges().length];
236                for (int i = 0; i < changeSet.getChanges().length; i++)
237                        appliedChanges[i] = applyChange(changeSet.getChanges()[i], cache);
238                
239                return appliedChanges;
240        }
241        
242        @SuppressWarnings("unchecked")
243        private Object applyChange(Change change, Set<Object> cache) {
244                Converters converters = GraniteContext.getCurrentInstance().getGraniteConfig().getConverters();
245                
246                Object appliedChange = null;
247                try {
248                        Class<?> entityClass = TypeUtil.forName(change.getClassName());
249                        if (change.getId() != null) {
250                Entity e = new Entity(entityClass);
251                Type identifierType = e.getIdentifierType();
252                                Object entity = persistenceAdapter.find(entityClass, (Serializable)converters.convert(change.getId(), identifierType));
253                                if (entity == null) {
254                                        log.debug("Entity not found, maybe has already been deleted by cascading");
255                                        return null;
256                                }
257                                
258                                e = new Entity(entity);
259                Long version = getVersion(e);
260                                if ((change.getVersion() != null && change.getVersion().longValue() < version) || (change.getVersion() == null && version != null))
261                                        persistenceAdapter.throwOptimisticLockException(entity);
262                                
263                                appliedChange = entity;
264                                
265                                for (Entry<String, Object> me : change.getChanges().entrySet()) {
266                                        try {
267                                                PropertyDescriptor[] propertyDescriptors = Introspector.getPropertyDescriptors(entityClass);
268                                                PropertyDescriptor propertyDescriptor = null;
269                                                for (PropertyDescriptor pd : propertyDescriptors) {
270                                                        if (pd.getName().equals(me.getKey())) {
271                                                                propertyDescriptor = pd;
272                                                                break;
273                                                        }
274                                                }
275                                                if (propertyDescriptor == null) {
276                                                        log.warn("Could not find property " + me.getKey() + " on class " + change.getClassName());
277                                                }
278                                                else {
279                                                        if (me.getValue() instanceof CollectionChanges) {
280                                                                Object collection = propertyDescriptor.getReadMethod().invoke(entity);
281                                                                
282                                                                CollectionChanges collectionChanges = (CollectionChanges)me.getValue();
283                                                                for (CollectionChange collectionChange : collectionChanges.getChanges()) {
284                                                                        if (collectionChange.getType() == 1) {
285                                                                                if (collection instanceof Set<?>) {
286                                                                                        Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
287                                                                                        Type elementType = Object.class;
288                                                                                        if (collectionType instanceof ParameterizedType)
289                                                                                                elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
290                                                                                        
291                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
292                                                                                        ((Set<Object>)collection).add(value);
293                                                                                }
294                                                                                else if (collection instanceof List<?>) {
295                                                                                        Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
296                                                                                        Type elementType = Object.class;
297                                                                                        if (collectionType instanceof ParameterizedType)
298                                                                                                elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
299                                                                                        
300                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
301                                                                                        ((List<Object>)collection).add((Integer)collectionChange.getKey(), value);
302                                                                                }
303                                                                                else if (collection instanceof Map<?, ?>) {
304                                                                                        Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
305                                                                                        Type keyType = Object.class;
306                                                                                        Type valueType = Object.class;
307                                                                                        if (mapType instanceof ParameterizedType) {
308                                                                                                keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
309                                                                                                valueType = ((ParameterizedType)mapType).getActualTypeArguments()[1];
310                                                                                        }
311                                                                                        
312                                                                                        Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
313                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), valueType);
314                                                                                        ((Map<Object, Object>)collection).put(key, value);
315                                                                                }
316                                                                        }
317                                                                        else if (collectionChange.getType() == -1) {
318                                                                                if (collection instanceof Set<?>) {
319                                                                                        Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
320                                                                                        Type elementType = Object.class;
321                                                                                        if (collectionType instanceof ParameterizedType)
322                                                                                                elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
323                                                                                        
324                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
325                                                                                        Object removed = ((Set<Object>)collection).remove(value);
326                                                                                        cache.add(removed);
327                                                                                }
328                                                                                else if (collection instanceof List<?>) {
329                                                                                        int index = (Integer)collectionChange.getKey();
330                                                                                        Object removed = ((List<Object>)collection).remove(index);
331                                                                                        cache.add(removed);
332                                                                                }
333                                                                                else if (collection instanceof Map<?, ?>) {
334                                                                                        Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
335                                                                                        Type keyType = Object.class;
336                                                                                        if (mapType instanceof ParameterizedType)
337                                                                                                keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
338                                                                                        
339                                                                                        Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
340                                                                                        Object removed = ((Map<Object, Object>)collection).remove(key);
341                                                                                        cache.add(removed);
342                                                                                }
343                                                                        }
344                                                                        else if (collectionChange.getType() == 0) {
345                                                                                if (collection instanceof Set<?>) {
346                                                                                        // This should not happen with a Set !!
347                                                                                        throw new IllegalStateException("Cannot replace an indexed element on a Set, don't use setItemAt on an ArrayCollection representing a Set !");
348                                                                                }
349                                                                                else if (collection instanceof List<?>) {
350                                                                                        int index = (Integer)collectionChange.getKey();
351                                                                                        Type collectionType = propertyDescriptor.getReadMethod().getGenericReturnType();
352                                                                                        Type elementType = Object.class;
353                                                                                        if (collectionType instanceof ParameterizedType)
354                                                                                                elementType = ((ParameterizedType)collectionType).getActualTypeArguments()[0];
355                                                                                        
356                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), elementType);
357                                                                                        ((List<Object>)collection).set(index, value);
358                                                                                }
359                                                                                else if (collection instanceof Map<?, ?>) {
360                                                                                        Type mapType = propertyDescriptor.getReadMethod().getGenericReturnType();
361                                                                                        Type keyType = Object.class;
362                                                                                        Type valueType = Object.class;
363                                                                                        if (mapType instanceof ParameterizedType) {
364                                                                                                keyType = ((ParameterizedType)mapType).getActualTypeArguments()[0];
365                                                                                                valueType = ((ParameterizedType)mapType).getActualTypeArguments()[1];
366                                                                                        }
367                                                                                        
368                                                                                        Object key = converters.convert(mergeObject(collectionChange.getKey(), cache), keyType);
369                                                                                        Object value = converters.convert(mergeObject(collectionChange.getValue(), cache), valueType);
370                                                                                        ((Map<Object, Object>)collection).put(key, value);
371                                                                                }
372                                                                        }
373                                                                }
374                                                        }
375                                                        else {
376                                                                if (propertyDescriptor.getWriteMethod() != null) {
377                                                                        Object value = mergeObject(me.getValue(), cache);
378                                                                        value = converters.convert(value, propertyDescriptor.getWriteMethod().getGenericParameterTypes()[0]);
379                                                                        propertyDescriptor.getWriteMethod().invoke(entity, value);
380                                                                }
381                                                                else
382                                                                        log.warn("Property " + me.getKey() + " on class " + change.getClassName() + " is not writeable");
383                                                        }
384                                                }
385                                        }
386                                        catch (InvocationTargetException ite) {
387                                                throw new ServiceException("Could not set property " + me.getKey(), ite.getTargetException());
388                                        }
389                                        catch (IllegalAccessException iae) {
390                                                throw new ServiceException("Could not set property " + me.getKey(), iae);
391                                        }
392                                }
393                        }
394                        
395                        return appliedChange;
396                }
397                catch (ClassNotFoundException cnfe) {
398                        throw new ServiceException("Could not find class " + change.getClassName(), cnfe);
399                }
400        }
401
402}