// $Id$
/*
 * Copyright (c) 2009, Red Hat Middleware LLC or third-party contributors as
 * indicated by the @author tags or express copyright attribution
 * statements applied by the authors.  All third-party contributions are
 * distributed under license by Red Hat Middleware LLC.
 *
 * This copyrighted material is made available to anyone wishing to use, modify,
 * copy, or redistribute it subject to the terms and conditions of the GNU
 * Lesser General Public License, as published by the Free Software Foundation.
 *
 * 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this distribution; if not, write to:
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA  02110-1301  USA
 */
package org.hibernate.ejb;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import javax.naming.BinaryRefAddr;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityNotFoundException;
import javax.persistence.MappedSuperclass;
import javax.persistence.PersistenceException;
import javax.persistence.spi.PersistenceUnitInfo;
import javax.persistence.spi.PersistenceUnitTransactionType;
import javax.sql.DataSource;

import org.dom4j.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;

import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.MappingException;
import org.hibernate.MappingNotFoundException;
import org.hibernate.SessionFactory;
import org.hibernate.SessionFactoryObserver;
import org.hibernate.cfg.AnnotationConfiguration;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.cfg.NamingStrategy;
import org.hibernate.cfg.Settings;
import org.hibernate.cfg.SettingsFactory;
import org.hibernate.cfg.annotations.reflection.XMLContext;
import org.hibernate.ejb.cfg.spi.IdentifierGeneratorStrategyProvider;
import org.hibernate.ejb.connection.InjectedDataSourceConnectionProvider;
import org.hibernate.ejb.instrument.InterceptFieldClassFileTransformer;
import org.hibernate.ejb.packaging.JarVisitorFactory;
import org.hibernate.ejb.packaging.NamedInputStream;
import org.hibernate.ejb.packaging.NativeScanner;
import org.hibernate.ejb.packaging.PersistenceMetadata;
import org.hibernate.ejb.packaging.PersistenceXmlLoader;
import org.hibernate.ejb.packaging.Scanner;
import org.hibernate.ejb.transaction.JoinableCMTTransactionFactory;
import org.hibernate.ejb.util.ConfigurationHelper;
import org.hibernate.ejb.util.LogHelper;
import org.hibernate.ejb.util.NamingHelper;
import org.hibernate.engine.FilterDefinition;
import org.hibernate.event.EventListeners;
import org.hibernate.id.factory.DefaultIdentifierGeneratorFactory;
import org.hibernate.mapping.AuxiliaryDatabaseObject;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.persister.PersisterClassProvider;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.secure.JACCConfiguration;
import org.hibernate.transaction.JDBCTransactionFactory;
import org.hibernate.util.CollectionHelper;
import org.hibernate.util.ReflectHelper;
import org.hibernate.util.StringHelper;
import org.hibernate.util.xml.MappingReader;
import org.hibernate.util.xml.OriginImpl;
import org.hibernate.util.xml.XmlDocument;

/**
 * Allow a fine tuned configuration of an EJB 3.0 EntityManagerFactory
 *
 * A Ejb3Configuration object is only guaranteed to create one EntityManagerFactory.
 * Multiple usage of #buildEntityManagerFactory() is not guaranteed.
 *
 * After #buildEntityManagerFactory() has been called, you no longer can change the configuration
 * state (no class adding, no property change etc)
 *
 * When serialized / deserialized or retrieved from the JNDI, you no longer can change the
 * configuration state (no class adding, no property change etc)
 *
 * Putting the configuration in the JNDI is an expensive operation that requires a partial
 * serialization
 *
 * @author Emmanuel Bernard
 */
public class Ejb3Configuration implements Serializable, Referenceable {
	private final Logger log = LoggerFactory.getLogger( Ejb3Configuration.class );
	private static final String IMPLEMENTATION_NAME = HibernatePersistence.class.getName();
	private static final String META_INF_ORM_XML = "META-INF/orm.xml";
	private static final String PARSED_MAPPING_DOMS = "hibernate.internal.mapping_doms";

	private static EntityNotFoundDelegate ejb3EntityNotFoundDelegate = new Ejb3EntityNotFoundDelegate();
	private static Configuration DEFAULT_CONFIGURATION = new AnnotationConfiguration();

	private static class Ejb3EntityNotFoundDelegate implements EntityNotFoundDelegate, Serializable {
		public void handleEntityNotFound(String entityName, Serializable id) {
			throw new EntityNotFoundException("Unable to find " + entityName  + " with id " + id);
		}
	}

	static {
		Version.touch();
	}

	private String persistenceUnitName;
	private String cfgXmlResource;

	private AnnotationConfiguration cfg;
	private SettingsFactory settingsFactory;
	//made transient and not restored in deserialization on purpose, should no longer be called after restoration
	private transient EventListenerConfigurator listenerConfigurator;
	private PersistenceUnitTransactionType transactionType;
	private boolean discardOnClose;
	//made transient and not restored in deserialization on purpose, should no longer be called after restoration
	private transient ClassLoader overridenClassLoader;
	private boolean isConfigurationProcessed = false;


	public Ejb3Configuration() {
		settingsFactory = new InjectionSettingsFactory();
		cfg = new AnnotationConfiguration( settingsFactory );
		cfg.setEntityNotFoundDelegate( ejb3EntityNotFoundDelegate );
		listenerConfigurator = new EventListenerConfigurator( this );
	}

	/**
	 * Used to inject a datasource object as the connection provider.
	 * If used, be sure to <b>not override</b> the hibernate.connection.provider_class
	 * property
	 */
	@SuppressWarnings({ "JavaDoc", "unchecked" })
	public void setDataSource(DataSource ds) {
		if ( ds != null ) {
			Map cpInjection = new HashMap();
			cpInjection.put( "dataSource", ds );
			( (InjectionSettingsFactory) settingsFactory ).setConnectionProviderInjectionData( cpInjection );
			this.setProperty( Environment.CONNECTION_PROVIDER, InjectedDataSourceConnectionProvider.class.getName() );
		}
	}

