/* * #%L * BroadleafCommerce Common Libraries * %% * Copyright (C) 2009 - 2013 Broadleaf Commerce * %% * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package org.broadleafcommerce.common.extensibility.jpa; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.broadleafcommerce.common.config.RuntimeEnvironmentPropertiesConfigurer; import org.broadleafcommerce.common.exception.ExceptionHelper; import org.broadleafcommerce.common.extensibility.jpa.convert.BroadleafClassTransformer; import org.broadleafcommerce.common.extensibility.jpa.convert.EntityMarkerClassTransformer; import org.broadleafcommerce.common.extensibility.jpa.copy.NullClassTransformer; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.jmx.export.MBeanExporter; import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.management.ObjectName; import javax.persistence.spi.PersistenceUnitInfo; import javax.sql.DataSource; /** * Merges jars, class names and mapping file names from several persistence.xml files. The * MergePersistenceUnitManager will continue to keep track of individual persistence unit * names (including individual data sources). When a specific PersistenceUnitInfo is requested * by unit name, the appropriate PersistenceUnitInfo is returned with modified jar files * urls, class names and mapping file names that include the comprehensive collection of these * values from all persistence.xml files. * * * @author jfischer, jjacobs */ public class MergePersistenceUnitManager extends DefaultPersistenceUnitManager { private static final Log LOG = LogFactory.getLog(MergePersistenceUnitManager.class); protected HashMap<String, PersistenceUnitInfo> mergedPus = new HashMap<String, PersistenceUnitInfo>(); protected List<BroadleafClassTransformer> classTransformers = new ArrayList<BroadleafClassTransformer>(); @Resource(name="blMergedPersistenceXmlLocations") protected Set<String> mergedPersistenceXmlLocations; @Resource(name="blMergedDataSources") protected Map<String, DataSource> mergedDataSources; @Resource(name="blMergedClassTransformers") protected Set<BroadleafClassTransformer> mergedClassTransformers; @Resource(name="blEntityMarkerClassTransformer") protected EntityMarkerClassTransformer entityMarkerClassTransformer; @Resource(name="blAutoDDLStatusExporter") protected MBeanExporter mBeanExporter; @Resource(name="blConfiguration") RuntimeEnvironmentPropertiesConfigurer configurer; /** * This should only be used in a test context to deal with the Spring ApplicationContext refreshing between different * test classes but not needing to do a new transformation of classes every time. This bean will get * re-initialized but all the classes have already been transformed */ protected static boolean transformed = false; @Override protected boolean isPersistenceUnitOverrideAllowed() { return true; } @PostConstruct public void configureMergedItems() { String[] tempLocations; try { Field persistenceXmlLocations = DefaultPersistenceUnitManager.class.getDeclaredField("persistenceXmlLocations"); persistenceXmlLocations.setAccessible(true); tempLocations = (String[]) persistenceXmlLocations.get(this); } catch (Exception e) { throw new RuntimeException(e); } for (String legacyLocation : tempLocations) { if (!legacyLocation.endsWith("/persistence.xml")) { //do not add the default JPA persistence location by default mergedPersistenceXmlLocations.add(legacyLocation); } } setPersistenceXmlLocations(mergedPersistenceXmlLocations.toArray(new String[mergedPersistenceXmlLocations.size()])); if (!mergedDataSources.isEmpty()) { setDataSources(mergedDataSources); } } @PostConstruct public void configureClassTransformers() throws InstantiationException, IllegalAccessException, ClassNotFoundException { classTransformers.addAll(mergedClassTransformers); } protected MutablePersistenceUnitInfo getMergedUnit(String persistenceUnitName, MutablePersistenceUnitInfo newPU) { if (!mergedPus.containsKey(persistenceUnitName)) { mergedPus.put(persistenceUnitName, newPU); } return (MutablePersistenceUnitInfo) mergedPus.get(persistenceUnitName); } @Override @SuppressWarnings({ "unchecked", "ToArrayCallWithZeroLengthArrayArgument" }) public void preparePersistenceUnitInfos() { //Need to use reflection to try and execute the logic in the DefaultPersistenceUnitManager //SpringSource added a block of code in version 3.1 to "protect" the user from having more than one PU with //the same name. Of course, in our case, this happens before a merge occurs. They have added //a block of code to throw an exception if more than one PU has the same name. We want to //use the logic of the DefaultPersistenceUnitManager without the exception in the case of //a duplicate name. This will require reflection in order to do what we need. try { Set<String> persistenceUnitInfoNames = null; Map<String, PersistenceUnitInfo> persistenceUnitInfos = null; ResourcePatternResolver resourcePatternResolver = null; Field[] fields = getClass().getSuperclass().getDeclaredFields(); for (Field field : fields) { if ("persistenceUnitInfoNames".equals(field.getName())) { field.setAccessible(true); persistenceUnitInfoNames = (Set<String>) field.get(this); } else if ("persistenceUnitInfos".equals(field.getName())) { field.setAccessible(true); persistenceUnitInfos = (Map<String, PersistenceUnitInfo>) field.get(this); } else if ("resourcePatternResolver".equals(field.getName())) { field.setAccessible(true); resourcePatternResolver = (ResourcePatternResolver) field.get(this); } } persistenceUnitInfoNames.clear(); persistenceUnitInfos.clear(); Method readPersistenceUnitInfos = getClass(). getSuperclass(). getDeclaredMethod("readPersistenceUnitInfos"); readPersistenceUnitInfos.setAccessible(true); //In Spring 3.0 this returns an array //In Spring 3.1 this returns a List Object pInfosObject = readPersistenceUnitInfos.invoke(this); Object[] puis; if (pInfosObject.getClass().isArray()) { puis = (Object[]) pInfosObject; } else { puis = ((Collection) pInfosObject).toArray(); } for (Object pui : puis) { MutablePersistenceUnitInfo mPui = (MutablePersistenceUnitInfo) pui; if (mPui.getPersistenceUnitRootUrl() == null) { Method determineDefaultPersistenceUnitRootUrl = getClass(). getSuperclass(). getDeclaredMethod("determineDefaultPersistenceUnitRootUrl"); determineDefaultPersistenceUnitRootUrl.setAccessible(true); mPui.setPersistenceUnitRootUrl((URL) determineDefaultPersistenceUnitRootUrl.invoke(this)); } ConfigurationOnlyState state = ConfigurationOnlyState.getState(); if ((state == null || !state.isConfigurationOnly()) && mPui.getNonJtaDataSource() == null) { mPui.setNonJtaDataSource(getDefaultDataSource()); } if (super.getLoadTimeWeaver() != null) { Method puiInitMethod = mPui.getClass().getDeclaredMethod("init", LoadTimeWeaver.class); puiInitMethod.setAccessible(true); puiInitMethod.invoke(pui, getLoadTimeWeaver()); } else { Method puiInitMethod = mPui.getClass().getDeclaredMethod("init", ClassLoader.class); puiInitMethod.setAccessible(true); puiInitMethod.invoke(pui, resourcePatternResolver.getClassLoader()); } postProcessPersistenceUnitInfo((MutablePersistenceUnitInfo) pui); String name = mPui.getPersistenceUnitName(); persistenceUnitInfoNames.add(name); persistenceUnitInfos.put(name, mPui); } } catch (Exception e) { throw new RuntimeException("An error occured reflectively invoking methods on " + "class: " + getClass().getSuperclass().getName(), e); } try { List<String> managedClassNames = new ArrayList<String>(); boolean weaverRegistered = true; for (PersistenceUnitInfo pui : mergedPus.values()) { for (BroadleafClassTransformer transformer : classTransformers) { try { if (!(transformer instanceof NullClassTransformer) && pui.getPersistenceUnitName().equals("blPU")) { pui.addTransformer(transformer); } } catch (Exception e) { Exception refined = ExceptionHelper.refineException(IllegalStateException.class, RuntimeException.class, e); if (refined instanceof IllegalStateException) { LOG.warn("A BroadleafClassTransformer is configured for this persistence unit, but Spring " + "reported a problem (likely that a LoadTimeWeaver is not registered). As a result, " + "the Broadleaf Commerce ClassTransformer ("+transformer.getClass().getName()+") is " + "not being registered with the persistence unit."); weaverRegistered = false; } else { throw refined; } } } } // Only validate transformation results if there was a LoadTimeWeaver registered in the first place if (weaverRegistered && !transformed) { for (PersistenceUnitInfo pui : mergedPus.values()) { for (String managedClassName : pui.getManagedClassNames()) { if (!managedClassNames.contains(managedClassName)) { // Force-load this class so that we are able to ensure our instrumentation happens globally. // If transformation is happening, it should be tracked in EntityMarkerClassTransformer Class.forName(managedClassName, true, getClass().getClassLoader()); managedClassNames.add(managedClassName); } } } // If a class happened to be loaded by the ClassLoader before we had a chance to set up our instrumentation, // it may not be in a consistent state. This verifies with the EntityMarkerClassTransformer that it // actually saw the classes loaded by the above process List<String> nonTransformedClasses = new ArrayList<String>(); for (PersistenceUnitInfo pui : mergedPus.values()) { for (String managedClassName : pui.getManagedClassNames()) { // We came across a class that is not a real persistence class (doesn't have the right annotations) // but is still being transformed/loaded by // the persistence unit. This might have unexpected results downstream, but it could also be benign // so just output a warning if (entityMarkerClassTransformer.getTransformedNonEntityClassNames().contains(managedClassName)) { LOG.warn("The class " + managedClassName + " is marked as a managed class within the MergePersistenceUnitManager" + " but is not annotated with @Entity, @MappedSuperclass or @Embeddable." + " This class is still referenced in a persistence.xml and is being transformed by" + " PersistenceUnit ClassTransformers which may result in problems downstream" + " and represents a potential misconfiguration. This class should be removed from" + " your persistence.xml"); } else if (!entityMarkerClassTransformer.getTransformedEntityClassNames().contains(managedClassName)) { // This means the class not in the 'warning' list, but it is also not in the list that we would // expect it to be in of valid entity classes that were transformed. This means that we // never got the chance to transform the class AT ALL even though it is a valid entity class nonTransformedClasses.add(managedClassName); } } } if (CollectionUtils.isNotEmpty(nonTransformedClasses)) { String message = "The classes\n" + Arrays.toString(nonTransformedClasses.toArray()) + "\nare managed classes within the MergePersistenceUnitManager" + "\nbut were not detected as being transformed by the EntityMarkerClassTransformer. These" + "\nclasses are likely loaded earlier in the application startup lifecyle by the servlet" + "\ncontainer. Verify that an empty <absolute-ordering /> element is contained in your" + "\nweb.xml to disable scanning for ServletContainerInitializer classes by your servlet" + "\ncontainer which can trigger early class loading. If the problem persists, ensure that" + "\nthere are no bean references to your entity class anywhere else in your Spring applicationContext" + "\nand consult the documentation for your servlet container to determine if classes are loaded" + "\nprior to the Spring context initialization. Also, it is a necessity that " + "\n'-javaagent:/path/to/spring-instrument-4.1.5.jar' be added to the JVM args of the server." + "\nFinally, ensure that Session Persistence is also disabled by your Servlet Container." + "\nTo do this in Tomcat, add <Manager pathname=\"\" /> inside of the <Context> element" + "\nin context.xml in your app's META-INF folder or your server's conf folder."; LOG.error(message); throw new IllegalStateException(message); } transformed = true; } if (transformed) { LOG.info("Did not recycle through class transformation since this has already occurred"); } } catch (Exception e) { throw new RuntimeException(e); } } @Override protected void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo newPU) { super.postProcessPersistenceUnitInfo(newPU); ConfigurationOnlyState state = ConfigurationOnlyState.getState(); String persistenceUnitName = newPU.getPersistenceUnitName(); MutablePersistenceUnitInfo pui = getMergedUnit(persistenceUnitName, newPU); List<String> managedClassNames = newPU.getManagedClassNames(); for (String managedClassName : managedClassNames){ if (!pui.getManagedClassNames().contains(managedClassName)) { pui.addManagedClassName(managedClassName); } } List<String> mappingFileNames = newPU.getMappingFileNames(); for (String mappingFileName : mappingFileNames) { if (!pui.getMappingFileNames().contains(mappingFileName)) { pui.addMappingFileName(mappingFileName); } } pui.setExcludeUnlistedClasses(newPU.excludeUnlistedClasses()); for (URL url : newPU.getJarFileUrls()) { // Avoid duplicate class scanning by Ejb3Configuration. Do not re-add the URL to the list of jars for this // persistence unit or duplicate the persistence unit root URL location (both types of locations are scanned) if (!pui.getJarFileUrls().contains(url) && !pui.getPersistenceUnitRootUrl().equals(url)) { pui.addJarFileUrl(url); } } if (pui.getProperties() == null) { pui.setProperties(newPU.getProperties()); } else { Properties props = newPU.getProperties(); if (props != null) { for (Object key : props.keySet()) { pui.getProperties().put(key, props.get(key)); for (BroadleafClassTransformer transformer : classTransformers) { try { transformer.compileJPAProperties(props, key); } catch (Exception e) { throw new RuntimeException(e); } } } } } disableSchemaCreateIfApplicable(persistenceUnitName, pui); if (state == null || !state.isConfigurationOnly()) { if (newPU.getJtaDataSource() != null) { pui.setJtaDataSource(newPU.getJtaDataSource()); } if (newPU.getNonJtaDataSource() != null) { pui.setNonJtaDataSource(newPU.getNonJtaDataSource()); } } else { pui.getProperties().setProperty("hibernate.hbm2ddl.auto", "none"); pui.getProperties().setProperty("hibernate.temp.use_jdbc_metadata_defaults", "false"); } pui.setTransactionType(newPU.getTransactionType()); if (newPU.getPersistenceProviderClassName() != null) { pui.setPersistenceProviderClassName(newPU.getPersistenceProviderClassName()); } if (newPU.getPersistenceProviderPackageName() != null) { pui.setPersistenceProviderPackageName(newPU.getPersistenceProviderPackageName()); } } /* (non-Javadoc) * @see org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager#obtainPersistenceUnitInfo(java.lang.String) */ @Override public PersistenceUnitInfo obtainPersistenceUnitInfo(String persistenceUnitName) { return mergedPus.get(persistenceUnitName); } /* (non-Javadoc) * @see org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager#obtainDefaultPersistenceUnitInfo() */ @Override public PersistenceUnitInfo obtainDefaultPersistenceUnitInfo() { throw new IllegalStateException("Default Persistence Unit is not supported. The persistence unit name must be specified at the entity manager factory."); } public List<BroadleafClassTransformer> getClassTransformers() { return classTransformers; } public void setClassTransformers(List<BroadleafClassTransformer> classTransformers) { this.classTransformers = classTransformers; } protected void disableSchemaCreateIfApplicable(String persistenceUnitName, MutablePersistenceUnitInfo pui) { String autoDDLStatus = pui.getProperties().getProperty("hibernate.hbm2ddl.auto"); boolean isCreate = autoDDLStatus != null && (autoDDLStatus.equals("create") || autoDDLStatus.equals("create-drop")); boolean detectedCreate = false; if (isCreate && configurer.determineEnvironment().equals(configurer.getDefaultEnvironment())) { try { if (mBeanExporter.getServer().isRegistered(ObjectName.getInstance("bean:name=autoDDLCreateStatusTestBean"))) { Boolean response = (Boolean) mBeanExporter.getServer().invoke(ObjectName.getInstance("bean:name=autoDDLCreateStatusTestBean"), "getStartedWithCreate", new Object[]{persistenceUnitName}, new String[]{String.class.getName()}); if (response == null) { mBeanExporter.getServer().invoke(ObjectName.getInstance("bean:name=autoDDLCreateStatusTestBean"), "setStartedWithCreate", new Object[]{persistenceUnitName, true}, new String[]{String.class.getName(), Boolean.class.getName()}); } else { detectedCreate = true; } } } catch (Exception e) { LOG.warn("Unable to query the mbean server for previous auto.ddl status", e); } } if (detectedCreate) { pui.getProperties().setProperty("hibernate.hbm2ddl.auto", "none"); } } }