Jump to: navigation, search

EclipseLink/Development/OSGi/ServiceProposal

< EclipseLink‎ | Development‎ | OSGi
Revision as of 12:05, 24 June 2008 by Shaun.smith.oracle.com (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Proposed Persistence Service and Extensions

This is an example of an Eclipse (RCP) application for a library. The first part of the example is from the point of view of the application developer. The second part is what goes on behind the scenes to make it all work.

Part 1: The Developer

The domain consists of one class: Book which is defined in the plugin org.acme.library.

package org.acme.library.entity;
 
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
public class Book
{
  @Id
  @Generated
  private long id;
  private String title;
  private String abstract;
  ...
}

The entity is declared as an extension in the plugin.xml (look Ma', no persistence.xml)

<plugin>
   <extension point="org.eclipse.persistence.jpa.persistenceContext">
      <context name="org.acme.library">
         <entity class="org.acme.library.entity.Book"/>
      </context>
   </extension>
</plugin>

Here is how a Book would be accessed using the proposed persistence service

IPersistenceService persistenceService = Activator().getDefault().getPersistenceService();
EntityManager em = persistenceService.createEntityManager("org.acme.library");
Book book = em.find(Book.class, 4368);
em.close();

Now, consider the library expanding and adding audio books. The class AudioBook is defined in the plugin org.acme.library.audio.

package org.acme.library.entity;
 
import org.acme.library.Book;
 
@Entity
public class AudioBook entends Book
{
  private boolean abridged;
  private String storyteller;
  ...
}

The entity is declared as an extension in the plugin.xml

<plugin>
   <extension point="org.eclipse.persistence.jpa.persistenceContext">
      <context name="org.acme.library">
         <entity class="org.acme.library.audio.entity.AudioBook"/>
      </context>
   </extension>
</plugin>

At runtime, the declared entities from the two plugins are combined into a single persistence context.

IPersistenceService persistenceService = Activator().getDefault().getPersistenceService();
EntityManager em = persistenceService.createEntityManager("org.acme.library");
Book book = em.find(Book.class, 4368);
AudioBook audioBook = em.find(AudioBook.class, 148905);
em.close();

Part 2: The Black Magic

When the persistence service starts, it loads various extensions. The first is loading of the IPersistenceConfigurationFactory. The factory takes the database connection information and creates a mapping of parameters specific to the JPA provider. My current implementation only allows for a single provider to be active for the application. To support multiple providers in the same application, the factories could be associated with some sort of provider id.

public interface IPersistenceConfigurationFactory
{
  Map<String, String> createConfiguration(DatabaseConnectionInfo connectionInfo);
}

Here is the code for the Hibernate provider. This factory only support MySQL and DB2. Some additional work is needed to make database support generic.

public class PersistenceConfigurationFactory implements IPersistenceConfigurationFactory
{
  public Map<String, String> createConfiguration(DatabaseConnectionInfo connectionInfo)
  {
    StringBuilder uri = new StringBuilder();
    uri.append("jdbc:");
    uri.append(connectionInfo.getType());
    uri.append("://");
    uri.append(connectionInfo.getServer());
 
    if(connectionInfo.getPort() > 0)
    {
      uri.append(':');
      uri.append(connectionInfo.getPort());
    }
 
   uri.append('/');
 
    if(connectionInfo.getDatabase() != null)
      uri.append(connectionInfo.getDatabase());
 
    final HashMap<String, String> config = new HashMap<String, String>();
    config.put("hibernate.dialect", getDialect(connectionInfo.getType()));
    config.put("hibernate.connection.driver_class", getDriver(connectionInfo.getType()));
    config.put("hibernate.connection.url", uri.toString());
    config.put("hibernate.connection.username", connectionInfo.getUser());
    config.put("hibernate.connection.password", connectionInfo.getPassword());
 
    return config;
  }
 
  private String getDialect(String type)
  {
    if (type.equals("mysql"))
      return "org.hibernate.dialect.MySQLDialect";
    else
      return "org.hibernate.dialect.DB2Dialect";
  }
 
  private String getDriver(String type)
  {
    // TODO better driver support
    if (type.equals("mysql"))
      return "com.mysql.jdbc.Driver";
    else
      return "com.ibm.db2.jcc.DB2Driver";
  }
}

Here is the declaration of the extension:

  <extension point="com.ibm.hdwb.core.persistence.ejb.configurationFactory">
    <factory class="com.ibm.hdwb.core.internal.persistence.ejb.hibernate.PersistenceConfigurationFactory">
    </factory>
  </extension>

The second extension point loaded is the declaration of the entities. The class names of the entities are stored in a map keyed on the persistence context.

private final HashMap<String, HashSet<String>> entities;

There is a third extension point that maps the persistence context to a database URI. These entries are stored in a map keyed on the persistence context.

private final HashMap<String, URI> configuration;

When a client requests an EntityManager, the context name is used to locate the EntityManagerFactory from the cache of factories. If the factory is found in the cache, it is used to create and return the EntityManger. If the factory does not yet exist, The persistence context name is used to locate the database URI from the configuration. From the database URI, the DatabaseConnectionInfo is obtained via an extension point. Once the DatabaseConnectionInfo is available, the IPersistenceConfigurationFactory is used to create the JPA provider specific configuration info for the persistence context. From the persistence context configuration, a PersistenceFactory is created that is a proxy for the EntityManagerFactory. The PersistenceFactory is responsible for working around context classloader issues (present in Hibernate) and creating the persistence.xml.

	public EntityManager createEntityManager(String instance, IProgressMonitor monitor, Map<String, String> config) throws PersistenceServiceException
	{
		monitor.beginTask("Connect to database", 4);
 
		PersistenceFactory factory = factories.get(instance);
		monitor.worked(1);
 
		if (factory == null)
		{
			if (entities.containsKey(instance))
			{
				final Map<String, String> conf = createConfiguration(instance);
 
				if(conf == null)
				{
					final Status status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, PersistenceService.STATUS_ERROR, "The datasource for the instance '" + instance + "' could not be found", null);
					Activator.getDefault().getLog().log(status);
					throw new PersistenceServiceException(status);					
				}
 
				conf.putAll(config);
				factory = new PersistenceFactory(instance, entities.get(instance), conf);
				factories.put(instance, factory);
			}
			else
			{
				final Status status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, PersistenceService.STATUS_ERROR, "The persistence context instance '" + instance + "' could not be found", null);
				Activator.getDefault().getLog().log(status);
				throw new PersistenceServiceException(status);
			}
		}
 
		monitor.worked(1);
		factory.connectToDatabase();
		monitor.worked(1);
 
		return factory.createEntityManager();
	}
 
	private Map<String, String> createConfiguration(String persistenceContext)
	{
		final URI datasource = configuration.get(persistenceContext);
 
		if (datasource == null)
			return null;
 
		final DatabaseConnectionInfo connectionInfo = Activator.getDefault().getConnectionService().getConnectionInfo(datasource);
		return configurationFactory.createConfiguration(connectionInfo);
	}