	/**
	 * create a factory from a parsed persistence.xml
	 * Especially the scanning of classes and additional jars is done already at this point.
	 * <p/>
	 * NOTE: public only for unit testing purposes; not a public API!
	 *
	 * @param metadata The information parsed from the persistence.xml
	 * @param overridesIn Any explicitly passed config settings
	 *
	 * @return this
	 */
	@SuppressWarnings({ "unchecked" })
	public Ejb3Configuration configure(PersistenceMetadata metadata, Map overridesIn) {
		log.debug( "Creating Factory: {}", metadata.getName() );

		Map overrides = new HashMap();
		if ( overridesIn != null ) {
			overrides.putAll( overridesIn );
		}

		Map workingVars = new HashMap();
		workingVars.put( AvailableSettings.PERSISTENCE_UNIT_NAME, metadata.getName() );
		this.persistenceUnitName = metadata.getName();

		if ( StringHelper.isNotEmpty( metadata.getJtaDatasource() ) ) {
			this.setProperty( Environment.DATASOURCE, metadata.getJtaDatasource() );
		}
		else if ( StringHelper.isNotEmpty( metadata.getNonJtaDatasource() ) ) {
			this.setProperty( Environment.DATASOURCE, metadata.getNonJtaDatasource() );
		}
		else {
			final String driver = (String) metadata.getProps().get( AvailableSettings.JDBC_DRIVER );
			if ( StringHelper.isNotEmpty( driver ) ) {
				this.setProperty( Environment.DRIVER, driver );
			}
			final String url = (String) metadata.getProps().get( AvailableSettings.JDBC_URL );
			if ( StringHelper.isNotEmpty( url ) ) {
				this.setProperty( Environment.URL, url );
			}
			final String user = (String) metadata.getProps().get( AvailableSettings.JDBC_USER );
			if ( StringHelper.isNotEmpty( user ) ) {
				this.setProperty( Environment.USER, user );
			}
			final String pass = (String) metadata.getProps().get( AvailableSettings.JDBC_PASSWORD );
			if ( StringHelper.isNotEmpty( pass ) ) {
				this.setProperty( Environment.PASS, pass );
			}
		}
		defineTransactionType( metadata.getTransactionType(), workingVars );
		if ( metadata.getClasses().size() > 0 ) {
			workingVars.put( AvailableSettings.CLASS_NAMES, metadata.getClasses() );
		}
		if ( metadata.getPackages().size() > 0 ) {
			workingVars.put( AvailableSettings.PACKAGE_NAMES, metadata.getPackages() );
		}
		if ( metadata.getMappingFiles().size() > 0 ) {
			workingVars.put( AvailableSettings.XML_FILE_NAMES, metadata.getMappingFiles() );
		}
		if ( metadata.getHbmfiles().size() > 0 ) {
			workingVars.put( AvailableSettings.HBXML_FILES, metadata.getHbmfiles() );
		}

		Properties props = new Properties();
		props.putAll( metadata.getProps() );

		// validation factory
		final Object validationFactory = overrides.get( AvailableSettings.VALIDATION_FACTORY );
		if ( validationFactory != null ) {
			props.put( AvailableSettings.VALIDATION_FACTORY, validationFactory );
		}
		overrides.remove( AvailableSettings.VALIDATION_FACTORY );

		// validation-mode (overrides has precedence)
		{
			final Object integrationValue = overrides.get( AvailableSettings.VALIDATION_MODE );
			if ( integrationValue != null ) {
				props.put( AvailableSettings.VALIDATION_MODE, integrationValue.toString() );
			}
			else if ( metadata.getValidationMode() != null ) {
				props.put( AvailableSettings.VALIDATION_MODE, metadata.getValidationMode() );
			}
			overrides.remove( AvailableSettings.VALIDATION_MODE );
		}

		// shared-cache-mode (overrides has precedence)
		{
			final Object integrationValue = overrides.get( AvailableSettings.SHARED_CACHE_MODE );
			if ( integrationValue != null ) {
				props.put( AvailableSettings.SHARED_CACHE_MODE, integrationValue.toString() );
			}
			else if ( metadata.getSharedCacheMode() != null ) {
				props.put( AvailableSettings.SHARED_CACHE_MODE, metadata.getSharedCacheMode() );
			}
			overrides.remove( AvailableSettings.SHARED_CACHE_MODE );
		}

		for ( Map.Entry entry : (Set<Map.Entry>) overrides.entrySet() ) {
			Object value = entry.getValue();
			props.put( entry.getKey(), value == null ? "" :  value ); //alter null, not allowed in properties
		}

		configure( props, workingVars );
		return this;
	}

	/**
	 * Build the configuration from an entity manager name and given the
	 * appropriate extra properties. Those properties override the one get through
	 * the peristence.xml file.
	 * If the persistence unit name is not found or does not match the Persistence Provider, null is returned
	 *
	 * This method is used in a non managed environment
	 *
	 * @param persistenceUnitName persistence unit name
	 * @param integration properties passed to the persistence provider
	 *
	 * @return configured Ejb3Configuration or null if no persistence unit match
	 *
	 * @see HibernatePersistence#createEntityManagerFactory(String, java.util.Map)
	 */
	@SuppressWarnings({ "unchecked" })
	public Ejb3Configuration configure(String persistenceUnitName, Map integration) {
		try {
			log.debug( "Look up for persistence unit: {}", persistenceUnitName );
			integration = integration == null ?
					CollectionHelper.EMPTY_MAP :
					Collections.unmodifiableMap( integration );
			Enumeration<URL> xmls = Thread.currentThread()
					.getContextClassLoader()
					.getResources( "META-INF/persistence.xml" );
			if ( ! xmls.hasMoreElements() ) {
				log.info( "Could not find any META-INF/persistence.xml file in the classpath");
			}
			while ( xmls.hasMoreElements() ) {
				URL url = xmls.nextElement();
				log.trace( "Analysing persistence.xml: {}", url );
				List<PersistenceMetadata> metadataFiles = PersistenceXmlLoader.deploy(
						url,
						integration,
						cfg.getEntityResolver(),
						PersistenceUnitTransactionType.RESOURCE_LOCAL );
				for ( PersistenceMetadata metadata : metadataFiles ) {
					log.trace( "{}", metadata );

					if ( metadata.getProvider() == null || IMPLEMENTATION_NAME.equalsIgnoreCase(
							metadata.getProvider()
					) ) {
						//correct provider

						//lazy load the scanner to avoid unnecessary IOExceptions
						Scanner scanner = null;
						URL jarURL = null;
						if ( metadata.getName() == null ) {
							scanner = buildScanner( metadata.getProps(), integration );
							jarURL = JarVisitorFactory.getJarURLFromURLEntry( url, "/META-INF/persistence.xml" );
							metadata.setName( scanner.getUnqualifiedJarName(jarURL) );
						}
						if ( persistenceUnitName == null && xmls.hasMoreElements() ) {
							throw new PersistenceException( "No name provided and several persistence units found" );
						}
						else if ( persistenceUnitName == null || metadata.getName().equals( persistenceUnitName ) ) {
							if (scanner == null) {
								scanner = buildScanner( metadata.getProps(), integration );
								jarURL = JarVisitorFactory.getJarURLFromURLEntry( url, "/META-INF/persistence.xml" );
							}
							//scan main JAR
							ScanningContext mainJarScanCtx = new ScanningContext()
									.scanner( scanner )
									.url( jarURL )
									.explicitMappingFiles( metadata.getMappingFiles() )
									.searchOrm( true );
							setDetectedArtifactsOnScanningContext( mainJarScanCtx, metadata.getProps(), integration,
																				metadata.getExcludeUnlistedClasses() );
							addMetadataFromScan( mainJarScanCtx, metadata );

							ScanningContext otherJarScanCtx = new ScanningContext()
									.scanner( scanner )
									.explicitMappingFiles( metadata.getMappingFiles() )
									.searchOrm( true );
							setDetectedArtifactsOnScanningContext( otherJarScanCtx, metadata.getProps(), integration,
																				false );
							for ( String jarFile : metadata.getJarFiles() ) {
								otherJarScanCtx.url( JarVisitorFactory.getURLFromPath( jarFile ) );
								addMetadataFromScan( otherJarScanCtx, metadata );
							}
							return configure( metadata, integration );
						}
					}
				}
			}
			return null;
		}
		catch (Exception e) {
			if ( e instanceof PersistenceException) {
				throw (PersistenceException) e;
			}
			else {
				throw new PersistenceException( getExceptionHeader() + "Unable to configure EntityManagerFactory", e );
			}
		}
	}

