1/*-
2 * See the file LICENSE for redistribution information.
3 *
4 * Copyright (c) 2002,2008 Oracle.  All rights reserved.
5 *
6 * $Id: AnnotationModel.java,v 1.1 2008/02/07 17:12:28 mark Exp $
7 */
8
9package com.sleepycat.persist.model;
10
11import java.lang.reflect.Field;
12import java.lang.reflect.Modifier;
13import java.lang.reflect.ParameterizedType;
14import java.lang.reflect.Type;
15import java.util.ArrayList;
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.HashSet;
19import java.util.List;
20import java.util.Map;
21import java.util.Set;
22
23/**
24 * The default annotation-based entity model.  An <code>AnnotationModel</code>
25 * is based on annotations that are specified for entity classes and their key
26 * fields.
27 *
28 * <p>{@code AnnotationModel} objects are thread-safe.  Multiple threads may
29 * safely call the methods of a shared {@code AnnotationModel} object.</p>
30 *
31 * <p>The set of persistent classes in the annotation model is the set of all
32 * classes with the {@link Persistent} or {@link Entity} annotation.</p>
33 *
34 * <p>The annotations used to define persistent classes are: {@link Entity},
35 * {@link Persistent}, {@link PrimaryKey}, {@link SecondaryKey} and {@link
36 * KeyField}.  A good starting point is {@link Entity}.</p>
37 *
38 * @author Mark Hayes
39 */
40public class AnnotationModel extends EntityModel {
41
42    private static class EntityInfo {
43        PrimaryKeyMetadata priKey;
44        Map<String,SecondaryKeyMetadata> secKeys =
45            new HashMap<String,SecondaryKeyMetadata>();
46    }
47
48    private Map<String,ClassMetadata> classMap;
49    private Map<String,EntityInfo> entityMap;
50
51    /**
52     * Constructs a model for annotated entity classes.
53     */
54    public AnnotationModel() {
55        super();
56        classMap = new HashMap<String,ClassMetadata>();
57        entityMap = new HashMap<String,EntityInfo>();
58    }
59
60    /* EntityModel methods */
61
62    @Override
63    public synchronized Set<String> getKnownClasses() {
64        return Collections.unmodifiableSet
65            (new HashSet<String>(classMap.keySet()));
66    }
67
68    @Override
69    public synchronized EntityMetadata getEntityMetadata(String className) {
70        /* Call getClassMetadata to collect metadata. */
71        getClassMetadata(className);
72        /* Return the collected entity metadata. */
73        EntityInfo info = entityMap.get(className);
74        if (info != null) {
75            return new EntityMetadata
76                (className, info.priKey,
77                 Collections.unmodifiableMap(info.secKeys));
78        } else {
79            return null;
80        }
81    }
82
83    @Override
84    public synchronized ClassMetadata getClassMetadata(String className) {
85        ClassMetadata metadata = classMap.get(className);
86        if (metadata == null) {
87            Class<?> type;
88            try {
89                type = EntityModel.classForName(className);
90            } catch (ClassNotFoundException e) {
91                return null;
92            }
93            /* Get class annotation. */
94            Entity entity = type.getAnnotation(Entity.class);
95            Persistent persistent = type.getAnnotation(Persistent.class);
96            if (entity == null && persistent == null) {
97                return null;
98            }
99            if (entity != null && persistent != null) {
100                throw new IllegalArgumentException
101                    ("Both @Entity and @Persistent are not allowed: " +
102                     type.getName());
103            }
104            boolean isEntity;
105            int version;
106            String proxiedClassName;
107            if (entity != null) {
108                isEntity = true;
109                version = entity.version();
110                proxiedClassName = null;
111            } else {
112                isEntity = false;
113                version = persistent.version();
114                Class proxiedClass = persistent.proxyFor();
115                proxiedClassName = (proxiedClass != void.class) ?
116                                    proxiedClass.getName() : null;
117            }
118            /* Get instance fields. */
119            List<Field> fields = new ArrayList<Field>();
120            for (Field field : type.getDeclaredFields()) {
121                int mods = field.getModifiers();
122                if (!Modifier.isTransient(mods) && !Modifier.isStatic(mods)) {
123                    fields.add(field);
124                }
125            }
126            /* Get the rest of the metadata and save it. */
127            metadata = new ClassMetadata
128                (className, version, proxiedClassName, isEntity,
129                 getPrimaryKey(type, fields),
130                 getSecondaryKeys(type, fields),
131                 getCompositeKeyFields(type, fields));
132            classMap.put(className, metadata);
133            /* Add any new information about entities. */
134            updateEntityInfo(metadata);
135        }
136        return metadata;
137    }
138
139    private PrimaryKeyMetadata getPrimaryKey(Class<?> type,
140                                             List<Field> fields) {
141        Field foundField = null;
142        String sequence = null;
143        for (Field field : fields) {
144            PrimaryKey priKey = field.getAnnotation(PrimaryKey.class);
145            if (priKey != null) {
146                if (foundField != null) {
147                    throw new IllegalArgumentException
148                        ("Only one @PrimaryKey allowed: " + type.getName());
149                } else {
150                    foundField = field;
151                    sequence = priKey.sequence();
152                    if (sequence.length() == 0) {
153                        sequence = null;
154                    }
155                }
156            }
157        }
158        if (foundField != null) {
159            return new PrimaryKeyMetadata
160                (foundField.getName(), foundField.getType().getName(),
161                 type.getName(), sequence);
162        } else {
163            return null;
164        }
165    }
166
167    private Map<String,SecondaryKeyMetadata> getSecondaryKeys(Class<?> type,
168                                                         List<Field> fields) {
169        Map<String,SecondaryKeyMetadata> map = null;
170        for (Field field : fields) {
171            SecondaryKey secKey = field.getAnnotation(SecondaryKey.class);
172            if (secKey != null) {
173                Relationship rel = secKey.relate();
174                String elemClassName = null;
175                if (rel == Relationship.ONE_TO_MANY ||
176                    rel == Relationship.MANY_TO_MANY) {
177                    elemClassName = getElementClass(field);
178                }
179                String keyName = secKey.name();
180                if (keyName.length() == 0) {
181                    keyName = field.getName();
182                }
183                Class<?> relatedClass = secKey.relatedEntity();
184                String relatedEntity = (relatedClass != void.class) ?
185                                        relatedClass.getName() : null;
186                DeleteAction deleteAction = (relatedEntity != null) ?
187                                        secKey.onRelatedEntityDelete() : null;
188                SecondaryKeyMetadata metadata = new SecondaryKeyMetadata
189                    (field.getName(), field.getType().getName(),
190                     type.getName(), elemClassName, keyName, rel,
191                     relatedEntity, deleteAction);
192                if (map == null) {
193                    map = new HashMap<String,SecondaryKeyMetadata>();
194                }
195                if (map.put(keyName, metadata) != null) {
196                    throw new IllegalArgumentException
197                        ("Only one @SecondaryKey with the same name allowed: "
198                         + type.getName() + '.' + keyName);
199                }
200            }
201        }
202        if (map != null) {
203            map = Collections.unmodifiableMap(map);
204        }
205        return map;
206    }
207
208    private String getElementClass(Field field) {
209        Class cls = field.getType();
210        if (cls.isArray()) {
211            return cls.getComponentType().getName();
212        }
213        if (java.util.Collection.class.isAssignableFrom(cls)) {
214            Type[] typeArgs = ((ParameterizedType) field.getGenericType()).
215                getActualTypeArguments();
216            if (typeArgs == null ||
217                typeArgs.length != 1 ||
218                !(typeArgs[0] instanceof Class)) {
219                throw new IllegalArgumentException
220                    ("Collection typed secondary key field must have a" +
221                     " single generic type argument: " +
222                     field.getDeclaringClass().getName() + '.' +
223                     field.getName());
224            }
225            return ((Class) typeArgs[0]).getName();
226        }
227        throw new IllegalArgumentException
228            ("ONE_TO_MANY or MANY_TO_MANY secondary key field must have" +
229             " an array or Collection type: " +
230             field.getDeclaringClass().getName() + '.' + field.getName());
231    }
232
233    private List<FieldMetadata> getCompositeKeyFields(Class<?> type,
234                                                      List<Field> fields) {
235        List<FieldMetadata> list = null;
236        for (Field field : fields) {
237            KeyField keyField = field.getAnnotation(KeyField.class);
238            if (keyField != null) {
239                int value = keyField.value();
240                if (value < 1 || value > fields.size()) {
241                    throw new IllegalArgumentException
242                        ("Unreasonable @KeyField index value " + value +
243                         ": " + type.getName());
244                }
245                if (list == null) {
246                    list = new ArrayList<FieldMetadata>(fields.size());
247                }
248                if (value <= list.size() && list.get(value - 1) != null) {
249                    throw new IllegalArgumentException
250                        ("@KeyField index value " + value +
251                         " is used more than once: " + type.getName());
252                }
253                while (value > list.size()) {
254                    list.add(null);
255                }
256                FieldMetadata metadata = new FieldMetadata
257                    (field.getName(), field.getType().getName(),
258                     type.getName());
259                list.set(value - 1, metadata);
260            }
261        }
262        if (list != null) {
263            if (list.size() < fields.size()) {
264                throw new IllegalArgumentException
265                    ("@KeyField is missing on one or more instance fields: " +
266                     type.getName());
267            }
268            for (int i = 0; i < list.size(); i += 1) {
269                if (list.get(i) == null) {
270                    throw new IllegalArgumentException
271                        ("@KeyField is missing for index value " + (i + 1) +
272                         ": " + type.getName());
273                }
274            }
275        }
276        if (list != null) {
277            list = Collections.unmodifiableList(list);
278        }
279        return list;
280    }
281
282    /**
283     * Add newly discovered metadata to our stash of entity info.  This info
284     * is maintained as it is discovered because it would be expensive to
285     * create it on demand -- all class metadata would have to be traversed.
286     */
287    private void updateEntityInfo(ClassMetadata metadata) {
288
289        /*
290         * Find out whether this class or its superclass is an entity.  In the
291         * process, traverse all superclasses to load their metadata -- this
292         * will populate as much entity info as possible.
293         */
294        String entityClass = null;
295        PrimaryKeyMetadata priKey = null;
296        Map<String,SecondaryKeyMetadata> secKeys =
297            new HashMap<String,SecondaryKeyMetadata>();
298        for (ClassMetadata data = metadata; data != null;) {
299            if (data.isEntityClass()) {
300                if (entityClass != null) {
301                    throw new IllegalArgumentException
302                        ("An entity class may not derived from another" +
303                         " entity class: " + entityClass +
304                         ' ' + data.getClassName());
305                }
306                entityClass = data.getClassName();
307            }
308            /* Save first primary key encountered. */
309            if (priKey == null) {
310                priKey = data.getPrimaryKey();
311            }
312            /* Save all secondary keys encountered by key name. */
313            Map<String,SecondaryKeyMetadata> classSecKeys =
314                data.getSecondaryKeys();
315            if (classSecKeys != null) {
316                for (SecondaryKeyMetadata secKey : classSecKeys.values()) {
317                    secKeys.put(secKey.getKeyName(), secKey);
318                }
319            }
320            /* Load superclass metadata. */
321            Class cls;
322            try {
323                cls = EntityModel.classForName(data.getClassName());
324            } catch (ClassNotFoundException e) {
325                throw new IllegalStateException(e);
326            }
327            cls = cls.getSuperclass();
328            if (cls != Object.class) {
329                data = getClassMetadata(cls.getName());
330                if (data == null) {
331                    throw new IllegalArgumentException
332                        ("Persistent class has non-persistent superclass: " +
333                         cls.getName());
334                }
335            } else {
336                data = null;
337            }
338        }
339
340        /* Add primary and secondary key entity info. */
341        if (entityClass != null) {
342            EntityInfo info = entityMap.get(entityClass);
343            if (info == null) {
344                info = new EntityInfo();
345                entityMap.put(entityClass, info);
346            }
347            if (priKey == null) {
348                throw new IllegalArgumentException
349                    ("Entity class has no primary key: " + entityClass);
350            }
351            info.priKey = priKey;
352            info.secKeys.putAll(secKeys);
353        }
354    }
355}
356