Java tutorial
/* * Version: 1.0 * * The contents of this file are subject to the OpenVPMS License Version * 1.0 (the 'License'); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.openvpms.org/license/ * * Software distributed under the License is distributed on an 'AS IS' basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * Copyright 2019 (C) OpenVPMS Ltd. All Rights Reserved. */ package org.openvpms.component.business.dao.hibernate.im.lookup; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.query.NativeQuery; import org.hibernate.query.Query; import org.openvpms.component.business.dao.hibernate.im.act.ActDOImpl; import org.openvpms.component.business.dao.hibernate.im.act.ActRelationshipDOImpl; import org.openvpms.component.business.dao.hibernate.im.act.ParticipationDOImpl; import org.openvpms.component.business.dao.hibernate.im.document.DocumentDOImpl; import org.openvpms.component.business.dao.hibernate.im.entity.EntityDOImpl; import org.openvpms.component.business.dao.hibernate.im.entity.EntityIdentityDOImpl; import org.openvpms.component.business.dao.hibernate.im.entity.EntityLinkDOImpl; import org.openvpms.component.business.dao.hibernate.im.entity.EntityRelationshipDOImpl; import org.openvpms.component.business.dao.hibernate.im.party.ContactDOImpl; import org.openvpms.component.business.dao.hibernate.im.party.PartyDOImpl; import org.openvpms.component.business.dao.hibernate.im.product.ProductDOImpl; import org.openvpms.component.business.dao.hibernate.im.product.ProductPriceDOImpl; import org.openvpms.component.business.dao.hibernate.im.security.UserDOImpl; import org.openvpms.component.business.domain.im.act.Act; import org.openvpms.component.business.domain.im.act.ActRelationship; import org.openvpms.component.business.domain.im.archetype.descriptor.ArchetypeDescriptor; import org.openvpms.component.business.domain.im.archetype.descriptor.NodeDescriptor; import org.openvpms.component.business.domain.im.common.Entity; import org.openvpms.component.business.domain.im.common.EntityIdentity; import org.openvpms.component.business.domain.im.common.EntityLink; import org.openvpms.component.business.domain.im.common.EntityRelationship; import org.openvpms.component.business.domain.im.common.Participation; import org.openvpms.component.business.domain.im.document.Document; import org.openvpms.component.business.domain.im.lookup.LookupRelationship; import org.openvpms.component.business.domain.im.party.Contact; import org.openvpms.component.business.domain.im.product.ProductPrice; import org.openvpms.component.business.service.archetype.descriptor.cache.IArchetypeDescriptorCache; import org.openvpms.component.model.lookup.Lookup; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.HashMap; import java.util.Map; /** * Replaces an instance of a lookup with another lookup. * <p/> * This has the following limitations: * <ol> * <li>it uses SQL to perform updates and queries that cannot be implemented in HQL. If persistent classes are added, * they may also need to be referenced by this class.</li> * <li>it performs batch updates which do not update persistent version numbers. While it also clears the second level * cache, other in-memory objects won't reflect the changes, potentially resulting in data-inconsistency.</li> * <li>no validation is performed on updated objects</li> * </ol> * * @author Tim Anderson */ public class LookupReplacer { /** * The archetype descriptors. */ private final IArchetypeDescriptorCache archetypes; /** * The entity_classifications table select statement. */ private static final String entityClassificationsSelect; /** * The entity_classifications table update statement. */ private static final String entityClassificationsUpdate; /** * The entity_classifications table delete statement. */ private static final String entityClassificationsDelete; /** * The contact_classifications table select statement. */ private static final String contactClassificationsSelect; /** * The contact_classifications table update statement. */ private static final String contactClassificationsUpdate; /** * The contact_classifications table delete statement. */ private static final String contactClassificationsDelete; /** * The product_price_classifications table select statement. */ private static final String priceClassificationsSelect; /** * The product_price_classifications table update statement. */ private static final String priceClassificationsUpdate; /** * The product_price_classifications table delete statement. */ private static final String priceClassificationsDelete; /** * The mappings. */ private static final Map<Class, Mapping> mappings = new HashMap<>(); static { addMapping(Act.class, "acts", "act_details", "act_id", ActDOImpl.class); addMapping(ActRelationship.class, "act_relationships", "act_relationship_details", "act_relationship_id", ActRelationshipDOImpl.class); addMapping(Contact.class, "contacts", "contact_details", "contact_id", ContactDOImpl.class); addMapping(Document.class, "documents", "document_details", "document_id", DocumentDOImpl.class); addMapping(Entity.class, "entities", "entity_details", "entity_id", EntityDOImpl.class); addMapping(EntityIdentity.class, "entity_identities", "entity_identity_details", "entity_identity_id", EntityIdentityDOImpl.class); addMapping(EntityRelationship.class, "entity_relationships", "entity_relationship_details", "entity_relationship_id", EntityRelationshipDOImpl.class); addMapping(EntityLink.class, "entity_links", "entity_link_details", "id", EntityLinkDOImpl.class); addMapping(org.openvpms.component.business.domain.im.lookup.Lookup.class, "lookups", "lookup_details", "lookup_id", LookupDOImpl.class); addMapping(LookupRelationship.class, "lookup_relationships", "lookup_relationship_details", "lookup_relationship_id", LookupRelationshipDOImpl.class); addMapping(Participation.class, "participations", "participation_details", "participation_id", ParticipationDOImpl.class); addMapping(ProductPrice.class, "product_prices", "product_price_details", "product_price_id", ProductPriceDOImpl.class); entityClassificationsSelect = createClassificationsSelectSQL("entity_classifications"); entityClassificationsUpdate = createClassificationsUpdateSQL("entity_classifications", "entity_id"); entityClassificationsDelete = createClassificationsDeleteSQL("entity_classifications"); contactClassificationsSelect = createClassificationsSelectSQL("contact_classifications"); contactClassificationsUpdate = createClassificationsUpdateSQL("contact_classifications", "contact_id"); contactClassificationsDelete = createClassificationsDeleteSQL("contact_classifications"); priceClassificationsSelect = createClassificationsSelectSQL("product_price_classifications"); priceClassificationsUpdate = createClassificationsUpdateSQL("product_price_classifications", "product_price_id"); priceClassificationsDelete = createClassificationsDeleteSQL("product_price_classifications"); } /** * Construct a <tt>LookupReplace</tt>. * * @param archetypes the archetype descriptors */ public LookupReplacer(IArchetypeDescriptorCache archetypes) { this.archetypes = archetypes; } /** * Determines if a lookup is being used. * * @param lookup the lookup * @param session the session * @return <tt>true</tt> if the lookup is being used */ public boolean isUsed(Lookup lookup, Session session) { if (isClassificationInUse(lookup, entityClassificationsSelect, session) || isClassificationInUse(lookup, contactClassificationsSelect, session) || isClassificationInUse(lookup, priceClassificationsSelect, session)) { return true; } // find all uses of the lookup where it is referred to by a node descriptor using the lookup's code LookupUsageFinder finder = new LookupUsageFinder(archetypes); Map<NodeDescriptor, ArchetypeDescriptor> refs = finder.getCodeReferences(lookup.getArchetype()); for (Map.Entry<NodeDescriptor, ArchetypeDescriptor> entry : refs.entrySet()) { NodeDescriptor node = entry.getKey(); ArchetypeDescriptor archetype = entry.getValue(); if (isDetailsNode(node)) { if (isUsedSQL(lookup, node, archetype, session)) { return true; } } else { if (isUsedHQL(lookup, node, archetype, session)) { return true; } } } return false; } /** * Replaces instances of the source lookup with the target. * * @param source the lookup to replace * @param target the lookup to replace <tt>source</tt> with * @param session the session */ public void replace(Lookup source, Lookup target, Session session) { if (source.getId() == target.getId()) { throw new IllegalArgumentException("Source and target lookups are identical"); } if (!source.getArchetype().equals(target.getArchetype())) { throw new IllegalArgumentException("Source and target lookups must have the same archetype"); } // find all uses of the lookup where it is referred to by a node descriptor using the lookup's code LookupUsageFinder finder = new LookupUsageFinder(archetypes); Map<NodeDescriptor, ArchetypeDescriptor> refs = finder.getCodeReferences(source.getArchetype()); // now replace them for (Map.Entry<NodeDescriptor, ArchetypeDescriptor> entry : refs.entrySet()) { NodeDescriptor node = entry.getKey(); ArchetypeDescriptor archetype = entry.getValue(); if (isDetailsNode(node)) { replaceCodeSQL(node, archetype, source, target, session); } else { replaceCodeHQL(node, archetype, source, target, session); } } // replace any uses of the lookup as classifications. Don't do it based on the archetypes that use the lookup, // as these could change over time. // NOTE: the list of persirstent classes needs to reflect the persistent class heirarchy if the second level // caches are to be cleared correctly replaceClassifications(source, target, entityClassificationsUpdate, entityClassificationsDelete, session, EntityDOImpl.class, PartyDOImpl.class, ProductDOImpl.class, UserDOImpl.class); replaceClassifications(source, target, contactClassificationsUpdate, contactClassificationsDelete, session, ContactDOImpl.class); replaceClassifications(source, target, priceClassificationsUpdate, priceClassificationsDelete, session, ProductPriceDOImpl.class); } /** * Determines if a lookup is used by a particular archetype 'details' node. * <p/> * This uses SQL rather than HQL as HQL cannot query the details table. * * @param lookup the lookup to check * @param node the node * @param archetype the archetype * @param session the hibernate session * @return <tt>true</tt> if the lookup is used, otherwise <tt>false</tt> */ private boolean isUsedSQL(Lookup lookup, NodeDescriptor node, ArchetypeDescriptor archetype, Session session) { Mapping mapping = getMapping(archetype, node); NativeQuery query = session.createSQLQuery(mapping.getIsUsedSQL()); query.setMaxResults(1); query.setParameter("archetype", archetype.getType().getShortName()); query.setParameter("name", node.getName()); query.setParameter("code", lookup.getCode()); return !query.list().isEmpty(); } /** * Determines if a lookup is used by a particular archetype node. * * @param lookup the lookup to check * @param node the node * @param archetype the archetype * @param session the hibernate session * @return <tt>true</tt> if the lookup is used, otherwise <tt>false</tt> */ private boolean isUsedHQL(Lookup lookup, NodeDescriptor node, ArchetypeDescriptor archetype, Session session) { Mapping mapping = getMapping(archetype, node); String name = node.getPath().substring(1); Query query = session.createQuery("select id from " + mapping.getPersistentClass().getName() + " where archetypeId.shortName = :archetype and " + name + " = :code"); query.setParameter("archetype", archetype.getType().getShortName()); query.setParameter("code", lookup.getCode()); query.setMaxResults(1); return !query.list().isEmpty(); } /** * Determines if a node is a 'details' node. These are mapped to a <em>*_details</em> table by hibernate, * and must be updated using SQL rather than HQL. * * @param node the node to check * @return <tt>true</tt> if the node is a details node */ private boolean isDetailsNode(NodeDescriptor node) { return node.getPath().startsWith("/details/"); } /** * Replaces instances of a lookup where it is referred to by a 'details' node via its code. * <p/> * This must be done using SQL, as HQL doesn't support updates to maps. * * @param node the node descriptor that refers to the lookup's archetype * @param archetype the node's archetype descriptor * @param source the lookup to replace * @param target the lookup to replace <tt>source</tt> with * @param session the Hibernate session */ private void replaceCodeSQL(NodeDescriptor node, ArchetypeDescriptor archetype, Lookup source, Lookup target, Session session) { Mapping mapping = getMapping(archetype, node); NativeQuery query = session.createSQLQuery(mapping.getUpdateSQL()); query.setParameter("archetype", archetype.getType().getShortName()); query.setParameter("name", node.getName()); query.setParameter("oldCode", source.getCode()); query.setParameter("newCode", target.getCode()); executeUpdate(query, session, mapping.getPersistentClass()); } /** * Replaces instances of a lookup where it is referred to by a node via its code. * * @param node the node descriptor that refers to the lookup's archetype * @param archetype the node's archetype descriptor * @param source the lookup to replace * @param target the lookup to replace <tt>source</tt> with * @param session the Hibernate session */ private void replaceCodeHQL(NodeDescriptor node, ArchetypeDescriptor archetype, Lookup source, Lookup target, Session session) { Mapping mapping = getMapping(archetype, node); String name = node.getPath().substring(1); Query query = session.createQuery("update " + mapping.getPersistentClass().getName() + " set " + name + " = :newCode" + " where archetypeId.shortName = :archetype and " + name + " = :oldCode"); query.setParameter("archetype", archetype.getType().getShortName()); query.setParameter("oldCode", source.getCode()); query.setParameter("newCode", target.getCode()); executeUpdate(query, session, mapping.getPersistentClass()); } /** * Determines if a classification lookup is in use. * * @param lookup the lookup * @param sql the SQL query * @param session the hibernate session * @return <tt>true</tt> if the lookup is in use, otherwise <tt>false</tt> */ private boolean isClassificationInUse(Lookup lookup, String sql, Session session) { Query query = session.createSQLQuery(sql); query.setParameter("id", lookup.getId()); query.setMaxResults(1); return !query.list().isEmpty(); } /** * Replaces one lookup classification with another. * <p/> * This requires two SQL statements as HQL doesn't support updates of collection nodes. * <p/> * The first SQL statement, <tt>updateSQL</tt>, replaces all instances of the source with the target, for a * given object, so long as the object hasn't already been classified with the target. This is required to avoid * duplicate keys. * <p/> * The second SQL statement, <tt>deleteSQL</tt>, removes all of those source instances that couldn't be replaced * due to duplicates. * * @param source the lookup classification to replace * @param target the lookup classification to replace <tt>source</tt> with * @param updateSQL the SQL to replace existing instances * @param deleteSQL the SQL to delete the source classification with * @param session the session * @param persistentClasses the persistent classes */ private void replaceClassifications(Lookup source, Lookup target, String updateSQL, String deleteSQL, Session session, Class... persistentClasses) { Query query = session.createSQLQuery(updateSQL); query.setParameter("oldId", source.getId()); query.setParameter("newId", target.getId()); executeUpdate(query, session, persistentClasses); query = session.createSQLQuery(deleteSQL); query.setParameter("oldId", source.getId()); query.executeUpdate(); } /** * Executes an update query. * <p/> * If any updates are made, the second level caches associated with the persisent classes are also cleared. * <p/> * <strong>NOTE</strong>: There is a small window where the second level cache will not reflect the state of the * database. * * @param query the update query * @param session the hibernate session * @param persistentClasses the persistent classes affected by the update */ private void executeUpdate(Query query, final Session session, final Class... persistentClasses) { int updates = query.executeUpdate(); if (updates != 0) { final SessionFactory factory = session.getSessionFactory(); if (TransactionSynchronizationManager.isActualTransactionActive()) { // clear the cache when the transaction commits TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCompletion(int status) { if (status == STATUS_COMMITTED) { clearCaches(persistentClasses, factory); } } }); } else { clearCaches(persistentClasses, factory); } } } /** * Clears the second level caches of the specified persistent classes. * * @param persistentClasses the persistent classes * @param factory the session factory */ private void clearCaches(Class[] persistentClasses, SessionFactory factory) { for (Class persistentClass : persistentClasses) { factory.getCache().evictEntityData(persistentClass); } } /** * Returns a mapping for a given archetype. * * @param archetype the archetype descriptor * @param node the node, for error reporting * @return the corresponding mapping */ private Mapping getMapping(ArchetypeDescriptor archetype, NodeDescriptor node) { Class clazz = archetype.getClazz(); Mapping mapping = mappings.get(clazz); while (mapping == null && !clazz.equals(Object.class)) { clazz = clazz.getSuperclass(); mapping = mappings.get(clazz); } if (mapping == null) { throw new IllegalStateException("Cannot update code node=" + node.getName() + ", archetype=" + archetype.getType() + ". Unsupported class: " + clazz); } return mapping; } /** * Helper to create an SQL update statement that updates the value column of a 'details' table * for a given archetype and value. * * @param table the table name, used to restrict on archetype * @param details the details table name * @param id the join column name * @return an SQL update statement */ private static String createUpdateSQL(String table, String details, String id) { return "update " + table + " t join " + details + " d on t." + id + " = d." + id + " set d.value = :newCode" + " where t.arch_short_name=:archetype and d.name = :name and d.value = :oldCode"; } /** * Helper to create an SQL statement the determines if a lookup is used by a 'details' node. * * @param table the table name, used to restrict on archetype * @param details the details table name * @param id the join column name * @return an SQL query statement */ private static String createIsUsedSQL(String table, String details, String id) { return "select t." + id + " from " + table + " t join " + details + " d on t." + id + " = d." + id + " where t.arch_short_name=:archetype and d.name = :name and d.value = :code"; } /** * Helper to create an SQL query statement that determines if a lookup is used by a classifications node. * * @param table the classification table * @return a new SQL statement */ private static String createClassificationsSelectSQL(String table) { return "select * from " + table + " t where t.lookup_id = :id"; } /** * Helper to create an SQL update statement that replaces a classification lookup with another, providing an * instance of that lookup doesn't already exist. This is required to avoid duplicate keys. * * @param table the classification table * @param id the id column to join on * @return a new SQL update statement */ private static String createClassificationsUpdateSQL(String table, String id) { return "update " + table + " c1 left join " + table + " c2 on c1." + id + " = c2." + id + " and c2.lookup_id = :newId " + "set c1.lookup_id = :newId " + "where c1.lookup_id = :oldId and c2.lookup_id is null"; } /** * Helper to create an SQL delete statement that removes a classification lookup. * * @param table the classification table * @return a new SQL delete statement */ private static String createClassificationsDeleteSQL(String table) { return "delete from " + table + " where lookup_id = :oldId"; } /** * Helper to register a mapping. * * @param clazz the archetype class * @param table the primary table that makes it persistent * @param detailsTable the details table * @param joinColumn the column to join the table and details table on * @param impl the persistent class */ private static void addMapping(Class clazz, String table, String detailsTable, String joinColumn, Class impl) { mappings.put(clazz, new Mapping(table, detailsTable, joinColumn, impl)); } /** * Mapping information. */ private static class Mapping { /** * SQL update statement that updates the value column of a 'details' table for a given archetype and value */ private final String updateSQL; /** * SQL statement the determines if a lookup is used by a 'details' node. */ private final String isUsedSQL; /** * The persistent class. */ private final Class persistentClass; /** * Constructs a <tt>Mapping</tt>. * * @param table the primary table * @param details the details table * @param joinColumn the column to join the two tables on * @param persistentClass the persistent clas */ public Mapping(String table, String details, String joinColumn, Class persistentClass) { this.updateSQL = createUpdateSQL(table, details, joinColumn); this.isUsedSQL = createIsUsedSQL(table, details, joinColumn); this.persistentClass = persistentClass; } /** * Returns an SQL update statement that updates the value column of a 'details' table for a given archetype and * value. * * @return the update SQL */ public String getUpdateSQL() { return updateSQL; } /** * Returns statement the determines if a lookup is used by a 'details' node. * * @return the is-used SQL */ public String getIsUsedSQL() { return isUsedSQL; } /** * Returns the persistent class. * * @return the persistent class */ public Class getPersistentClass() { return persistentClass; } } }