	private Scanner buildScanner(Properties properties, Map<?,?> integration) {
		//read the String or Instance from the integration map first and use the properties as a backup.
		Object scanner = integration.get( AvailableSettings.SCANNER );
		if (scanner == null) {
			scanner = properties.getProperty( AvailableSettings.SCANNER );
		}
		if (scanner != null) {
			Class<?> scannerClass;
			if ( scanner instanceof String ) {
				try {
					scannerClass = ReflectHelper.classForName( (String) scanner, this.getClass() );
				}
				catch ( ClassNotFoundException e ) {
					throw new PersistenceException(  "Cannot find scanner class. " + AvailableSettings.SCANNER + "=" + scanner, e );
				}
			}
			else if (scanner instanceof Class) {
				scannerClass = (Class<? extends Scanner>) scanner;
			}
			else if (scanner instanceof Scanner) {
				return (Scanner) scanner;
			}
			else {
				throw new PersistenceException(  "Scanner class configuration error: unknown type on the property. " + AvailableSettings.SCANNER );
			}
			try {
				return (Scanner) scannerClass.newInstance();
			}
			catch ( InstantiationException e ) {
				throw new PersistenceException(  "Unable to load Scanner class: " + scannerClass, e );
			}
			catch ( IllegalAccessException e ) {
				throw new PersistenceException(  "Unable to load Scanner class: " + scannerClass, e );
			}
		}
		else {
			return new NativeScanner();
		}
	}

	private static class ScanningContext {
		//boolean excludeUnlistedClasses;
		private Scanner scanner;
		private URL url;
		private List<String> explicitMappingFiles;
		private boolean detectClasses;
		private boolean detectHbmFiles;
		private boolean searchOrm;

		public ScanningContext scanner(Scanner scanner) {
			this.scanner = scanner;
			return this;
		}

		public ScanningContext url(URL url) {
			this.url = url;
			return this;
		}

		public ScanningContext explicitMappingFiles(List<String> explicitMappingFiles) {
			this.explicitMappingFiles = explicitMappingFiles;
			return this;
		}

		public ScanningContext detectClasses(boolean detectClasses) {
			this.detectClasses = detectClasses;
			return this;
		}

		public ScanningContext detectHbmFiles(boolean detectHbmFiles) {
			this.detectHbmFiles = detectHbmFiles;
			return this;
		}

		public ScanningContext searchOrm(boolean searchOrm) {
			this.searchOrm = searchOrm;
			return this;
		}
	}

	private static void addMetadataFromScan(ScanningContext scanningContext, PersistenceMetadata metadata) throws IOException {
		List<String> classes = metadata.getClasses();
		List<String> packages = metadata.getPackages();
		List<NamedInputStream> hbmFiles = metadata.getHbmfiles();
		List<String> mappingFiles = metadata.getMappingFiles();
		addScannedEntries( scanningContext, classes, packages, hbmFiles, mappingFiles );
	}

	private static void addScannedEntries(ScanningContext scanningContext, List<String> classes, List<String> packages, List<NamedInputStream> hbmFiles, List<String> mappingFiles) throws IOException {
		Scanner scanner = scanningContext.scanner;
		if (scanningContext.detectClasses) {
			Set<Class<? extends Annotation>> annotationsToExclude = new HashSet<Class<? extends Annotation>>(3);
			annotationsToExclude.add( Entity.class );
			annotationsToExclude.add( MappedSuperclass.class );
			annotationsToExclude.add( Embeddable.class );
			Set<Class<?>> matchingClasses = scanner.getClassesInJar( scanningContext.url, annotationsToExclude );
			for (Class<?> clazz : matchingClasses) {
				classes.add( clazz.getName() );
			}

			Set<Package> matchingPackages = scanner.getPackagesInJar( scanningContext.url, new HashSet<Class<? extends Annotation>>(0) );
			for (Package pkg : matchingPackages) {
				packages.add( pkg.getName() );
			}
		}
		Set<String> patterns = new HashSet<String>();
		if (scanningContext.searchOrm) {
			patterns.add( META_INF_ORM_XML );
		}
		if (scanningContext.detectHbmFiles) {
			patterns.add( "**/*.hbm.xml" );
		}
		if ( mappingFiles != null) patterns.addAll( mappingFiles );
		if (patterns.size() !=0) {
			Set<NamedInputStream> files = scanner.getFilesInJar( scanningContext.url, patterns );
			for (NamedInputStream file : files) {
				hbmFiles.add( file );
				if (mappingFiles != null) mappingFiles.remove( file.getName() );
			}
		}
	}

