001package org.tynamo.seedentity.jpa.services;
002
003import java.lang.annotation.Annotation;
004import java.lang.reflect.Field;
005import java.lang.reflect.Member;
006import java.lang.reflect.Method;
007import java.util.ArrayList;
008import java.util.HashMap;
009import java.util.HashSet;
010import java.util.List;
011import java.util.Map;
012import java.util.Set;
013
014import javax.persistence.Column;
015import javax.persistence.Entity;
016import javax.persistence.EntityManager;
017import javax.persistence.EntityTransaction;
018import javax.persistence.JoinColumn;
019import javax.persistence.Table;
020import javax.persistence.UniqueConstraint;
021import javax.persistence.criteria.CriteriaBuilder;
022import javax.persistence.criteria.CriteriaQuery;
023import javax.persistence.criteria.Predicate;
024import javax.persistence.criteria.Root;
025import javax.persistence.metamodel.Attribute;
026import javax.persistence.metamodel.EntityType;
027import javax.persistence.metamodel.Metamodel;
028import javax.persistence.metamodel.SingularAttribute;
029import javax.persistence.metamodel.Type;
030
031import org.apache.tapestry5.ioc.annotations.EagerLoad;
032import org.apache.tapestry5.ioc.annotations.Inject;
033import org.apache.tapestry5.ioc.annotations.Symbol;
034import org.apache.tapestry5.ioc.services.PropertyAccess;
035import org.apache.tapestry5.jpa.EntityManagerManager;
036import org.slf4j.Logger;
037import org.tynamo.seedentity.SeedEntityIdentifier;
038import org.tynamo.seedentity.SeedEntityUpdater;
039
040@EagerLoad
041public class SeedEntityImpl implements SeedEntity {
042        @SuppressWarnings("unchecked")
043        private Map<Class, SeedEntityIdentifier> typeIdentifiers = new HashMap<Class, SeedEntityIdentifier>();
044        private Logger logger;
045        // track newly added entities so you know to update only those ones and otherwise ignore by default
046        private List<Object> newlyAddedEntities = new ArrayList<Object>();
047        private PropertyAccess propertyAccess;
048
049        public SeedEntityImpl(Logger logger, PropertyAccess propertyAccess, EntityManagerManager entityManagerManager,
050                @Inject @Symbol(SeedEntity.PERSISTENCEUNIT) String persistenceUnitName, List<Object> entities) {
051                // Create a new session for this rather than participate in the existing session (through SessionManager)
052                // since we need to manage transactions ourselves
053                this.logger = logger;
054                this.propertyAccess = propertyAccess;
055
056                EntityManager entityManager = null;
057                if (persistenceUnitName.isEmpty()) {
058                        if (entityManagerManager.getEntityManagers().size() != 1)
059                                throw new IllegalArgumentException(
060                                        "You have to specify the persistenceunit for seedentity if multiple persistence units are configured in the system. Contribute a value for SeedEntity.PERSISTENCEUNIT");
061                        entityManager = entityManagerManager.getEntityManagers().values().iterator().next();
062                } else {
063                        entityManager = entityManagerManager.getEntityManager(persistenceUnitName);
064                        if (entityManager == null)
065                                throw new IllegalArgumentException(
066                                        "Persistence unit '"
067                                                + persistenceUnitName
068                                                + "' is configured for seedentity, but it was not found. Check that the contributed name matches with persistenceunit configuration");
069                }
070
071                // Session session = sessionSource.create();
072                seed(entityManager, entities);
073                // session.close();
074        }
075
076        @SuppressWarnings("unchecked")
077        void seed(EntityManager entityManager, List<Object> entities) {
078                Metamodel metamodel = entityManager.getMetamodel();
079                EntityTransaction tx = entityManager.getTransaction();
080                tx.begin();
081                for (Object object : entities) {
082                        Object entity;
083                        if (object instanceof String) {
084                                try {
085                                        entityManager.createNativeQuery(object.toString()).executeUpdate();
086                                        tx.commit();
087                                        tx.begin();
088                                } catch (Exception e) {
089                                        logger.info("Couldn't execute native seed query '" + object
090                                                + "', perhaps already executed? Rolling back all statements up to this point. Query failed with: "
091                                                + e.getMessage());
092                                        tx.rollback();
093                                        tx.begin();
094                                }
095                                continue;
096                        }
097                        if (object instanceof SeedEntityUpdater) {
098                                SeedEntityUpdater entityUpdater = (SeedEntityUpdater) object;
099                                if (!newlyAddedEntities.contains(entityUpdater.getOriginalEntity())) {
100                                        if (!entityUpdater.isForceUpdate()) {
101                                                logger.info("Entity '" + entityUpdater.getUpdatedEntity() + "' of type "
102                                                        + entityUpdater.getUpdatedEntity().getClass().getSimpleName() + " was not newly added, ignoring update");
103                                                continue;
104                                        }
105                                }
106                                if (!entityUpdater.getOriginalEntity().getClass().equals(entityUpdater.getUpdatedEntity().getClass()))
107                                        throw new ClassCastException("The type of original entity doesn't match with the updated entity");
108
109                                EntityType entityType = metamodel.entity(entityUpdater.getOriginalEntity().getClass());
110                                Type idType = entityType.getIdType();
111                                SingularAttribute idAttr = entityType.getId(idType.getJavaType());
112
113                                Object identifier = propertyAccess.get(entityUpdater.getOriginalEntity(), idAttr.getName());
114                                if (identifier == null)
115                                        throw new IllegalStateException("Cannot make an update to the entity '" + entityUpdater.getUpdatedEntity()
116                                                + " of type " + entityUpdater.getUpdatedEntity().getClass().getSimpleName()
117                                                + " because the identifier of the original entity is not set");
118                                propertyAccess.set(entityUpdater.getUpdatedEntity(), idAttr.getName(), identifier);
119                                entityManager.merge(entityUpdater.getUpdatedEntity());
120                                continue;
121                        }
122
123                        String uniquelyIdentifyingProperty = null;
124                        if (object instanceof SeedEntityIdentifier) {
125                                // SeedEntityIdentifier interface can be used for setting identifier for specific entity only
126                                // or for all enties of the same type
127                                SeedEntityIdentifier entityIdentifier = (SeedEntityIdentifier) object;
128                                if (entityIdentifier.getEntity() instanceof Class) {
129                                        typeIdentifiers.put((Class) entityIdentifier.getEntity(), entityIdentifier);
130                                        continue;
131                                } else {
132                                        uniquelyIdentifyingProperty = entityIdentifier.getUniquelyIdentifyingProperty();
133                                        entity = entityIdentifier.getEntity();
134                                }
135                        } else entity = object;
136
137                        if (entity.getClass().getAnnotation(Entity.class) == null) {
138                                logger.warn("Contributed object '" + entity + "' is not an entity, cannot be used a seed");
139                                continue;
140                        }
141
142                        if (uniquelyIdentifyingProperty == null && typeIdentifiers.containsKey(object.getClass()))
143                                uniquelyIdentifyingProperty = typeIdentifiers.get(object.getClass()).getUniquelyIdentifyingProperty();
144
145                        // create a query using unique properties
146                        // Note that we ignore the identifier - so seed entities with manually set ids will be re-seeded
147                        EntityType entityType = metamodel.entity(entity.getClass());
148                        Set<SingularAttribute> singularAttributes = entityType.getSingularAttributes();
149
150                        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
151                        CriteriaQuery<?> query = cb.createQuery(entity.getClass());
152                        Root<?> root = query.from(entityType);
153                        SingularAttribute idAttr = null;
154
155                        Set<Predicate> predicates = new HashSet<Predicate>();
156                        for (SingularAttribute a : singularAttributes) {
157                                if (a.isId()) {
158                                        // FIXME doesn't deal with composite ids
159                                        idAttr = a;
160                                        continue;
161                                }
162                                if (uniquelyIdentifyingProperty == null && isUnique(entityManager, entity.getClass(), a))
163                                        predicates.add(cb.equal(root.get(a.getName()), propertyAccess.get(entity, a.getName())));
164                        }
165                        if (uniquelyIdentifyingProperty != null)
166                                predicates.add(cb.equal(root.get(uniquelyIdentifyingProperty),
167                                        propertyAccess.get(entity, uniquelyIdentifyingProperty)));
168
169                        // always re-seed if there are no unique attributes
170                        if (predicates.size() > 0) {
171                                query.where(cb.and(predicates.toArray(new Predicate[0])));
172
173                                List results = entityManager.createQuery(query).getResultList();
174
175                                if (results.size() > 0) {
176                                        logger.info("At least one existing entity with the same unique properties as '" + entity + "' of type '"
177                                                + entity.getClass().getSimpleName() + "' already exists, skipping seeding this entity");
178                                        // Need to set the id to the seed bean so a new seed entity with a relationship to existing seed entity can be
179                                        // saved.
180                                        // Results should include only one object and we don't know any better which is the right object anyway
181                                        // so use the first one
182                                        Object existingObject = results.get(0);
183                                        // Always evict though it's only needed if existing objects are updated
184                                        entityManager.detach(existingObject);
185                                        propertyAccess.set(entity, idAttr.getName(), propertyAccess.get(existingObject, idAttr.getName()));
186                                        continue;
187                                }
188                        }
189                        entityManager.persist(entity);
190                        newlyAddedEntities.add(entity);
191                }
192                tx.commit();
193                newlyAddedEntities.clear();
194        }
195
196        private boolean isUnique(EntityManager entityManager, Class entityType, SingularAttribute attribute) {
197                if (entityType.isAnnotationPresent(Table.class)) {
198                        Table annotation = (Table) entityType.getAnnotation(Table.class);
199                        if (annotation.uniqueConstraints() != null) {
200                                for (UniqueConstraint uniqueConstraint : annotation.uniqueConstraints())
201                                        for (String uniqueColumn : uniqueConstraint.columnNames()) {
202                                                String columnName = attribute.getName();
203                                                // check customised name in @Column
204                                                Column columnAnnotation = (Column) getAnnotation(attribute.getJavaMember(), Column.class);
205                                                if (columnAnnotation != null && columnAnnotation.name() != null) columnName = columnAnnotation.name();
206
207                                                // check @ManyToOne
208                                                if (attribute.getPersistentAttributeType().equals(Attribute.PersistentAttributeType.MANY_TO_ONE)) {
209                                                        JoinColumn joinColumnAnnotation = (JoinColumn) getAnnotation(attribute.getJavaMember(), JoinColumn.class);
210                                                        if (joinColumnAnnotation != null && joinColumnAnnotation.name() != null) {
211                                                                columnName = joinColumnAnnotation.name();
212                                                        } else {
213                                                                // lookup the referenced @ManyToOne entity and find it's primary key to create the default generated FK
214                                                                // column name
215                                                                EntityType<?> referencedEntity = entityManager.getMetamodel().entity(attribute.getJavaType());
216                                                                for (SingularAttribute<?, ?> singularAttr : referencedEntity.getSingularAttributes()) {
217                                                                        if (!singularAttr.isId()) continue;
218                                                                        Column columnAnn = (Column) getAnnotation(singularAttr.getJavaMember(), Column.class);
219                                                                        // according to JPA2 specification
220                                                                        columnName = String.format("%s_%s", attribute.getName(), columnAnn == null ? singularAttr.getName()
221                                                                                : columnAnn.name());
222                                                                }
223                                                        }
224                                                }
225                                                if (columnName.equalsIgnoreCase(uniqueColumn)) return true;
226                                        }
227                        }
228                }
229
230                Column annotation = (Column) getAnnotation(attribute.getJavaMember(), Column.class);
231                if (annotation != null && annotation.unique()) return true;
232                return false;
233        }
234
235        private Annotation getAnnotation(Member member, Class annotationType) {
236                return member instanceof Field ? ((Field) member).getAnnotation(annotationType)
237                        : member instanceof Method ? ((Method) member).getAnnotation(annotationType) : null;
238        }
239
240        // Metamodel metamodel = entityManager.getMetamodel();
241        // EntityType entityType = metamodel.entity(entity.getClass());
242        // Set<SingularAttribute> singularAttributes = entityType.getSingularAttributes();
243        //
244        // CriteriaBuilder cb = entityManager.getCriteriaBuilder();
245        // CriteriaQuery<?> query = cb.createQuery(entity.getClass());
246        // Root<?> root = query.from(entityType);
247        //
248        // // FIXME this is wrong - we should only add the singular attributes that are marked as unique
249        // // see how Hibernate seedentity does this
250        // // and absolutely filter out id attribute
251        //
252        // // TODO see http://stackoverflow.com/questions/7077464/how-to-get-singularattribute-mapped-value-of-a-persistent-object
253        // // how to use the metamodel api to do this without beanutil
254        // for (SingularAttribute a : singularAttributes) {
255        // query.where(cb.equal(root.get(a), propertyAccess.get(object, a.getName())));
256        // }
257        // List results = entityManager.createQuery(query).getResultList();
258        //
259        // if (results.size() > 0) {
260        // logger.info("At least one existing entity with same unique properties as '" + entity + "' of type '"
261        // + entity.getClass().getSimpleName() + "' already exists, skipping seeding this entity");
262        // // Need to set the id to the seed bean so a new seed entity with a relationship to existing seed entity can be
263        // // saved.
264        //
265        // // Results should include only one object and we don't know any better which is the right object anyway
266        // // so use the first one
267        //
268        // Type idType = entityType.getIdType();
269        // SingularAttribute idAttr = entityType.getId(idType.getJavaType());
270        // propertyAccess.set(entity, idAttr.getName(), propertyAccess.get(results.get(0), idAttr.getName()));
271        //
272        // continue;
273        // }
274        // entityManager.persist(entity);
275        // // FIXME need to flush for latter persist() to "see" the previous calls
276        // //em.flush();
277        // }
278        // tx.commit();
279        // }
280}