public class PersistenceFactory
{
	public PersistenceFactory(String context, Set<String> entities, Map<String, String> properties)
	{
		active = false;
		persistenceContext = context;
		contextProperties = properties;
		this.entities = entities;
	}
 
	public synchronized void connectToDatabase() throws PersistenceServiceException
	{
		if (factory != null)
			return;
 
		try
		{
			final ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
			final ClassLoader parentLoader = PersistenceActivator.class.getClassLoader();
			final PersistenceClassLoader newLoader = new PersistenceClassLoader(parentLoader, getPersistenceXML());
			Thread.currentThread().setContextClassLoader(newLoader);
			factory = Persistence.createEntityManagerFactory(persistenceContext, contextProperties);
			Thread.currentThread().setContextClassLoader(oldLoader);
			active = true;
		}
		catch (final Exception e)
		{
			final Status status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, PersistenceService.STATUS_ERROR, "Failed to construct EntityManagerFactory for persistence context: '" + persistenceContext
					+ "'", e);
			Activator.getDefault().getLog().log(status);
			throw new PersistenceServiceException(status);
		}
	}
 
	public EntityManager createEntityManager() throws PersistenceServiceException
	{
		if (factory == null)
		{
			final Status status = new Status(IStatus.ERROR, Activator.PLUGIN_ID, PersistenceService.STATUS_ERROR, "Attempted to access the persistence context: '" + persistenceContext
					+ "' when it is off-line", null);
			Activator.getDefault().getLog().log(status);
			throw new PersistenceServiceException(status);
		}
 
		return factory.createEntityManager();
	}
 
	public synchronized void disconnectFromDatabase()
	{
		if (factory == null)
			return;
 
		factory.close();
		factory = null;
	}
 
	public String getPersistenceContext()
	{
		return persistenceContext;
	}
 
	public byte[] getPersistenceXML()
	{
		final StringBuilder xml = new StringBuilder();
		xml.append("<persistence>");
		xml.append(System.getProperty("line.separator"));
 
		xml.append("<persistence-unit name=\"");
		xml.append(persistenceContext);
		xml.append("\" transaction-type=\"RESOURCE_LOCAL\">");
		xml.append(System.getProperty("line.separator"));
 
		for (final String entity : entities)
		{
			xml.append("<class>");
			xml.append(entity);
			xml.append("</class>");
			xml.append(System.getProperty("line.separator"));
		}
 
		xml.append("</persistence-unit>");
 
		xml.append(System.getProperty("line.separator"));
		xml.append("</persistence>");
 
		return xml.toString().getBytes();
	}
 
	public boolean isActive()
	{
		return active;
	}
 
	Map<String, String> contextProperties;
	Set<String> entities;
	private boolean active;
	private final String persistenceContext;
	private EntityManagerFactory factory;
}

The loading of the persistence.xml is handled via a specialized ClassLoader, URLStreamHandler, and URLConnection:

public class PersistenceClassLoader extends ClassLoader
{
	PersistenceClassLoader(ClassLoader delegate, byte[] xmlData)
	{
		super(delegate);
 
		urls = new Vector<URL>();
 
		try
		{
			urls.add(new URL("bundleresource", Long.toString(Activator.getDefault().getBundle().getBundleId()), -1, "/META-INF/persistence.xml", new Handler(xmlData)));
		}
		catch (final MalformedURLException e)
		{
			e.printStackTrace();
		}
	}
 
	@Override
	public Enumeration<URL> getResources(String name) throws IOException
	{
		if (name.equals("META-INF/persistence.xml"))
			return urls.elements();
 
		return super.getResources(name);
	}
 
	Vector<URL> urls;
}
 
public class Handler extends URLStreamHandler
{
	public Handler(byte[] xmlData)
	{
		this.xmlData = xmlData;
	}
 
	@Override
	protected URLConnection openConnection(URL arg0) throws IOException
	{
		return new PersistenceURLConnection(arg0, xmlData);
	}
 
	byte[] xmlData;
}
 
public class PersistenceURLConnection extends URLConnection
{
	protected PersistenceURLConnection(URL arg0, byte[] xmlData)
	{
		super(arg0);
		this.xmlData = xmlData;
	}
 
	@Override
	public void connect() throws IOException
	{
	}
 
	@Override
	public InputStream getInputStream() throws IOException
	{
		final ByteArrayInputStream stream = new ByteArrayInputStream(xmlData);
		return stream;
	}
 
	private byte[] xmlData;
}