	/**
	 * Process configuration from a PersistenceUnitInfo object; typically called by the container
	 * via {@link javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory}.
	 * In Hibernate EM, this correlates to {@link HibernatePersistence#createContainerEntityManagerFactory}
	 *
	 * @param info The persistence unit info passed in by the container (usually from processing a persistence.xml).
	 * @param integration The map of integration properties from the container to configure the provider.
	 *
	 * @return The configured EJB3Configurartion object
	 *
	 * @see HibernatePersistence#createContainerEntityManagerFactory
	 */
	@SuppressWarnings({ "unchecked" })
	public Ejb3Configuration configure(PersistenceUnitInfo info, Map integration) {
		if ( log.isDebugEnabled() ) {
			log.debug( "Processing {}", LogHelper.logPersistenceUnitInfo( info ) );
		}
		else {
			log.info( "Processing PersistenceUnitInfo [\n\tname: {}\n\t...]", info.getPersistenceUnitName() );
		}

		// Spec says the passed map may be null, so handle that to make further processing easier...
		integration = integration != null ? Collections.unmodifiableMap( integration ) : CollectionHelper.EMPTY_MAP;

		// See if we (Hibernate) are the persistence provider
		String provider = (String) integration.get( AvailableSettings.PROVIDER );
		if ( provider == null ) {
			provider = info.getPersistenceProviderClassName();
		}
		if ( provider != null && ! provider.trim().startsWith( IMPLEMENTATION_NAME ) ) {
			log.info( "Required a different provider: {}", provider );
			return null;
		}

		// set the classloader, passed in by the container in info, to set as the TCCL so that
		// Hibernate uses it to properly resolve class references.
		if ( info.getClassLoader() == null ) {
			throw new IllegalStateException(
					"[PersistenceUnit: " + info.getPersistenceUnitName() == null ? "" : info.getPersistenceUnitName()
							+ "] " + "PersistenceUnitInfo.getClassLoader() id null" );
		}
		Thread thread = Thread.currentThread();
		ClassLoader contextClassLoader = thread.getContextClassLoader();
		boolean sameClassLoader = info.getClassLoader().equals( contextClassLoader );
		if ( ! sameClassLoader ) {
			overridenClassLoader = info.getClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		else {
			overridenClassLoader = null;
		}

		// Best I can tell, 'workingVars' is some form of additional configuration contract.
		// But it does not correlate 1-1 to EMF/SF settings.  It really is like a set of de-typed
		// additional configuration info.  I think it makes better sense to define this as an actual
		// contract if that was in fact the intent; the code here is pretty confusing.
		try {
			Map workingVars = new HashMap();
			workingVars.put( AvailableSettings.PERSISTENCE_UNIT_NAME, info.getPersistenceUnitName() );
			this.persistenceUnitName = info.getPersistenceUnitName();
			List<String> entities = new ArrayList<String>( 50 );
			if ( info.getManagedClassNames() != null ) entities.addAll( info.getManagedClassNames() );
			List<NamedInputStream> hbmFiles = new ArrayList<NamedInputStream>();
			List<String> packages = new ArrayList<String>();
			List<String> xmlFiles = new ArrayList<String>( 50 );
			List<XmlDocument> xmlDocuments = new ArrayList<XmlDocument>( 50 );
			if ( info.getMappingFileNames() != null ) {
				xmlFiles.addAll( info.getMappingFileNames() );
			}
			//Should always be true if the container is not dump
			boolean searchForORMFiles = ! xmlFiles.contains( META_INF_ORM_XML );

			ScanningContext context = new ScanningContext();
			final Properties copyOfProperties = (Properties) info.getProperties().clone();
			ConfigurationHelper.overrideProperties( copyOfProperties, integration );
			context.scanner( buildScanner( copyOfProperties, integration ) )
					.searchOrm( searchForORMFiles )
					.explicitMappingFiles( null ); //URLs provided by the container already

			//context for other JARs
			setDetectedArtifactsOnScanningContext(context, info.getProperties(), null, false );
			for ( URL jar : info.getJarFileUrls() ) {
				context.url(jar);
				scanForClasses( context, packages, entities, hbmFiles );
			}

			//main jar
			context.url( info.getPersistenceUnitRootUrl() );
			setDetectedArtifactsOnScanningContext( context, info.getProperties(), null, info.excludeUnlistedClasses() );
			scanForClasses( context, packages, entities, hbmFiles );

			Properties properties = info.getProperties() != null ? info.getProperties() : new Properties();
			ConfigurationHelper.overrideProperties( properties, integration );

			//FIXME entities is used to enhance classes and to collect annotated entities this should not be mixed
			//fill up entities with the on found in xml files
			addXMLEntities( xmlFiles, info, entities, xmlDocuments );

			//FIXME send the appropriate entites.
			if ( "true".equalsIgnoreCase( properties.getProperty( AvailableSettings.USE_CLASS_ENHANCER ) ) ) {
				info.addTransformer( new InterceptFieldClassFileTransformer( entities ) );
			}

			workingVars.put( AvailableSettings.CLASS_NAMES, entities );
			workingVars.put( AvailableSettings.PACKAGE_NAMES, packages );
			workingVars.put( AvailableSettings.XML_FILE_NAMES, xmlFiles );
			workingVars.put( PARSED_MAPPING_DOMS, xmlDocuments );

			if ( hbmFiles.size() > 0 ) {
				workingVars.put( AvailableSettings.HBXML_FILES, hbmFiles );
			}

			// validation factory
			final Object validationFactory = integration.get( AvailableSettings.VALIDATION_FACTORY );
			if ( validationFactory != null ) {
				properties.put( AvailableSettings.VALIDATION_FACTORY, validationFactory );
			}

			// validation-mode (integration has precedence)
			{
				final Object integrationValue = integration.get( AvailableSettings.VALIDATION_MODE );
				if ( integrationValue != null ) {
					properties.put( AvailableSettings.VALIDATION_MODE, integrationValue.toString() );
				}
				else if ( info.getValidationMode() != null ) {
					properties.put( AvailableSettings.VALIDATION_MODE, info.getValidationMode().name() );
				}
			}

			// shared-cache-mode (integration has precedence)
			{
				final Object integrationValue = integration.get( AvailableSettings.SHARED_CACHE_MODE );
				if ( integrationValue != null ) {
					properties.put( AvailableSettings.SHARED_CACHE_MODE, integrationValue.toString() );
				}
				else if ( info.getSharedCacheMode() != null ) {
					properties.put( AvailableSettings.SHARED_CACHE_MODE, info.getSharedCacheMode().name() );
				}
			}

			//datasources
			Boolean isJTA = null;
			boolean overridenDatasource = false;
			if ( integration.containsKey( AvailableSettings.JTA_DATASOURCE ) ) {
				String dataSource = (String) integration.get( AvailableSettings.JTA_DATASOURCE );
				overridenDatasource = true;
				properties.setProperty( Environment.DATASOURCE, dataSource );
				isJTA = Boolean.TRUE;
			}
			if ( integration.containsKey( AvailableSettings.NON_JTA_DATASOURCE ) ) {
				String dataSource = (String) integration.get( AvailableSettings.NON_JTA_DATASOURCE );
				overridenDatasource = true;
				properties.setProperty( Environment.DATASOURCE, dataSource );
				if (isJTA == null) isJTA = Boolean.FALSE;
			}

			if ( ! overridenDatasource && ( info.getJtaDataSource() != null || info.getNonJtaDataSource() != null ) ) {
				isJTA = info.getJtaDataSource() != null ? Boolean.TRUE : Boolean.FALSE;
				this.setDataSource(
						isJTA ? info.getJtaDataSource() : info.getNonJtaDataSource()
				);
				this.setProperty(
						Environment.CONNECTION_PROVIDER, InjectedDataSourceConnectionProvider.class.getName()
				);
			}
			/*
			 * If explicit type => use it
			 * If a JTA DS is used => JTA transaction,
			 * if a non JTA DS is used => RESOURCe_LOCAL
			 * if none, set to JavaEE default => JTA transaction
			 */
			PersistenceUnitTransactionType transactionType = info.getTransactionType();
			if (transactionType == null) {
				if (isJTA == Boolean.TRUE) {
					transactionType = PersistenceUnitTransactionType.JTA;
				}
				else if ( isJTA == Boolean.FALSE ) {
					transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL;
				}
				else {
					transactionType = PersistenceUnitTransactionType.JTA;
				}
			}
			defineTransactionType( transactionType, workingVars );
			configure( properties, workingVars );
		}
		finally {
			//After EMF, set the CCL back
			if ( ! sameClassLoader ) {
				thread.setContextClassLoader( contextClassLoader );
			}
		}
		return this;
	}

	/**
	 * Processes {@code xmlFiles} argument and populates:<ul>
	 * <li>the {@code entities} list with encountered classnames</li>
	 * <li>the {@code xmlDocuments} list with parsed/validated {@link XmlDocument} corrolary to each xml file</li>
	 * </ul>
	 *
	 * @param xmlFiles The XML resource names; these will be resolved by classpath lookup and parsed/validated.
	 * @param info The PUI
	 * @param entities (output) The names of all encountered "mapped" classes
	 * @param xmlDocuments (output) The list of {@link XmlDocument} instances of each entry in {@code xmlFiles}
	 */
	@SuppressWarnings({ "unchecked" })
	private void addXMLEntities(
			List<String> xmlFiles,
			PersistenceUnitInfo info,
			List<String> entities,
			List<XmlDocument> xmlDocuments) {
		//TODO handle inputstream related hbm files
		ClassLoader classLoaderToUse = info.getNewTempClassLoader();
		if ( classLoaderToUse == null ) {
			log.warn(
					"Persistence provider caller does not implement the EJB3 spec correctly." +
							"PersistenceUnitInfo.getNewTempClassLoader() is null."
			);
			return;
		}
		for ( final String xmlFile : xmlFiles ) {
			final InputStream fileInputStream = classLoaderToUse.getResourceAsStream( xmlFile );
			if ( fileInputStream == null ) {
				log.info( "Unable to resolve mapping file [{}]", xmlFile );
				continue;
			}
			final InputSource inputSource = new InputSource( fileInputStream );

			XmlDocument metadataXml = MappingReader.INSTANCE.readMappingDocument(
					cfg.getEntityResolver(),
					inputSource,
					new OriginImpl( "persistence-unit-info", xmlFile )
			);
			xmlDocuments.add( metadataXml );
			try {
				final Element rootElement = metadataXml.getDocumentTree().getRootElement();
				if ( rootElement != null && "entity-mappings".equals( rootElement.getName() ) ) {
					Element element = rootElement.element( "package" );
					String defaultPackage = element != null ? element.getTextTrim() : null;
					List<Element> elements = rootElement.elements( "entity" );
					for (Element subelement : elements ) {
						String classname = XMLContext.buildSafeClassName( subelement.attributeValue( "class" ), defaultPackage );
						if ( ! entities.contains( classname ) ) {
							entities.add( classname );
						}
					}
					elements = rootElement.elements( "mapped-superclass" );
					for (Element subelement : elements ) {
						String classname = XMLContext.buildSafeClassName( subelement.attributeValue( "class" ), defaultPackage );
						if ( ! entities.contains( classname ) ) {
							entities.add( classname );
						}
					}
					elements = rootElement.elements( "embeddable" );
					for (Element subelement : elements ) {
						String classname = XMLContext.buildSafeClassName( subelement.attributeValue( "class" ), defaultPackage );
						if ( ! entities.contains( classname ) ) {
							entities.add( classname );
						}
					}
				}
				else if ( rootElement != null && "hibernate-mappings".equals( rootElement.getName() ) ) {
					//FIXME include hbm xml entities to enhance them but entities is also used to collect annotated entities
				}
			}
			finally {
				try {
					fileInputStream.close();
				}
				catch (IOException ioe) {
					log.warn( "Could not close input stream", ioe );
				}
			}
		}
		xmlFiles.clear();
	}

	private void defineTransactionType(Object overridenTxType, Map workingVars) {
		if ( overridenTxType == null ) {
//			if ( transactionType == null ) {
//				transactionType = PersistenceUnitTransactionType.JTA; //this is the default value
//			}
			//nothing to override
		}
		else if ( overridenTxType instanceof String ) {
			transactionType = PersistenceXmlLoader.getTransactionType( (String) overridenTxType );
		}
		else if ( overridenTxType instanceof PersistenceUnitTransactionType ) {
			transactionType = (PersistenceUnitTransactionType) overridenTxType;
		}
		else {
			throw new PersistenceException( getExceptionHeader() +
					AvailableSettings.TRANSACTION_TYPE + " of the wrong class type"
							+ ": " + overridenTxType.getClass()
			);
		}

	}

	public Ejb3Configuration setProperty(String key, String value) {
		cfg.setProperty( key, value );
		return this;
	}

	/**
	 * Set ScanningContext detectClasses and detectHbmFiles according to context
	 */
	private void setDetectedArtifactsOnScanningContext(ScanningContext context,
													   Properties properties,
													   Map overridenProperties,
													   boolean excludeIfNotOverriden) {

		boolean detectClasses = false;
		boolean detectHbm = false;
		String detectSetting = overridenProperties != null ?
				(String) overridenProperties.get( AvailableSettings.AUTODETECTION ) :
				null;
		detectSetting = detectSetting == null ?
				properties.getProperty( AvailableSettings.AUTODETECTION) :
				detectSetting;
		if ( detectSetting == null && excludeIfNotOverriden) {
			//not overriden through HibernatePersistence.AUTODETECTION so we comply with the spec excludeUnlistedClasses
			context.detectClasses( false ).detectHbmFiles( false );
			return;
		}

		if ( detectSetting == null){
			detectSetting = "class,hbm";
		}
		StringTokenizer st = new StringTokenizer( detectSetting, ", ", false );
		while ( st.hasMoreElements() ) {
			String element = (String) st.nextElement();
			if ( "class".equalsIgnoreCase( element ) ) detectClasses = true;
			if ( "hbm".equalsIgnoreCase( element ) ) detectHbm = true;
		}
		log.debug( "Detect class: {}; detect hbm: {}", detectClasses, detectHbm );
		context.detectClasses( detectClasses ).detectHbmFiles( detectHbm );
	}

	private void scanForClasses(ScanningContext scanningContext, List<String> packages, List<String> entities, List<NamedInputStream> hbmFiles) {
		if (scanningContext.url == null) {
			log.error( "Container is providing a null PersistenceUnitRootUrl: discovery impossible");
			return;
		}
		try {
			addScannedEntries( scanningContext, entities, packages, hbmFiles, null );
		}
		catch (RuntimeException e) {
			throw new RuntimeException( "error trying to scan <jar-file>: " + scanningContext.url.toString(), e );
		}
		catch( IOException e ) {
			throw new RuntimeException( "Error while reading " + scanningContext.url.toString(), e );
		}
	}

	/**
	 * create a factory from a list of properties and
	 * HibernatePersistence.CLASS_NAMES -> Collection<String> (use to list the classes from config files
	 * HibernatePersistence.PACKAGE_NAMES -> Collection<String> (use to list the mappings from config files
	 * HibernatePersistence.HBXML_FILES -> Collection<InputStream> (input streams of hbm files)
	 * HibernatePersistence.LOADED_CLASSES -> Collection<Class> (list of loaded classes)
	 * <p/>
	 * <b>Used by JBoss AS only</b>
	 * @deprecated use the Java Persistence API
	 */
	// This is used directly by JBoss so don't remove until further notice.  bill@jboss.org
	public EntityManagerFactory createEntityManagerFactory(Map workingVars) {
		Properties props = new Properties();
		if ( workingVars != null ) {
			props.putAll( workingVars );
			//remove huge non String elements for a clean props
			props.remove( AvailableSettings.CLASS_NAMES );
			props.remove( AvailableSettings.PACKAGE_NAMES );
			props.remove( AvailableSettings.HBXML_FILES );
			props.remove( AvailableSettings.LOADED_CLASSES );
		}
		configure( props, workingVars );
		return buildEntityManagerFactory();
	}

	/**
	 * Process configuration and build an EntityManagerFactory <b>when</b> the configuration is ready
	 * @deprecated
	 */
	public EntityManagerFactory createEntityManagerFactory() {
		configure( cfg.getProperties(), new HashMap() );
		return buildEntityManagerFactory();
	}

	public EntityManagerFactory buildEntityManagerFactory() {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			configure( (Properties)null, null );
			NamingHelper.bind(this);
			return new EntityManagerFactoryImpl(
					cfg.buildSessionFactory(),
					transactionType,
					discardOnClose,
					getSessionInterceptorClass( cfg.getProperties() ),
					cfg
			);
		}
		catch (HibernateException e) {
			throw new PersistenceException( getExceptionHeader() + "Unable to build EntityManagerFactory", e );
		}
		finally {
			if (thread != null) {
				thread.setContextClassLoader( contextClassLoader );
			}
		}
	}

