001    /*
002      GRANITE DATA SERVICES
003      Copyright (C) 2013 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.persistence;
022    
023    import java.lang.annotation.Annotation;
024    import java.lang.annotation.ElementType;
025    import java.lang.annotation.Target;
026    import java.lang.reflect.Field;
027    import java.lang.reflect.Method;
028    import java.lang.reflect.Modifier;
029    import java.util.concurrent.ConcurrentHashMap;
030    import java.util.concurrent.ConcurrentMap;
031    
032    import org.granite.client.persistence.collection.PersistentCollection;
033    
034    /**
035     * @author Franck WOLFF
036     */
037    public class Persistence {
038    
039            private static final String INITIALIZED_FIELD_NAME = "__initialized__";
040            private static final String DETACHED_STATE_FIELD_NAME = "__detachedState__";
041            
042            private static final PropertyAccessor NULL_PROPERTY_ACCESSOR = new MethodAccessor(null, null, null);
043            
044            private static final ConcurrentMap<Class<?>, PropertyAccessor> idAccessors = new ConcurrentHashMap<Class<?>, PropertyAccessor>();
045            private static final ConcurrentMap<Class<?>, PropertyAccessor> uidAccessors = new ConcurrentHashMap<Class<?>, PropertyAccessor>();
046            private static final ConcurrentMap<Class<?>, PropertyAccessor> versionAccessors = new ConcurrentHashMap<Class<?>, PropertyAccessor>();
047            
048            private static final Field NULL_FIELD;
049            static {
050                    try {
051                            NULL_FIELD = Persistence.class.getDeclaredField("NULL_FIELD");
052                    }
053                    catch (Exception e) {
054                            throw new ExceptionInInitializerError(e);
055                    }
056            }
057            
058            private static final ConcurrentMap<Class<?>, Field> initializedFields = new ConcurrentHashMap<Class<?>, Field>();
059            private static final ConcurrentMap<Class<?>, Field> detachedStateFields = new ConcurrentHashMap<Class<?>, Field>();
060    
061            public static Field getInitializedField(Class<?> cls) {
062                    return findField(cls, initializedFields, INITIALIZED_FIELD_NAME, Boolean.TYPE);
063            }
064    
065            public static Field getDetachedStateField(Class<?> cls) {
066                    return findField(cls, detachedStateFields, DETACHED_STATE_FIELD_NAME, String.class);
067            }
068            
069            public static boolean isInitialized(Object o) {
070                    if (o instanceof PersistentCollection)
071                            return ((PersistentCollection)o).wasInitialized();
072                    
073                    Class<?> cls = o.getClass();
074                    if (!cls.isAnnotationPresent(Entity.class))
075                            return true;
076    
077                    Field field = findField(cls, initializedFields, INITIALIZED_FIELD_NAME, Boolean.TYPE);
078                    if (field == null)
079                            return true;
080                    
081                    try {
082                            return field.getBoolean(o);
083                    }
084                    catch (Exception e) {
085                            throw new RuntimeException("Could not get field " + field + " value on object " + o, e);
086                    }
087            }
088            
089            public static void setInitialized(Object o, boolean value) throws IllegalAccessException {
090                    Class<?> cls = o.getClass();
091                    if (!cls.isAnnotationPresent(Entity.class))
092                            throw new IllegalAccessException("Not annotated with @Entity: " + cls);
093                    
094                    Field field = findField(cls, initializedFields, INITIALIZED_FIELD_NAME, Boolean.TYPE);
095                    if (field == null)
096                            throw new IllegalAccessException("Could not find field " + INITIALIZED_FIELD_NAME + " in " + o);
097                    
098                    try {
099                            field.setBoolean(o, value);
100                    }
101                    catch (Exception e) {
102                            throw new RuntimeException("Could not set field " + field + " on object " + o + " to value " + value, e);
103                    }               
104            }
105            
106            public static String getDetachedState(Object o) throws IllegalAccessException {
107                    Class<?> cls = o.getClass();
108                    if (!cls.isAnnotationPresent(Entity.class))
109                            return null;
110    
111                    Field field = findField(cls, detachedStateFields, DETACHED_STATE_FIELD_NAME, String.class);
112                    if (field == null)
113                            throw new IllegalAccessException("Could not find field " + DETACHED_STATE_FIELD_NAME + " in " + o);
114                    
115                    return (String)field.get(o);
116            }
117            
118            public static void setDetachedState(Object o, String value) throws IllegalAccessException {
119                    Class<?> cls = o.getClass();
120                    if (!cls.isAnnotationPresent(Entity.class))
121                            throw new IllegalAccessException("Not annotated with @Entity: " + cls);
122                    
123                    Field field = findField(cls, detachedStateFields, DETACHED_STATE_FIELD_NAME, String.class);
124                    if (field == null)
125                            throw new IllegalAccessException("Could not find field " + DETACHED_STATE_FIELD_NAME + " in " + o);
126                    
127                    try {
128                            field.set(o, value);
129                    }
130                    catch (Exception e) {
131                            throw new RuntimeException("Could not set field " + field + " on object " + o + " to value " + value, e);
132                    }               
133            }
134            
135            @SuppressWarnings("unchecked")
136            public static <T> T getId(Object entity) throws IllegalAccessException {
137                    return (T)getIdProperty(entity).getValue();
138            }
139    
140            public static void setId(Object entity, Object value) throws IllegalAccessException {
141                    getIdProperty(entity).setValue(value);
142            }
143            
144            public static String getUid(Object entity) throws IllegalAccessException {
145                    return (String)getUidProperty(entity).getValue();
146            }
147            
148            public static void setUid(Object entity, String value) throws IllegalAccessException {
149                    getUidProperty(entity).setValue(value);
150            }
151            
152            @SuppressWarnings("unchecked")
153            public static <T> T getVersion(Object entity) throws IllegalAccessException {
154                    return (T)getVersionProperty(entity).getValue();
155            }
156    
157            public static Property getIdProperty(Object entity) throws IllegalAccessException {
158                    return getProperty(entity, idAccessors, Id.class);
159            }
160    
161            public static Property getUidProperty(Object entity) throws IllegalAccessException {
162                    return getProperty(entity, uidAccessors, Uid.class);
163            }
164    
165            public static Property getVersionProperty(Object entity) throws IllegalAccessException {
166                    return getProperty(entity, versionAccessors, Version.class);
167            }
168            
169            private static Property getProperty(Object entity, ConcurrentMap<Class<?>, PropertyAccessor> cache, Class<? extends Annotation> annotationClass) throws IllegalAccessException {
170                    PropertyAccessor accessor = findPropertyAccessor(entity.getClass(), cache, annotationClass);
171                    if (accessor == null)
172                            throw newIllegalAccessException(entity.getClass(), annotationClass);
173                    return new Property(entity, accessor);
174            }
175            
176            private static IllegalAccessException newIllegalAccessException(Class<?> cls, Class<? extends Annotation> annotationClass) {
177                    return new IllegalAccessException("Could not find property annotated with " + annotationClass + " in " + cls);
178            }
179            
180            private static Field findField(Class<?> cls, ConcurrentMap<Class<?>, Field> cache, String name, Class<?> type) {
181                    
182                    Field field = cache.get(cls);
183                    
184                    if (field == null) {
185                            for (Class<?> c = cls; c != null && c != Object.class; c = c.getSuperclass()) {
186                                    try {
187                                            field = c.getDeclaredField(name);
188                                    }
189                                    catch (Exception e) {
190                                            continue;
191                                    }
192                                    
193                                    if (field.getType() != type)
194                                            continue;
195                                    
196                                    field.setAccessible(true);
197                                    break;
198                            }
199                            
200                            if (field == null)
201                                    field = NULL_FIELD;
202                            
203                            Field previous = cache.putIfAbsent(cls, field);
204                            if (previous != null)
205                                    field = previous;
206                    }
207                    
208                    return (field != NULL_FIELD ? field : null);
209            }
210            
211            private static PropertyAccessor findPropertyAccessor(Class<?> cls, ConcurrentMap<Class<?>, PropertyAccessor> cache, Class<? extends Annotation> annotationClass) {
212                    
213                    PropertyAccessor accessor = cache.get(cls);
214                    
215                    if (accessor == null) {
216                            boolean searchFields = false;
217                            boolean searchMethods = false;
218                            
219                            if (!annotationClass.isAnnotationPresent(Target.class))
220                                    searchFields = searchMethods = true;
221                            else {
222                                    Target target = annotationClass.getAnnotation(Target.class);
223                                    for (ElementType targetType : target.value()) {
224                                            if (targetType == ElementType.FIELD)
225                                                    searchFields = true;
226                                            else if (targetType == ElementType.METHOD)
227                                                    searchMethods = true;
228                                    }
229                            }
230                            
231                            if (searchFields == false && searchMethods == false)
232                                    return null;
233                            
234                            final int modifierMask = Modifier.PUBLIC | Modifier.STATIC;
235                            
236                            classLoop:
237                            for (Class<?> c = cls; c != null && c != Object.class; c = c.getSuperclass()) {
238                                    if (searchMethods) {
239                                            for (Method method : c.getDeclaredMethods()) {
240                                                    if ((method.getModifiers() & modifierMask) != Modifier.PUBLIC ||
241                                                            !method.isAnnotationPresent(annotationClass))
242                                                            continue;
243                                                    
244                                                    if (method.getReturnType() == Void.TYPE) {
245                                                            if (method.getName().startsWith("set") && method.getParameterTypes().length == 1) {
246                                                                    String name = method.getName().substring(3);
247                                                                    
248                                                                    if (name.length() == 0)
249                                                                            continue;
250                                                                    
251                                                                    Method getter = null;
252                                                                    try {
253                                                                            getter = cls.getMethod("get" + name);
254                                                                    }
255                                                                    catch (Exception e) {
256                                                                            try {
257                                                                                    getter = cls.getMethod("is" + name);
258                                                                            }
259                                                                            catch (Exception f) {
260                                                                            }
261                                                                    }
262                                                                    
263                                                                    if (getter != null && (getter.getModifiers() & Modifier.STATIC) != 0 &&
264                                                                            getter.getReturnType() != method.getParameterTypes()[0])
265                                                                            getter = null;
266                                                                    
267                                                                    accessor = new MethodAccessor(getter, method, uncapitalizePropertyName(name));
268                                                                    break classLoop;
269                                                            }
270                                                    }
271                                                    else if (method.getParameterTypes().length == 0 && (method.getName().startsWith("get") || method.getName().startsWith("is"))) {
272                                                            String name;
273                                                            if (method.getName().startsWith("get"))
274                                                                    name = method.getName().substring(3);
275                                                            else
276                                                                    name = method.getName().substring(2);
277                                                            
278                                                            if (name.length() == 0)
279                                                                    continue;
280                                                            
281                                                            Method setter = null;
282                                                            try {
283                                                                    setter = cls.getMethod("set" + name);
284                                                            }
285                                                            catch (Exception e) {
286                                                            }
287                                                            
288                                                            if (setter != null && (setter.getModifiers() & Modifier.STATIC) != 0 &&
289                                                                    method.getReturnType() != setter.getParameterTypes()[0])
290                                                                    setter = null;
291                                                            
292                                                            accessor = new MethodAccessor(method, setter, uncapitalizePropertyName(name));
293                                                            break classLoop;
294                                                    }
295                                            }
296                                    }
297                                    
298                                    if (searchFields) {
299                                            for (Field field : c.getDeclaredFields()) {
300                                                    if ((field.getModifiers() & Modifier.STATIC) == 0 && field.isAnnotationPresent(annotationClass)) {
301                                                            accessor = new FieldAccessor(field);
302                                                            break classLoop;
303                                                    }
304                                            }
305                                    }
306                            }
307                            
308                            if (accessor == null)
309                                    accessor = NULL_PROPERTY_ACCESSOR;
310                            
311                            PropertyAccessor previous = cache.putIfAbsent(cls, accessor);
312                            if (previous != null)
313                                    accessor = previous;
314                    }
315                    
316                    return (accessor != NULL_PROPERTY_ACCESSOR ? accessor : null);
317            }
318            
319            private static String uncapitalizePropertyName(String name) {
320                    if (name.length() > 0 && Character.isUpperCase(name.charAt(0)))
321                            name = name.substring(0, 1).toLowerCase() + name.substring(1);
322                    return name;
323            }
324            
325            static interface PropertyAccessor {
326                    
327                    String getName();
328                    Class<?> getType();
329                    
330                    boolean isReadable();
331                    boolean isWritable();
332                    
333                    Object get(Object obj) throws IllegalAccessException;
334                    void set(Object obj, Object value) throws IllegalAccessException;
335            }
336            
337            static class FieldAccessor implements PropertyAccessor {
338    
339                    private final Field field;
340                    
341                    public FieldAccessor(Field field) {
342                            field.setAccessible(true);
343    
344                            this.field = field;
345                    }
346    
347                    public Field getField() {
348                            return field;
349                    }
350    
351                    public String getName() {
352                            return field.getName();
353                    }
354    
355                    public Class<?> getType() {
356                            return field.getType();
357                    }
358    
359                    public boolean isReadable() {
360                            return true;
361                    }
362    
363                    public boolean isWritable() {
364                            return true;
365                    }
366    
367                    public Object get(Object obj) throws IllegalAccessException {
368                            try {
369                                    return field.get(obj);
370                            }
371                            catch (IllegalAccessException e) {
372                                    throw e;
373                            }
374                            catch (Exception e) {
375                                    throw new IllegalAccessException("Could not get field " + field + " value on object " + obj + ": " + e.toString());
376                            }
377                    }
378    
379                    public void set(Object obj, Object value) throws IllegalAccessException {
380                            try {
381                                    field.set(obj, value);
382                            }
383                            catch (IllegalAccessException e) {
384                                    throw e;
385                            }
386                            catch (Exception e) {
387                                    throw new IllegalAccessException("Could not set field " + field + " on object " + obj + " to value " + value + ": " + e.toString());
388                            }
389                    }
390            }
391            
392            static class MethodAccessor implements PropertyAccessor {
393    
394                    private final Method getter;
395                    private final Method setter;
396                    
397                    private final String name;
398    
399                    public MethodAccessor(Method getter, Method setter, String name) {
400                            
401                            if (getter != null)
402                                    getter.setAccessible(true);
403                            if (setter != null)
404                                    setter.setAccessible(true);
405                            
406                            this.getter = getter;
407                            this.setter = setter;
408                            
409                            this.name = name;
410                    }
411    
412                    public Method getGetter() {
413                            return getter;
414                    }
415    
416                    public Method getSetter() {
417                            return setter;
418                    }
419    
420                    public String getName() {
421                            return name;
422                    }
423    
424                    public Class<?> getType() {
425                            return (getter != null ? getter.getReturnType() : setter.getParameterTypes()[0]);
426                    }
427    
428                    public boolean isReadable() {
429                            return getter != null;
430                    }
431    
432                    public boolean isWritable() {
433                            return setter != null;
434                    }
435    
436                    public Object get(Object obj) throws IllegalAccessException {
437                            try {
438                                    return getter.invoke(obj);
439                            }
440                            catch (IllegalAccessException e) {
441                                    throw e;
442                            }
443                            catch (Exception e) {
444                                    throw new IllegalAccessException("Could not invoke getter " + getter + " on object " + obj + ": " + e.toString());
445                            }
446                    }
447    
448                    public void set(Object obj, Object value) throws IllegalAccessException {
449                            try {
450                                    setter.invoke(obj, value);
451                            }
452                            catch (IllegalAccessException e) {
453                                    throw e;
454                            }
455                            catch (Exception e) {
456                                    throw new IllegalAccessException("Could not invoke setter " + setter + " on object " + obj + " with " + value + ": " + e.toString());
457                            }
458                    }
459            }
460            
461            public static class Property {
462                    
463                    private final Object obj;
464                    private final PropertyAccessor accessor;
465    
466                    public Property(Object obj, PropertyAccessor accessor) {
467                            this.obj = obj;
468                            this.accessor = accessor;
469                    }
470                    
471                    public String getName() {
472                            return accessor.getName();
473                    }
474                    
475                    public Class<?> getType() {
476                            return accessor.getType();
477                    }
478    
479                    public boolean isReadable() {
480                            return accessor.isReadable();
481                    }
482    
483                    public boolean isWritable() {
484                            return accessor.isWritable();
485                    }
486    
487                    public Object getValue() throws IllegalAccessException {
488                            return accessor.get(obj);
489                    }
490    
491                    public void setValue(Object value) throws IllegalAccessException {
492                            accessor.set(obj, value);
493                    }
494            }
495    }