001    /*
002     * Copyright 2004 Chris Nelson
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
007     * Unless required by applicable law or agreed to in writing,
008     * software distributed under the License is distributed on an "AS IS" BASIS,
009     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010     * See the License for the specific language governing permissions and limitations under the License.
011     */
012    package org.tynamo.hibernate.services;
013    
014    import ognl.Ognl;
015    import ognl.OgnlException;
016    import org.apache.commons.logging.Log;
017    import org.apache.commons.logging.LogFactory;
018    import org.apache.tapestry5.hibernate.HibernateSessionSource;
019    import org.hibernate.HibernateException;
020    import org.hibernate.cfg.Configuration;
021    import org.hibernate.mapping.*;
022    import org.hibernate.metadata.ClassMetadata;
023    import org.hibernate.metadata.CollectionMetadata;
024    import org.hibernate.type.ComponentType;
025    import org.hibernate.type.Type;
026    import org.tynamo.descriptor.*;
027    import org.tynamo.descriptor.extension.EnumReferenceDescriptor;
028    import org.tynamo.exception.MetadataNotFoundException;
029    import org.tynamo.exception.TynamoRuntimeException;
030    
031    import java.beans.IntrospectionException;
032    import java.beans.Introspector;
033    import java.beans.PropertyDescriptor;
034    import java.lang.reflect.Field;
035    import java.lang.reflect.Method;
036    import java.util.ArrayList;
037    import java.util.Collection;
038    import java.util.Iterator;
039    import java.util.List;
040    
041    /**
042     * This decorator will add metadata information. It will replace simple
043     * reflection based TynamoPropertyTynamoPropertyDescriptors with appropriate
044     * Hibernate descriptors <p/> Background... TynamoDescriptorService operates one
045     * ReflectorDescriptorFactory - TynamoDescriptorService iterates/scans all class
046     * types encountered - ReflectorDescriptorFactory allocates property descriptor
047     * instance for the class type - TynamoDescriptorService decorates property
048     * descriptor by calling this module HibernateDescriptorDecorator -
049     * HibernateDescriptorDecorator caches the decorated property descriptor into a
050     * decorated descriptor list - decorated descriptor list gets populated into
051     * class descriptor for class type - TynamoDescriptorService finally populates
052     * decorated class descriptor and it's aggregated list of decorated property
053     * descriptors into it's own list/cache of referenced class descriptors
054     * 
055     * @see TynamoPropertyDescriptor
056     * @see ObjectReferenceDescriptor
057     * @see CollectionDescriptor
058     * @see EmbeddedDescriptor
059     */
060    public class HibernateDescriptorDecorator implements DescriptorDecorator
061    {
062            protected static final Log LOG = LogFactory.getLog(HibernateDescriptorDecorator.class);
063    
064            private HibernateSessionSource hibernateSessionSource;
065    
066            private DescriptorFactory descriptorFactory;
067    
068            /**
069             * Columns longer than this will have their large property set to true.
070             */
071            private final int largeColumnLength;
072    
073            private final boolean ignoreNonHibernateTypes;
074    
075            public HibernateDescriptorDecorator(HibernateSessionSource hibernateSessionSource, DescriptorFactory descriptorFactory, int largeColumnLength, boolean ignoreNonHibernateTypes)
076            {
077                    this.hibernateSessionSource = hibernateSessionSource;
078                    this.descriptorFactory = descriptorFactory;
079                    this.largeColumnLength = largeColumnLength;
080                    this.ignoreNonHibernateTypes = ignoreNonHibernateTypes;
081            }
082    
083            public TynamoClassDescriptor decorate(TynamoClassDescriptor descriptor)
084            {
085                    ArrayList<TynamoPropertyDescriptor> decoratedPropertyDescriptors = new ArrayList<TynamoPropertyDescriptor>();
086    
087                    Class type = descriptor.getType();
088                    ClassMetadata classMetaData = null;
089    
090                    try
091                    {
092                            classMetaData = findMetadata(type);
093                    } catch (MetadataNotFoundException e)
094                    {
095                            if (ignoreNonHibernateTypes) {
096                                    LOG.warn("MetadataNotFound! Ignoring:" + descriptor.getType().toString());
097                                    return descriptor;
098                            } else {
099                                    throw new TynamoRuntimeException(e);
100                            }
101                    }
102    
103                    for (TynamoPropertyDescriptor propertyDescriptor : descriptor.getPropertyDescriptors())
104                    {
105                            try
106                            {
107                                    TynamoPropertyDescriptor descriptorReference;
108    
109                                    if (propertyDescriptor.getName().equals(getIdentifierProperty(type)))
110                                    {
111                                            descriptorReference = createIdentifierDescriptor(type, propertyDescriptor, descriptor);
112                                    } else if (notAHibernateProperty(classMetaData, propertyDescriptor))
113                                    {
114                                            // If this is not a hibernate property (i.e. marked
115                                            // Transient), it's certainly not searchable
116                                            // Are the any other properties like this?
117                                            propertyDescriptor.setSearchable(false);
118                                            descriptorReference = propertyDescriptor;
119                                    } else
120                                    {
121                                            Property mappingProperty = getMapping(type).getProperty(propertyDescriptor.getName());
122                                            descriptorReference = decoratePropertyDescriptor(type, mappingProperty, propertyDescriptor,
123                                                            descriptor);
124                                    }
125    
126                                    decoratedPropertyDescriptors.add(descriptorReference);
127    
128                            } catch (HibernateException e)
129                            {
130                                    throw new TynamoRuntimeException(e);
131                            }
132                    }
133                    descriptor.setPropertyDescriptors(decoratedPropertyDescriptors);
134                    return descriptor;
135            }
136    
137            protected TynamoPropertyDescriptor decoratePropertyDescriptor(Class type, Property mappingProperty,
138                            TynamoPropertyDescriptor descriptor, TynamoClassDescriptor parentClassDescriptor)
139            {
140                    if (isFormula(mappingProperty))
141                    {
142                            descriptor.setReadOnly(true);
143                            return descriptor;
144                    }
145                    descriptor.setLength(findColumnLength(mappingProperty));
146                    descriptor.setLarge(isLarge(mappingProperty));
147                    if (!mappingProperty.isOptional())
148                    {
149                            descriptor.setRequired(true);
150                    }
151    
152                    if (!mappingProperty.isInsertable() && !mappingProperty.isUpdateable())
153                    {
154                            descriptor.setReadOnly(true);
155                    }
156    
157                    TynamoPropertyDescriptor descriptorReference = descriptor;
158                    Type hibernateType = mappingProperty.getType();
159                    if (mappingProperty.getType() instanceof ComponentType)
160                    {
161                            descriptorReference = buildEmbeddedDescriptor(type, mappingProperty, descriptor, parentClassDescriptor);
162                    } else if (Collection.class.isAssignableFrom(descriptor.getPropertyType()))
163                    {
164                            descriptorReference = decorateCollectionDescriptor(type, descriptor, parentClassDescriptor);
165                    } else if (hibernateType.isAssociationType())
166                    {
167                            descriptorReference = decorateAssociationDescriptor(type, mappingProperty, descriptor,
168                                            parentClassDescriptor);
169                    } else if (hibernateType.getReturnedClass().isEnum())
170                    {
171                            descriptor.addExtension(EnumReferenceDescriptor.class.getName(), new EnumReferenceDescriptor(hibernateType
172                                            .getReturnedClass()));
173                    }
174    
175                    return descriptorReference;
176            }
177    
178            private EmbeddedDescriptor buildEmbeddedDescriptor(Class type, Property mappingProperty,
179                            TynamoPropertyDescriptor descriptor, TynamoClassDescriptor parentClassDescriptor)
180            {
181                    Component componentMapping = (Component) mappingProperty.getValue();
182                    TynamoClassDescriptor baseDescriptor = descriptorFactory.buildClassDescriptor(descriptor.getPropertyType());
183                    // build from base descriptor
184                    EmbeddedDescriptor embeddedDescriptor = new EmbeddedDescriptor(type, baseDescriptor);
185                    // and copy from property descriptor
186                    embeddedDescriptor.copyFrom(descriptor);
187                    ArrayList<TynamoPropertyDescriptor> decoratedProperties = new ArrayList<TynamoPropertyDescriptor>();
188                    // go thru each property and decorate it with Hibernate info
189                    for (TynamoPropertyDescriptor propertyDescriptor : embeddedDescriptor.getPropertyDescriptors())
190                    {
191                            if (notAHibernateProperty(componentMapping, propertyDescriptor))
192                            {
193                                    decoratedProperties.add(propertyDescriptor);
194                            } else
195                            {
196                                    Property property = componentMapping.getProperty(propertyDescriptor.getName());
197                                    TynamoPropertyDescriptor TynamoPropertyDescriptor = decoratePropertyDescriptor(embeddedDescriptor.getBeanType(),
198                                                    property, propertyDescriptor, parentClassDescriptor);
199                                    decoratedProperties.add(TynamoPropertyDescriptor);
200                            }
201                    }
202                    embeddedDescriptor.setPropertyDescriptors(decoratedProperties);
203                    return embeddedDescriptor;
204            }
205    
206            /**
207             * The default way to order our property descriptors is by the order they
208             * appear in the hibernate config, with id first. Any non-mapped properties
209             * are tacked on at the end, til I think of a better way.
210             * 
211             * @param propertyDescriptors
212             * @return
213             */
214            protected List sortPropertyDescriptors(Class type, List propertyDescriptors)
215            {
216                    ArrayList sortedPropertyDescriptors = new ArrayList();
217    
218                    try
219                    {
220                            sortedPropertyDescriptors.add(Ognl.getValue("#this.{? identifier == true}[0]", propertyDescriptors));
221                            for (Iterator iter = getMapping(type).getPropertyIterator(); iter.hasNext();)
222                            {
223                                    Property mapping = (Property) iter.next();
224                                    sortedPropertyDescriptors.addAll((List) Ognl.getValue("#this.{ ? name == \"" + mapping.getName()
225                                                    + "\"}", propertyDescriptors));
226                            }
227                    } catch (Exception ex)
228                    {
229                            throw new TynamoRuntimeException(ex);
230                    }
231                    return sortedPropertyDescriptors;
232            }
233    
234            /**
235             * Find the Hibernate metadata for this type, traversing up the hierarchy to
236             * supertypes if necessary
237             * 
238             * @param type
239             * @return
240             */
241            protected ClassMetadata findMetadata(Class type) throws MetadataNotFoundException
242            {
243                    ClassMetadata metaData = hibernateSessionSource.getSessionFactory().getClassMetadata(type);
244                    if (metaData != null)
245                    {
246                            return metaData;
247                    }
248                    if (!type.equals(Object.class))
249                    {
250                            return findMetadata(type.getSuperclass());
251                    } else
252                    {
253                            throw new MetadataNotFoundException("Failed to find metadata.");
254                    }
255            }
256    
257            private boolean isFormula(Property mappingProperty)
258            {
259                    for (Iterator iter = mappingProperty.getColumnIterator(); iter.hasNext();)
260                    {
261                            Selectable selectable = (Selectable) iter.next();
262                            if (selectable.isFormula())
263                            {
264                                    return true;
265                            }
266                    }
267                    return false;
268            }
269    
270            /**
271             * Checks to see if a property descriptor is in a component mapping
272             * 
273             * @param componentMapping
274             * @param propertyDescriptor
275             * @return true if the propertyDescriptor property is in componentMapping
276             */
277            protected boolean notAHibernateProperty(Component componentMapping, TynamoPropertyDescriptor propertyDescriptor)
278            {
279                    for (Iterator iter = componentMapping.getPropertyIterator(); iter.hasNext();)
280                    {
281                            Property property = (Property) iter.next();
282                            if (property.getName().equals(propertyDescriptor.getName()))
283                            {
284                                    return false;
285                            }
286                    }
287                    return true;
288            }
289    
290            private boolean isLarge(Property mappingProperty)
291            {
292                    // Hack to avoid setting large property if length
293                    // is exactly equal to Hibernate default column length
294                    return findColumnLength(mappingProperty) != Column.DEFAULT_LENGTH
295                                    && findColumnLength(mappingProperty) > largeColumnLength;
296            }
297    
298            private int findColumnLength(Property mappingProperty)
299            {
300                    int length = 0;
301                    for (Iterator iter = mappingProperty.getColumnIterator(); iter.hasNext();)
302                    {
303                            Column column = (Column) iter.next();
304                            length += column.getLength();
305                    }
306                    return length;
307            }
308    
309            /**
310             * @param classMetaData
311             * @param descriptor
312             * @return
313             */
314            protected boolean notAHibernateProperty(ClassMetadata classMetaData, TynamoPropertyDescriptor descriptor)
315            {
316                    try
317                    {
318                            return (Boolean) Ognl.getValue("propertyNames.{ ? #this == \"" + descriptor.getName() + "\"}.size() == 0",
319                                            classMetaData);
320                    } catch (OgnlException oe)
321                    {
322                            throw new TynamoRuntimeException(oe);
323                    }
324            }
325    
326            /**
327             * @param type
328             * @param descriptor
329             * @param parentClassDescriptor
330             * @return
331             */
332            private IdentifierDescriptor createIdentifierDescriptor(Class type, TynamoPropertyDescriptor descriptor, TynamoClassDescriptor parentClassDescriptor)
333            {
334                    IdentifierDescriptor identifierDescriptor;
335                    PersistentClass mapping = getMapping(type);
336    
337                    /**
338                     * fix for TRAILS-92
339                     */
340                    if (mapping.getProperty(descriptor.getName()).getType() instanceof ComponentType)
341                    {
342                            EmbeddedDescriptor embeddedDescriptor = buildEmbeddedDescriptor(type,
343                                            mapping.getProperty(descriptor.getName()), descriptor, parentClassDescriptor);
344                            embeddedDescriptor.setIdentifier(true);
345                            identifierDescriptor = embeddedDescriptor;
346                    } else
347                    {
348                            identifierDescriptor = new IdentifierDescriptorImpl(type, descriptor);
349                    }
350    
351                    if (((SimpleValue) mapping.getIdentifier()).getIdentifierGeneratorStrategy().equals("assigned"))
352                    {
353                            identifierDescriptor.setGenerated(false);
354                    }
355    
356                    return identifierDescriptor;
357            }
358    
359            /**
360             * @param type
361             * @return
362             */
363            protected PersistentClass getMapping(Class type)
364            {
365                    Configuration cfg = hibernateSessionSource.getConfiguration();
366    
367                    return cfg.getClassMapping(type.getName());
368            }
369    
370            /**
371             * @param type
372             * @param descriptor
373             * @param parentClassDescriptor 
374             */
375            private CollectionDescriptor decorateCollectionDescriptor(Class type, TynamoPropertyDescriptor descriptor,
376                            TynamoClassDescriptor parentClassDescriptor)
377            {
378                    try
379                    {
380                            CollectionDescriptor collectionDescriptor = new CollectionDescriptor(type, descriptor);
381                            org.hibernate.mapping.Collection collectionMapping = findCollectionMapping(type, descriptor.getName());
382                            // It is a child relationship if it has delete-orphan specified in
383                            // the mapping
384                            collectionDescriptor.setChildRelationship(collectionMapping.hasOrphanDelete());
385                            CollectionMetadata collectionMetaData = hibernateSessionSource.getSessionFactory().getCollectionMetadata(
386                                            collectionMapping.getRole());
387    
388                            collectionDescriptor.setElementType(collectionMetaData.getElementType().getReturnedClass());
389    
390                            collectionDescriptor.setOneToMany(collectionMapping.isOneToMany());
391    
392                            decorateOneToManyCollection(parentClassDescriptor, collectionDescriptor, collectionMapping);
393    
394                            return collectionDescriptor;
395    
396                    } catch (HibernateException e)
397                    {
398                            throw new TynamoRuntimeException(e);
399                    }
400            }
401    
402            public TynamoPropertyDescriptor decorateAssociationDescriptor(Class type, Property mappingProperty,
403                            TynamoPropertyDescriptor descriptor, TynamoClassDescriptor parentClassDescriptor)
404            {
405                    Type hibernateType = mappingProperty.getType();
406                    Class parentClassType = parentClassDescriptor.getType();
407                    ObjectReferenceDescriptor descriptorReference = new ObjectReferenceDescriptor(type, descriptor, hibernateType
408                                    .getReturnedClass());
409    
410                    try
411                    {
412                            Field propertyField = parentClassType.getDeclaredField(descriptor.getName());
413                            PropertyDescriptor beanPropDescriptor = (PropertyDescriptor) Ognl.getValue(
414                                            "propertyDescriptors.{? name == '" + descriptor.getName() + "'}[0]", Introspector
415                                                            .getBeanInfo(parentClassType));
416                            Method readMethod = beanPropDescriptor.getReadMethod();
417    
418                            // Start by checking for and retrieving mappedBy attribute inside
419                            // the annotation
420                            String inverseProperty = "";
421                            if (readMethod.isAnnotationPresent(javax.persistence.OneToOne.class))
422                            {
423                                    inverseProperty = readMethod.getAnnotation(javax.persistence.OneToOne.class).mappedBy();
424                            } else if (propertyField.isAnnotationPresent(javax.persistence.OneToOne.class))
425                            {
426                                    inverseProperty = propertyField.getAnnotation(javax.persistence.OneToOne.class).mappedBy();
427                            } else
428                            {
429                                    // If there is none then just return the
430                                    // ObjectReferenceDescriptor
431                                    return descriptorReference;
432                            }
433    
434                            if ("".equals(inverseProperty))
435                            {
436                                    // http://forums.hibernate.org/viewtopic.php?t=974287&sid=12d018b08dffe07e263652190cfc4e60
437                                    // Caution... this does not support multiple
438                                    // class references across the OneToOne relationship
439                                    Class returnType = readMethod.getReturnType();
440                                    for (int i = 0; i < returnType.getDeclaredMethods().length; i++)
441                                    {
442                                            if (returnType.getDeclaredMethods()[i].getReturnType().equals(propertyField.getDeclaringClass()))
443                                            {
444                                                    Method theProperty = returnType.getDeclaredMethods()[i];
445                                                    /* strips preceding 'get' */
446                                                    inverseProperty = theProperty.getName().substring(3).toLowerCase();
447                                                    break;
448                                            }
449                                    }
450                            }
451    
452                    } catch (SecurityException e)
453                    {
454                            LOG.error(e.getMessage());
455                    } catch (NoSuchFieldException e)
456                    {
457                            LOG.error(e.getMessage());
458                    } catch (OgnlException e)
459                    {
460                            LOG.error(e.getMessage());
461                    } catch (IntrospectionException e)
462                    {
463                            LOG.error(e.getMessage());
464                    }
465                    return descriptorReference;
466            }
467    
468            /**
469             * I couldn't find a way to get the "mappedBy" value from the collection
470             * metadata, so I'm getting it from the OneToMany annotation.
471             */
472            private void decorateOneToManyCollection(TynamoClassDescriptor parentClassDescriptor,
473                            CollectionDescriptor collectionDescriptor, org.hibernate.mapping.Collection collectionMapping)
474            {
475                    Class type = parentClassDescriptor.getType();
476                    if (collectionDescriptor.isOneToMany() && collectionMapping.isInverse())
477                    {
478                            try
479                            {
480    
481                                    Field propertyField = type.getDeclaredField(collectionDescriptor.getName());
482                                    PropertyDescriptor beanPropDescriptor = (PropertyDescriptor) Ognl.getValue(
483                                                    "propertyDescriptors.{? name == '" + collectionDescriptor.getName() + "'}[0]", Introspector
484                                                                    .getBeanInfo(type));
485                                    Method readMethod = beanPropDescriptor.getReadMethod();
486                                    String mappedBy = "";
487                                    if (readMethod.isAnnotationPresent(javax.persistence.OneToMany.class))
488                                    {
489                                            mappedBy = readMethod.getAnnotation(javax.persistence.OneToMany.class).mappedBy();
490                                    } else if (propertyField.isAnnotationPresent(javax.persistence.OneToMany.class))
491                                    {
492                                            mappedBy = propertyField.getAnnotation(javax.persistence.OneToMany.class).mappedBy();
493                                    }
494    
495                                    if (!"".equals(mappedBy))
496                                    {
497                                            collectionDescriptor.setInverseProperty(mappedBy);
498                                    }
499    
500                                    parentClassDescriptor.setHasCyclicRelationships(true);
501    
502                            } catch (SecurityException e)
503                            {
504                                    LOG.error(e.getMessage());
505                            } catch (NoSuchFieldException e)
506                            {
507                                    LOG.error(e.getMessage());
508                            } catch (OgnlException e)
509                            {
510                                    LOG.error(e.getMessage());
511                            } catch (IntrospectionException e)
512                            {
513                                    LOG.error(e.getMessage());
514                            }
515                    }
516            }
517    
518            protected org.hibernate.mapping.Collection findCollectionMapping(Class type, String name)
519            {
520                    String roleName = type.getName() + "." + name;
521                    org.hibernate.mapping.Collection collectionMapping = hibernateSessionSource.getConfiguration()
522                                    .getCollectionMapping(roleName);
523                    if (collectionMapping != null)
524                    {
525                            return collectionMapping;
526                    } else if (!type.equals(Object.class))
527                    {
528                            return findCollectionMapping(type.getSuperclass(), name);
529                    } else
530                    {
531                            throw new MetadataNotFoundException("Metadata not found.");
532                    }
533    
534            }
535    
536            /*
537             * (non-Javadoc)
538             * 
539             * @see org.tynamo.descriptor.PropertyDescriptorService#getIdentifierProperty(java.lang.Class)
540             */
541            public String getIdentifierProperty(Class type)
542            {
543                    try
544                    {
545                            return hibernateSessionSource.getSessionFactory().getClassMetadata(type).getIdentifierPropertyName();
546                    } catch (HibernateException e)
547                    {
548                            throw new TynamoRuntimeException(e);
549                    }
550            }
551    }