	private Class getSessionInterceptorClass(Properties properties) {
		String sessionInterceptorClassname = (String) properties.get( AvailableSettings.SESSION_INTERCEPTOR );
		if ( StringHelper.isNotEmpty( sessionInterceptorClassname ) ) {
			try {
				Class interceptorClass = ReflectHelper.classForName( sessionInterceptorClassname, Ejb3Configuration.class );
				interceptorClass.newInstance();
				return interceptorClass;
			}
			catch (ClassNotFoundException e) {
				throw new PersistenceException( getExceptionHeader() + "Unable to load "
						+ AvailableSettings.SESSION_INTERCEPTOR + ": " + sessionInterceptorClassname, e);
			}
			catch (IllegalAccessException e) {
				throw new PersistenceException( getExceptionHeader() + "Unable to instanciate "
						+ AvailableSettings.SESSION_INTERCEPTOR + ": " + sessionInterceptorClassname, e);
			}
			catch (InstantiationException e) {
				throw new PersistenceException( getExceptionHeader() + "Unable to instanciate "
						+ AvailableSettings.SESSION_INTERCEPTOR + ": " + sessionInterceptorClassname, e);
			}
		}
		else {
			return null;
		}
	}

	public Reference getReference() throws NamingException {
		log.debug("Returning a Reference to the Ejb3Configuration");
		ByteArrayOutputStream stream = new ByteArrayOutputStream();
		ObjectOutput out = null;
		byte[] serialized;
		try {
			out = new ObjectOutputStream( stream );
			out.writeObject( this );
			out.close();
			serialized = stream.toByteArray();
			stream.close();
		}
		catch (IOException e) {
			NamingException namingException = new NamingException( "Unable to serialize Ejb3Configuration" );
			namingException.setRootCause( e );
			throw namingException;
		}

		return new Reference(
				Ejb3Configuration.class.getName(),
				new BinaryRefAddr("object", serialized ),
				Ejb3ConfigurationObjectFactory.class.getName(),
				null
		);
	}

