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}