/* * gvNIX is an open source tool for rapid application development (RAD). * Copyright (C) 2010 Generalitat Valenciana * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see <http://www.gnu.org/licenses/>. */ package org.gvnix.addon.jpa.addon.entitylistener; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Logger; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.apache.felix.scr.annotations.Component; import org.apache.felix.scr.annotations.Reference; import org.apache.felix.scr.annotations.Service; import org.springframework.roo.classpath.TypeLocationService; import org.springframework.roo.classpath.operations.AbstractOperations; import org.springframework.roo.model.JavaType; import org.springframework.roo.process.manager.MutableFile; import org.springframework.roo.project.LogicalPath; import org.springframework.roo.project.Path; import org.springframework.roo.project.PathResolver; import org.springframework.roo.project.ProjectOperations; import org.springframework.roo.support.logging.HandlerUtils; import org.springframework.roo.support.util.XmlUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; /** * Default implementation of {@link JpaOrmEntityListenerOperations} * * @author <a href="http://www.disid.com">DISID Corporation S.L.</a> made for <a * href="http://www.dgti.gva.es">General Directorate for Information * Technologies (DGTI)</a> */ @Component @Service public class JpaOrmEntityListenerOperationsImpl extends AbstractOperations implements JpaOrmEntityListenerOperations { private static final String ORM_XML_TEMPLATE_LOCATION = "orm.xml"; private static final String ORM_XML_LOCATION = "META-INF/orm.xml"; private static final String ENTITY_LISTENERS_TAG = "entity-listeners"; private static final String ENTITY_LISTENER_TAG = "entity-listener"; private static final String CLASS_ATTRIBUTE = "class"; private static final Logger LOGGER = HandlerUtils .getLogger(JpaOrmEntityListenerOperationsImpl.class); @Reference private ProjectOperations projectOperations; @Reference private TypeLocationService typeLocationService; @Reference private JpaOrmEntityListenerRegistry registry; private final Set<JavaType> entWListenRegs = new HashSet<JavaType>(); /** * {@inheritDoc} */ @Override public void addEntityListener(JpaOrmEntityListener definition, String sourceMetadataProvider) { // Load xml file Pair<MutableFile, Document> loadFileResult = loadOrmFile(true); MutableFile ormXmlMutableFile = loadFileResult.getLeft(); Document ormXml = loadFileResult.getRight(); Element root = ormXml.getDocumentElement(); // xml modification flag boolean modified = false; JavaType entityClass = definition.getEntityClass(); // get entity element for entity class Pair<Element, Boolean> entityElementResult = getOrCreateEntityElement( ormXml, root, entityClass); Element entityElement = entityElementResult.getLeft(); modified = modified || entityElementResult.getRight(); // Get entity-listener element Pair<Element, Boolean> entityListenersElementResult = getOrCreateEntityListenersElement( ormXml, entityElement); Element entityListenerElement = entityListenersElementResult.getLeft(); modified = modified || entityListenersElementResult.getRight(); // find all listener for this entity List<Element> entityListenerElements = XmlUtils.findElements( ENTITY_LISTENER_TAG, entityListenerElement); // Remove listener which classes can't be found on project if (cleanUpMissingListeners(entityClass, entityListenerElement, entityListenerElements)) { modified = true; // get entityListenerElements again entityListenerElements = XmlUtils.findElements( "/".concat(ENTITY_LISTENER_TAG), entityListenerElement); } JavaType listenerClass = definition.getListenerClass(); // Find listener-element index int currentIndex = indexOfListener(entityListenerElements, listenerClass); if (currentIndex < 0) { // Not found: create element getOrCreateListenerElement(ormXml, entityListenerElement, listenerClass, sourceMetadataProvider); modified = true; } // If there is more than one listeners if (entityElement.getElementsByTagName(ENTITY_LISTENER_TAG).getLength() > 1) { entWListenRegs.add(entityClass); // check listeners order modified = adjustEntityListenerOrder(ormXml, entityListenerElement, entityListenerElements) || modified; } if (modified) { entWListenRegs.add(entityClass); // If there is any changes on orm.xml save it XmlUtils.writeXml(ormXmlMutableFile.getOutputStream(), ormXml); } } /** * Adjust listener order as is registered on * {@link JpaOrmEntityListenerRegistry} * * @param ormXml * @param entityListenerElement * @param entityListenerElements * @return true if xml elements had been changed */ private boolean adjustEntityListenerOrder(Document ormXml, Element entityListenerElement, List<Element> entityListenerElements) { // Prepare a Pair list which is a representation of current // entity-listener List<Pair<String, String>> currentOrder = new ArrayList<Pair<String, String>>(); for (Element currentElement : entityListenerElements) { // Each Pair: key (left) = entity-listener class; value (right) // metadataProvider id currentOrder.add(ImmutablePair.of( currentElement.getAttribute(CLASS_ATTRIBUTE), getEntityListenerElementType(currentElement))); } // Create a comparator which can sort the list based on order configured // on registry ListenerOrderComparator comparator = new ListenerOrderComparator( registry.getListenerOrder()); // Clone the Pair list and sort it List<Pair<String, String>> ordered = new ArrayList<Pair<String, String>>( currentOrder); Collections.sort(ordered, comparator); // Check if elements order is different form original boolean changeOrder = false; Pair<String, String> currentPair, orderedPair; for (int i = 0; i < currentOrder.size(); i++) { currentPair = currentOrder.get(i); orderedPair = ordered.get(i); if (!StringUtils.equals(currentPair.getKey(), orderedPair.getKey())) { changeOrder = true; break; } } if (!changeOrder) { // Order is correct: nothing to do return false; } // List for new elements to add List<Node> newList = new ArrayList<Node>(entityListenerElements.size()); // Iterate over final ordered list int curIndex; Node cloned, old; for (int i = 0; i < ordered.size(); i++) { orderedPair = ordered.get(i); // Gets old listener XML node curIndex = indexOfListener(entityListenerElements, orderedPair.getKey()); old = entityListenerElements.get(curIndex); // Clone old node and add to new elements list cloned = old.cloneNode(true); newList.add(cloned); // Remove old listener node from parent entityListenerElement.removeChild(old); } // Add listeners xml nodes to parent again in final order for (Node node : newList) { entityListenerElement.appendChild(node); } return true; } /** * Iterate over entityListenerElements to check if referred class exists on * project. Remove elements which missing class. * * @param entityListenerElement * @param entityListenerElements * @return */ private boolean cleanUpMissingListeners(JavaType entity, Element entityListenerElement, List<Element> entityListenerElements) { boolean changed = false; String classOfListener; for (Element current : entityListenerElements) { classOfListener = current.getAttribute(CLASS_ATTRIBUTE); if (typeLocationService.getPhysicalTypeIdentifier(new JavaType( classOfListener)) == null) { // class not found entityListenerElement.removeChild(current); LOGGER.info(String .format("Removing missing entity-listenen '%s' of entity '%s' from orm.xml", classOfListener, entity)); changed = true; } } return changed; } /** * Gets index of the element which attribute <code>class</code> is * <code>listenerClass</code> * * @param entityListenersElements * @param listenerClass * @return */ private int indexOfListener(List<Element> entityListenersElements, JavaType listenerClass) { return indexOfListener(entityListenersElements, listenerClass.getFullyQualifiedTypeName()); } /** * Gets index of the element which attribute <code>class</code> is * <code>className</code> * * @param entityListenersElements * @param className * @return */ private int indexOfListener(List<Element> entityListenersElements, String className) { Element current; for (int i = 0; i < entityListenersElements.size(); i++) { current = entityListenersElements.get(i); if (className.equals(current.getAttribute(CLASS_ATTRIBUTE))) { return i; } } // Not found return -1; } /** * @param current * @return */ private String getEntityListenerElementType(Element current) { Element description = XmlUtils .findFirstElement("/description", current); if (description == null) { return null; } return description.getTextContent(); } /** * Gets the <code>entity-listener</code> xml element for * <code>listenerClass</code>. Creates it if not found. * * @param document XML document instance * @param entityListenerElement parent XML Element * @param listenerClass listener class to search/create * @param sourceMetadataProvider metadaProviderId of listener class * @return Element found/created, true if element has been created */ private Pair<Element, Boolean> getOrCreateListenerElement( Document document, Element entityListenerElement, JavaType listenerClass, String sourceMetadataProvider) { Pair<Element, Boolean> result = getOrCreateElement( document, entityListenerElement, ENTITY_LISTENER_TAG, ImmutablePair.of(CLASS_ATTRIBUTE, listenerClass.getFullyQualifiedTypeName())); // If has not been changed if (!result.getRight()) { return result; } // Add source MetadataProviderId on description child tag Element element = result.getLeft(); Element description = document.createElement("description"); description.setTextContent(sourceMetadataProvider); element.appendChild(description); return ImmutablePair.of(element, true); } /** * Gets or create <code>entity-listeners</code> xml element and add it as * child of <code>entity</code> xml element * * @param document XML document * @param entityElement entity element * @return the new xml element */ private Pair<Element, Boolean> getOrCreateEntityListenersElement( Document document, Element entityElement) { return getOrCreateElement(document, entityElement, ENTITY_LISTENERS_TAG, null); } /** * Gets or creates a xml element (with tag-name <code>elementName</code>) on * <code>parent</code>. * <p/> * If <code>attributeValue</code> is provided will be used to search and * applied to the creation. * * @param document xml document instance * @param parent node to add the new xml element * @param elementName new xml tag name * @param attributeValue (optional) attribute name + attribute value * @return Element found; true if element is new */ private Pair<Element, Boolean> getOrCreateElement(Document document, Element parent, String elementName, Pair<String, String> attributeValue) { boolean changed = false; // prepare xpath expression to search for element StringBuilder sbXpath = new StringBuilder(); sbXpath.append(elementName); if (attributeValue != null) { sbXpath.append("[@"); sbXpath.append(attributeValue.getKey()); sbXpath.append("='"); sbXpath.append(attributeValue.getValue()); sbXpath.append("']"); } String xpath = sbXpath.toString(); // Search for element Element targetElement = XmlUtils.findFirstElement(xpath, parent); if (targetElement == null) { // Not found: create it targetElement = document.createElement(elementName); if (attributeValue != null) { targetElement.setAttribute(attributeValue.getKey(), attributeValue.getValue()); } parent.appendChild(targetElement); // search again targetElement = XmlUtils.findFirstElement(xpath, parent); if (targetElement == null) { // something went worng throw new IllegalStateException("Can't create ".concat(xpath) .concat(" element")); } changed = true; } return ImmutablePair.of(targetElement, changed); } /** * Gets or creates the <code>entity</code> xml element * * @param document XML document * @param root element * @param entityClass * @return the new xml element, changes made */ private Pair<Element, Boolean> getOrCreateEntityElement(Document document, Element root, JavaType entityClass) { return getOrCreateElement( document, root, "entity", ImmutablePair.of(CLASS_ATTRIBUTE, entityClass.getFullyQualifiedTypeName())); } /** * Gets orm.xml path on project. * <p/> * if <code>create</code> Creates it from add-on resources if not exists. * * @param create file if not exists * @return orm.xml file path */ private String getOrmXmlPath(boolean create) { PathResolver pathResolver = projectOperations.getPathResolver(); String ormXmlPath = pathResolver.getIdentifier( LogicalPath.getInstance(Path.SRC_MAIN_RESOURCES, ""), ORM_XML_LOCATION); if (!fileManager.exists(ormXmlPath)) { if (!create) { return null; } InputStream ins = null; try { ins = getClass().getResourceAsStream(ORM_XML_TEMPLATE_LOCATION); String contents = IOUtils.toString(ins); fileManager.createOrUpdateTextFileIfRequired(ormXmlPath, contents, false); fileManager.commit(); } catch (Exception e) { throw new IllegalStateException("Can't create orm.xml", e); } finally { IOUtils.closeQuietly(ins); } } return ormXmlPath; } /** * Comparator to allow adjust order of entityListeners from the order * registered on {@link JpaOrmEntityListenerRegistry} * <p/> * This compares instances of {@link Pair} which left (or key) element is * listener class and right (or value) is the metadataProviderId. * * @author <a href="http://www.disid.com">DISID Corporation S.L.</a> made * for <a href="http://www.dgti.gva.es">General Directorate for * Information Technologies (DGTI)</a> */ private class ListenerOrderComparator implements Comparator<Pair<String, String>> { private List<String> dependencyOrder; public ListenerOrderComparator(List<String> dependencyOrder) { this.dependencyOrder = dependencyOrder; } @Override public int compare(Pair<String, String> arg0, Pair<String, String> arg1) { int dependencyIndex0 = dependencyOrder.indexOf(arg0.getValue()); int dependencyIndex1 = dependencyOrder.indexOf(arg1.getValue()); if (dependencyIndex0 < dependencyIndex1) { return -1; } else if (dependencyIndex0 > dependencyIndex1) { return 1; } return arg0.getKey().compareTo(arg1.getKey()); } } @Override public void cleanUpEntityListeners(JavaType entity) { // Load xml file Pair<MutableFile, Document> loadFileResult = loadOrmFile(false); if (loadFileResult == null) { // orm.xml not exists: nothing to do return; } MutableFile ormXmlMutableFile = loadFileResult.getLeft(); Document ormXml = loadFileResult.getRight(); Element root = ormXml.getDocumentElement(); // xml modification flag boolean modified = false; // get entity element for entity class Pair<Element, Boolean> entityElementResult = getOrCreateEntityElement( ormXml, root, entity); Element entityElement = entityElementResult.getLeft(); modified = modified || entityElementResult.getRight(); if (modified) { // entity element do not exists on orm.xml: nothing to clean up entWListenRegs.remove(entity); return; } // Get entity-listener element Pair<Element, Boolean> entityListenersElementResult = getOrCreateEntityListenersElement( ormXml, entityElement); Element entityListenerElement = entityListenersElementResult.getLeft(); modified = modified || entityListenersElementResult.getRight(); if (modified) { // entity-listeners element do not exists on orm.xml: nothing to // clean up entWListenRegs.remove(entity); return; } // find all listener for this entity List<Element> entityListenerElements = XmlUtils.findElements( ENTITY_LISTENER_TAG, entityListenerElement); if (entityListenerElements == null || entityListenerElements.isEmpty()) { // no entity-listener element found on orm.xml: nothing to clean up entWListenRegs.remove(entity); return; } // Remove listener which classes can't be found on project if (cleanUpMissingListeners(entity, entityListenerElement, entityListenerElements)) { modified = true; } if (!modified) { // is all right: to do return; } if (modified) { // Update cache of entities with listeners: if (XmlUtils.findElements(ENTITY_LISTENER_TAG, entityListenerElement).isEmpty()) { entWListenRegs.remove(entity); } else { entWListenRegs.add(entity); } // If there is any changes on orm.xml save it XmlUtils.writeXml(ormXmlMutableFile.getOutputStream(), ormXml); } } /** * Load the orm.xml file * * @param create file if not exists * * @return {@link MutableFile} and {@link Document} */ private Pair<MutableFile, Document> loadOrmFile(boolean create) { // Get the orm.xml path (and create it if not exists) String ormXmlPath = getOrmXmlPath(create); if (ormXmlPath == null) { return null; } // Load xml file MutableFile ormXmlMutableFile = null; Document ormXml; try { // Get orm.xml content and parse it ormXmlMutableFile = fileManager.updateFile(ormXmlPath); ormXml = XmlUtils.getDocumentBuilder().parse( ormXmlMutableFile.getInputStream()); } catch (Exception e) { LOGGER.severe("Error loading file '".concat(ormXmlPath).concat("'")); throw new IllegalStateException("Error loading file '".concat( ormXmlPath).concat("'"), e); } return new ImmutablePair<MutableFile, Document>(ormXmlMutableFile, ormXml); } /** * {@inheritDoc} */ @Override public boolean hasAnyListener(JavaType entity) { return entWListenRegs.contains(entity); } }