	/**
	 * Configures this configuration object from 2 distinctly different sources.
	 *
	 * @param properties These are the properties that came from the user, either via
	 * a persistence.xml or explicitly passed in to one of our
	 * {@link javax.persistence.spi.PersistenceProvider}/{@link HibernatePersistence} contracts.
	 * @param workingVars Is collection of settings which need to be handled similarly
	 * between the 2 main bootstrap methods, but where the values are determine very differently
	 * by each bootstrap method.  todo eventually make this a contract (class/interface)
	 *
	 * @return The configured configuration
	 *
	 * @see HibernatePersistence
	 */
	private Ejb3Configuration configure(Properties properties, Map workingVars) {
		//TODO check for people calling more than once this method (except buildEMF)
		if (isConfigurationProcessed) return this;
		isConfigurationProcessed = true;
		Properties preparedProperties = prepareProperties( properties, workingVars );
		if ( workingVars == null ) workingVars = CollectionHelper.EMPTY_MAP;

		if ( preparedProperties.containsKey( AvailableSettings.CFG_FILE ) ) {
			String cfgFileName = preparedProperties.getProperty( AvailableSettings.CFG_FILE );
			cfg.configure( cfgFileName );
		}

		cfg.addProperties( preparedProperties ); //persistence.xml has priority over hibernate.cfg.xml

		addClassesToSessionFactory( workingVars );

		//processes specific properties
		List<String> jaccKeys = new ArrayList<String>();


		Interceptor defaultInterceptor = DEFAULT_CONFIGURATION.getInterceptor();
		NamingStrategy defaultNamingStrategy = DEFAULT_CONFIGURATION.getNamingStrategy();

		Iterator propertyIt = preparedProperties.keySet().iterator();
		while ( propertyIt.hasNext() ) {
			Object uncastObject = propertyIt.next();
			//had to be safe
			if ( uncastObject != null && uncastObject instanceof String ) {
				String propertyKey = (String) uncastObject;
				if ( propertyKey.startsWith( AvailableSettings.CLASS_CACHE_PREFIX ) ) {
					setCacheStrategy( propertyKey, preparedProperties, true, workingVars );
				}
				else if ( propertyKey.startsWith( AvailableSettings.COLLECTION_CACHE_PREFIX ) ) {
					setCacheStrategy( propertyKey, preparedProperties, false, workingVars );
				}
				else if ( propertyKey.startsWith( AvailableSettings.JACC_PREFIX )
						&& ! ( propertyKey.equals( AvailableSettings.JACC_CONTEXT_ID )
						|| propertyKey.equals( AvailableSettings.JACC_ENABLED ) ) ) {
					jaccKeys.add( propertyKey );
				}
			}
		}
		final Interceptor interceptor = instantiateCustomClassFromConfiguration(
				preparedProperties,
				defaultInterceptor,
				cfg.getInterceptor(),
				AvailableSettings.INTERCEPTOR,
				"interceptor",
				Interceptor.class
		);
		if ( interceptor != null ) {
			cfg.setInterceptor( interceptor );
		}
		final NamingStrategy namingStrategy = instantiateCustomClassFromConfiguration(
				preparedProperties,
				defaultNamingStrategy,
				cfg.getNamingStrategy(),
				AvailableSettings.NAMING_STRATEGY,
				"naming strategy",
				NamingStrategy.class
		);
		if ( namingStrategy != null ) {
			cfg.setNamingStrategy( namingStrategy );
		}

		final PersisterClassProvider persisterClassProvider = instantiateCustomClassFromConfiguration(
				preparedProperties,
				null,
				cfg.getPersisterClassProvider(),
				AvailableSettings.PERSISTER_CLASS_PROVIDER,
				"persister class provider",
				PersisterClassProvider.class
		);
		if ( persisterClassProvider != null ) {
			cfg.setPersisterClassProvider( persisterClassProvider );
		}

		final SessionFactoryObserver observer = instantiateCustomClassFromConfiguration(
				preparedProperties,
				null,
				cfg.getSessionFactoryObserver(),
				AvailableSettings.SESSION_FACTORY_OBSERVER,
				"SessionFactory observer",
				SessionFactoryObserver.class
		);
		if ( observer != null ) {
			cfg.setSessionFactoryObserver( observer );
		}

		final IdentifierGeneratorStrategyProvider strategyProvider = instantiateCustomClassFromConfiguration(
				preparedProperties,
				null,
				null,
				AvailableSettings.IDENTIFIER_GENERATOR_STRATEGY_PROVIDER,
				"Identifier generator strategy provider",
				IdentifierGeneratorStrategyProvider.class
		);
		if ( strategyProvider != null ) {
			final DefaultIdentifierGeneratorFactory identifierGeneratorFactory = cfg.getIdentifierGeneratorFactory();
			for ( Map.Entry<String,Class<?>> entry : strategyProvider.getStrategies().entrySet() ) {
				identifierGeneratorFactory.register( entry.getKey(), entry.getValue() );
			}
		}

		if ( jaccKeys.size() > 0 ) {
			addSecurity( jaccKeys, preparedProperties, workingVars );
		}

		//initialize listeners
		listenerConfigurator.setProperties( preparedProperties );
		listenerConfigurator.configure();

		//some spec compliance checking
		//TODO centralize that?
		if ( ! "true".equalsIgnoreCase( cfg.getProperty( Environment.AUTOCOMMIT ) ) ) {
			log.warn( "{} = false break the EJB3 specification", Environment.AUTOCOMMIT );
		}
		discardOnClose = preparedProperties.getProperty( AvailableSettings.DISCARD_PC_ON_CLOSE )
				.equals( "true" );
		return this;
	}

	private <T> T instantiateCustomClassFromConfiguration(
			Properties preparedProperties,
			T defaultObject,
			T cfgObject,
			String propertyName,
			String classDescription,
			Class<T> objectClass) {
		if ( preparedProperties.containsKey( propertyName )
				&& ( cfgObject == null || cfgObject.equals( defaultObject ) ) ) {
			//cfg.setXxx has precedence over configuration file
			String className = preparedProperties.getProperty( propertyName );
			try {
				Class<T> clazz = (Class<T>) classForName( className );
				return clazz.newInstance();
				//cfg.setInterceptor( (Interceptor) instance.newInstance() );
			}
			catch (ClassNotFoundException e) {
				throw new PersistenceException(
						getExceptionHeader() + "Unable to find " + classDescription + " class: " + className, e
				);
			}
			catch (IllegalAccessException e) {
				throw new PersistenceException(
						getExceptionHeader() + "Unable to access " + classDescription + " class: " + className, e
				);
			}
			catch (InstantiationException e) {
				throw new PersistenceException(
						getExceptionHeader() + "Unable to instantiate " + classDescription + " class: " + className, e
				);
			}
			catch (ClassCastException e) {
				throw new PersistenceException(
						getExceptionHeader() + classDescription + " class does not implement " + objectClass + " interface: "
								+ className, e
				);
			}
		}
		return null;
	}

	@SuppressWarnings({ "unchecked" })
	private void addClassesToSessionFactory(Map workingVars) {
		if ( workingVars.containsKey( AvailableSettings.CLASS_NAMES ) ) {
			Collection<String> classNames = (Collection<String>) workingVars.get(
					AvailableSettings.CLASS_NAMES
			);
			addNamedAnnotatedClasses( this, classNames, workingVars );
		}

		if ( workingVars.containsKey( PARSED_MAPPING_DOMS ) ) {
			Collection<XmlDocument> xmlDocuments = (Collection<XmlDocument>) workingVars.get( PARSED_MAPPING_DOMS );
			for ( XmlDocument xmlDocument : xmlDocuments ) {
				cfg.add( xmlDocument );
			}
		}

		//TODO apparently only used for Tests, get rid of it?
		if ( workingVars.containsKey( AvailableSettings.LOADED_CLASSES ) ) {
			Collection<Class> classes = (Collection<Class>) workingVars.get( AvailableSettings.LOADED_CLASSES );
			for ( Class clazz : classes ) {
				cfg.addAnnotatedClass( clazz );
			}
		}
		if ( workingVars.containsKey( AvailableSettings.PACKAGE_NAMES ) ) {
			Collection<String> packages = (Collection<String>) workingVars.get(
					AvailableSettings.PACKAGE_NAMES
			);
			for ( String pkg : packages ) {
				cfg.addPackage( pkg );
			}
		}
		if ( workingVars.containsKey( AvailableSettings.XML_FILE_NAMES ) ) {
			Collection<String> xmlFiles = (Collection<String>) workingVars.get(
					AvailableSettings.XML_FILE_NAMES
			);
			for ( String xmlFile : xmlFiles ) {
				Boolean useMetaInf = null;
				try {
					if ( xmlFile.endsWith( META_INF_ORM_XML ) ) useMetaInf = true;
					cfg.addResource( xmlFile );
				}
				catch( MappingNotFoundException e ) {
					if ( ! xmlFile.endsWith( META_INF_ORM_XML ) ) {
						throw new PersistenceException( getExceptionHeader()
								+ "Unable to find XML mapping file in classpath: " + xmlFile);
					}
					else {
						useMetaInf = false;
						//swallow it, the META-INF/orm.xml is optional
					}
				}
				catch( MappingException me ) {
					throw new PersistenceException( getExceptionHeader()
								+ "Error while reading JPA XML file: " + xmlFile, me);
				}
				if ( log.isInfoEnabled() ) {
					if ( Boolean.TRUE.equals( useMetaInf ) ) {
						log.info( "{} {} found", getExceptionHeader(), META_INF_ORM_XML);
					}
					else if (Boolean.FALSE.equals( useMetaInf ) ) {
						log.info( "{} No {} found", getExceptionHeader(), META_INF_ORM_XML);
					}
				}
			}
		}
		if ( workingVars.containsKey( AvailableSettings.HBXML_FILES ) ) {
			Collection<NamedInputStream> hbmXmlFiles = (Collection<NamedInputStream>) workingVars.get(
					AvailableSettings.HBXML_FILES
			);
			for ( NamedInputStream is : hbmXmlFiles ) {
				try {
					//addInputStream has the responsibility to close the stream
					cfg.addInputStream( new BufferedInputStream( is.getStream() ) );
				}
				catch (MappingException me) {
					//try our best to give the file name
					if ( StringHelper.isEmpty( is.getName() ) ) {
						throw me;
					}
					else {
						throw new MappingException("Error while parsing file: " + is.getName(), me );
					}
				}
			}
		}
	}

	private String getExceptionHeader() {
		if ( StringHelper.isNotEmpty( persistenceUnitName ) ) {
			return "[PersistenceUnit: " + persistenceUnitName + "] ";
		}
		else {
			return "";
		}
	}

	private Properties prepareProperties(Properties properties, Map workingVars) {
		Properties preparedProperties = new Properties();

		//defaults different from Hibernate
		preparedProperties.setProperty( Environment.RELEASE_CONNECTIONS, "auto" );
		preparedProperties.setProperty( Environment.JPAQL_STRICT_COMPLIANCE, "true" );
		//settings that always apply to a compliant EJB3
		preparedProperties.setProperty( Environment.AUTOCOMMIT, "true" );
		preparedProperties.setProperty( Environment.USE_IDENTIFIER_ROLLBACK, "false" );
		preparedProperties.setProperty( Environment.FLUSH_BEFORE_COMPLETION, "false" );
		preparedProperties.setProperty( AvailableSettings.DISCARD_PC_ON_CLOSE, "false" );
		if (cfgXmlResource != null) {
			preparedProperties.setProperty( AvailableSettings.CFG_FILE, cfgXmlResource );
			cfgXmlResource = null;
		}

		//override the new defaults with the user defined ones
		//copy programmatically defined properties
		if ( cfg.getProperties() != null ) preparedProperties.putAll( cfg.getProperties() );
		//copy them coping from configuration
		if ( properties != null ) preparedProperties.putAll( properties );
		//note we don't copy cfg.xml properties, since they have to be overriden

		if (transactionType == null) {
			//if it has not been set, the user use a programmatic way
			transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL;
		}
		defineTransactionType(
				preparedProperties.getProperty( AvailableSettings.TRANSACTION_TYPE ),
				workingVars
		);
		boolean hasTxStrategy = StringHelper.isNotEmpty(
				preparedProperties.getProperty( Environment.TRANSACTION_STRATEGY )
		);
		if ( ! hasTxStrategy && transactionType == PersistenceUnitTransactionType.JTA ) {
			preparedProperties.setProperty(
					Environment.TRANSACTION_STRATEGY, JoinableCMTTransactionFactory.class.getName()
			);
		} //else if RESOURCE_LOCAL, let the default being used.
		if ( hasTxStrategy ) {
			log.warn(
					"Overriding {} is dangerous, this might break the EJB3 specification implementation",
					Environment.TRANSACTION_STRATEGY
			);
		}
		if ( preparedProperties.getProperty( Environment.FLUSH_BEFORE_COMPLETION ).equals( "true" ) ) {
			preparedProperties.setProperty( Environment.FLUSH_BEFORE_COMPLETION, "false" );
			log.warn( "Defining {}=true ignored in HEM", Environment.FLUSH_BEFORE_COMPLETION );
		}
		return preparedProperties;
	}

	private Class classForName(String className) throws ClassNotFoundException {
		return ReflectHelper.classForName( className, this.getClass() );
	}

	private void setCacheStrategy(String propertyKey, Map properties, boolean isClass, Map workingVars) {
		String role = propertyKey.substring(
				( isClass ? AvailableSettings.CLASS_CACHE_PREFIX
						.length() : AvailableSettings.COLLECTION_CACHE_PREFIX.length() )
						+ 1
		);
		//dot size added
		String value = (String) properties.get( propertyKey );
		StringTokenizer params = new StringTokenizer( value, ";, " );
		if ( !params.hasMoreTokens() ) {
			StringBuilder error = new StringBuilder( "Illegal usage of " );
			error.append(
					isClass ? AvailableSettings.CLASS_CACHE_PREFIX : AvailableSettings.COLLECTION_CACHE_PREFIX
			);
			error.append( ": " ).append( propertyKey ).append( " " ).append( value );
			throw new PersistenceException( getExceptionHeader() + error.toString() );
		}
		String usage = params.nextToken();
		String region = null;
		if ( params.hasMoreTokens() ) {
			region = params.nextToken();
		}
		if ( isClass ) {
			boolean lazyProperty = true;
			if ( params.hasMoreTokens() ) {
				lazyProperty = "all".equalsIgnoreCase( params.nextToken() );
			}
			cfg.setCacheConcurrencyStrategy( role, usage, region, lazyProperty );
		}
		else {
			cfg.setCollectionCacheConcurrencyStrategy( role, usage, region );
		}
	}

	private void addSecurity(List<String> keys, Map properties, Map workingVars) {
		log.debug( "Adding security" );
		if ( !properties.containsKey( AvailableSettings.JACC_CONTEXT_ID ) ) {
			throw new PersistenceException( getExceptionHeader() +
					"Entities have been configured for JACC, but "
							+ AvailableSettings.JACC_CONTEXT_ID
							+ " has not been set"
			);
		}
		String contextId = (String) properties.get( AvailableSettings.JACC_CONTEXT_ID );
		setProperty( Environment.JACC_CONTEXTID, contextId );

		int roleStart = AvailableSettings.JACC_PREFIX.length() + 1;

		for ( String key : keys ) {
			JACCConfiguration jaccCfg = new JACCConfiguration( contextId );
			try {
				String role = key.substring( roleStart, key.indexOf( '.', roleStart ) );
				int classStart = roleStart + role.length() + 1;
				String clazz = key.substring( classStart, key.length() );
				String actions = (String) properties.get( key );
				jaccCfg.addPermission( role, clazz, actions );
			}
			catch (IndexOutOfBoundsException e) {
				throw new PersistenceException( getExceptionHeader() +
						"Illegal usage of " + AvailableSettings.JACC_PREFIX + ": " + key );
			}
		}
	}

	private void addNamedAnnotatedClasses(
			Ejb3Configuration cfg, Collection<String> classNames, Map workingVars
	) {
		for ( String name : classNames ) {
			try {
				Class clazz = classForName( name );
				cfg.addAnnotatedClass( clazz );
			}
			catch (ClassNotFoundException cnfe) {
				Package pkg;
				try {
					pkg = classForName( name + ".package-info" ).getPackage();
				}
				catch (ClassNotFoundException e) {
					pkg = null;
				}
				if ( pkg == null ) {
					throw new PersistenceException( getExceptionHeader() +  "class or package not found", cnfe );
				}
				else {
					cfg.addPackage( name );
				}
			}
		}
	}


	public Settings buildSettings() throws HibernateException {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			return settingsFactory.buildSettings( cfg.getProperties() );
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public Ejb3Configuration addProperties(Properties props) {
		cfg.addProperties( props );
		return this;
	}

	public Ejb3Configuration addAnnotatedClass(Class persistentClass) throws MappingException {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			cfg.addAnnotatedClass( persistentClass );
			return this;
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public Ejb3Configuration configure(String resource) throws HibernateException {
		//delay the call to configure to allow proper addition of all annotated classes (EJB-330)
		if (cfgXmlResource != null)
			throw new PersistenceException("configure(String) method already called for " + cfgXmlResource);
		this.cfgXmlResource = resource;
		return this;
	}

	public Ejb3Configuration addPackage(String packageName) throws MappingException {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			cfg.addPackage( packageName );
			return this;
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public Ejb3Configuration addFile(String xmlFile) throws MappingException {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			cfg.addFile( xmlFile );
			return this;
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public Ejb3Configuration addClass(Class persistentClass) throws MappingException {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			cfg.addClass( persistentClass );
			return this;
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public Ejb3Configuration addFile(File xmlFile) throws MappingException {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			cfg.addFile( xmlFile );
			return this;
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public void buildMappings() {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			cfg.buildMappings();
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public Iterator getClassMappings() {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			return cfg.getClassMappings();
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public EventListeners getEventListeners() {
		return cfg.getEventListeners();
	}

	SessionFactory buildSessionFactory() throws HibernateException {
		return cfg.buildSessionFactory();
	}

	public Iterator getTableMappings() {
		return cfg.getTableMappings();
	}

	public PersistentClass getClassMapping(String persistentClass) {
		return cfg.getClassMapping( persistentClass );
	}

	public org.hibernate.mapping.Collection getCollectionMapping(String role) {
		return cfg.getCollectionMapping( role );
	}

	public void setEntityResolver(EntityResolver entityResolver) {
		cfg.setEntityResolver( entityResolver );
	}

	public Map getNamedQueries() {
		return cfg.getNamedQueries();
	}

	public Interceptor getInterceptor() {
		return cfg.getInterceptor();
	}

	public Properties getProperties() {
		return cfg.getProperties();
	}

	public Ejb3Configuration setInterceptor(Interceptor interceptor) {
		cfg.setInterceptor( interceptor );
		return this;
	}

	public Ejb3Configuration setProperties(Properties properties) {
		cfg.setProperties( properties );
		return this;
	}

	public Map getFilterDefinitions() {
		return cfg.getFilterDefinitions();
	}

	public void addFilterDefinition(FilterDefinition definition) {
		cfg.addFilterDefinition( definition );
	}

	public void addAuxiliaryDatabaseObject(AuxiliaryDatabaseObject object) {
		cfg.addAuxiliaryDatabaseObject( object );
	}

	public NamingStrategy getNamingStrategy() {
		return cfg.getNamingStrategy();
	}

	public Ejb3Configuration setNamingStrategy(NamingStrategy namingStrategy) {
		cfg.setNamingStrategy( namingStrategy );
		return this;
	}

	public Ejb3Configuration setPersisterClassProvider(PersisterClassProvider persisterClassProvider) {
		cfg.setPersisterClassProvider( persisterClassProvider );
		return this;
	}

	public Ejb3Configuration setSessionFactoryObserver(SessionFactoryObserver observer) {
		cfg.setSessionFactoryObserver( observer );
		return this;
	}

	public void setListeners(String type, String[] listenerClasses) {
		cfg.setListeners( type, listenerClasses );
	}

	public void setListeners(String type, Object[] listeners) {
		cfg.setListeners( type, listeners );
	}

	/**
	 * This API is intended to give a read-only configuration.
	 * It is sueful when working with SchemaExport or any Configuration based
	 * tool.
	 * DO NOT update configuration through it.
	 */
	public AnnotationConfiguration getHibernateConfiguration() {
		//TODO make it really read only (maybe through proxying)
		return cfg;
	}

	public Ejb3Configuration addInputStream(InputStream xmlInputStream) throws MappingException {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			cfg.addInputStream( xmlInputStream );
			return this;
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public Ejb3Configuration addResource(String path) throws MappingException {
		Thread thread = null;
		ClassLoader contextClassLoader = null;
		if (overridenClassLoader != null) {
			thread = Thread.currentThread();
			contextClassLoader = thread.getContextClassLoader();
			thread.setContextClassLoader( overridenClassLoader );
		}
		try {
			cfg.addResource( path );
			return this;
		}
		finally {
			if (thread != null) thread.setContextClassLoader( contextClassLoader );
		}
	}

	public Ejb3Configuration addResource(String path, ClassLoader classLoader) throws MappingException {
		cfg.addResource( path, classLoader );
		return this;
	}

	private enum XML_SEARCH {
		HBM,
		ORM_XML,
		BOTH,
		NONE;

		public static XML_SEARCH getType(boolean searchHbm, boolean searchOrm) {
			return searchHbm ?
					searchOrm ? XML_SEARCH.BOTH : XML_SEARCH.HBM :
					searchOrm ? XML_SEARCH.ORM_XML : XML_SEARCH.NONE;
		}
	}
}
