Set JPA entities at runtime

We use JPA as persistence mechanism for EPoS. The JPA implementation for the next release will be EclipseLink and I was adding that over the weekend. One important thing: I don’t know the entity classes at compile time. That makes it impossible to add the class names to the persistence.xml directly. Somehow we need a way to specify the classes at runtime (well, at least at initialization time, when we boto up the EntityManagerFactory). That is something taht seems to be not part of the JPA spec for Java SE environments ( in Java EE, you can let it search the jar files for entity classes).

Ok, so we need a way to manipulate the content of the persistence.xml at runtime to add the class entries and I found way, hacking into the mechanism that reads the content of the persistence.xml.
Basically, what we do is, we occupy the stream that represents the file and manipulate it at runtime. To do so, we have to extend the initialization process, but, thanks to protected variables used by the EclipseLink team, this works without any manipulation of the EclipseLink source code. We have to extend some classes, to make it work, but thats it. And it turned out that the changes are rather minimal and the hack works straight forward. So, lets jump into the code and get things running. Still, keep in mind, we work on top of internal initialization code ! There is a good chance that this breaks with a new release, although I think it will be straight forward to keep things running…at least as long as the EclipseLink team sticks to protected access modifiers for some of their variables.

The interface we have to implement is org.eclipse.persistence.internal.jpa.deployment.Archive. This defines the access mechanism used by EclipseLink and the implementations are used to access the content of the persistence.xml via the getEntry method:

/**
* Returns the InputStream for the given entry name. Returns null if no such
* entry exists. The entry name must be relative to the root of the module.
*
* @param entryPath the file name relative to the root of the module.
* @return the InputStream for the given entry name or null if not found.
*/
InputStream getEntry(String entryPath) throws IOException;

Well, we don’t want to provide a complete implementation, we just want to know when the persistence.xml is accessed. Therefore we implemented a WrapperArchive that delegates to a parent archive and checks for access to persistence.xml.

...
public InputStream getEntry(String entryPath) throws IOException {
    if (entryPath.equals("META-INF/persistence.xml")) {
        return createPersitenceString(parent
                .getEntry("META-INF/persistence.xml"));
    }
    return parent.getEntry(entryPath);
}
...
protected InputStream createPersitenceString(InputStream in) {
    BufferedReader bi = new BufferedReader(new InputStreamReader(in));
    StringBuffer b = new StringBuffer();
    String l = null;
    try {
        l = bi.readLine();
        while (l != null) {
            if (l.trim().startsWith("")) {
                if (entityCalsses != null) {
                    for (String clazz : entityCalsses) {
                        b.append("");
                        b.append(clazz);
                        b.append("");
                        b.append("\n");
                    }
                }
            } else {
                b.append(l);
            }
            l = bi.readLine();
        }
    } catch (IOException e) {
        Logger.getLogger(getClass()).error("Error while creating dynamic persistence.xml", e);
    }
    return new ByteArrayInputStream(b.toString().getBytes());
}
...

All other methods delegate directly to the parent archive and we intercept access to META-INF/persistence.xml. If someone tries to get a stream to the persistence.xml, we intercept that call, read the original stream, manipulate it and return the changed content. Well, we just look for <!–$$CLASSES$$–> somewhere in the original, and replace that with the class definitions. I know, I know, there are better ways, but this works for now.
Now, all we have to do is make sure that the WrapperArchive is used. To achieve this, we extend the PersistenceProvider:

public class DynamicPersistenceProvider extends PersistenceProvider{
    private DynamicPersistenceProvider() {
        super();
        initializationHelper = new DynamicInitializationHelper();
    }
    public DynamicPersistenceProvider(String[] classes) {
        this();
        WrapperArchive.setEntityCalsses(Arrays.asList(classes));
    }
}

The dynamic provider is initialized with the set of entity classes and we can use the openness of the EclipseLink implementation (thanks for using protected variables !!!). Internally they use an initialization helper that is used to get access to a JPAInitializer that has a method initialize(Map, PersistenceInitializerHelper) that creates the archives. To get that in, we implement the a DynamicInitializationHelper that extends PersistenceInitializationHelper:

public class DynamicInitializationHelper extends PersistenceInitializationHelper{
    public JPAInitializer getInitializer(ClassLoader classLoader, Map m) {
        return DynamicJavaSECMPInitializer.getJavaSECMPInitializer();
    }
}

This returns our own JPAInitializer

public class DynamicJavaSECMPInitializer extends JavaSECMPInitializer {
    public static synchronized DynamicJavaSECMPInitializer getJavaSECMPInitializer() {
        if (javaSECMPInitializer == null || !(javaSECMPInitializer instanceof DynamicJavaSECMPInitializer)) {
           javaSECMPInitializer = new DynamicJavaSECMPInitializer();
        }
        initializeTopLinkLoggingFile();
        return (DynamicJavaSECMPInitializer) javaSECMPInitializer;
    }
    public void initialize(Map m, PersistenceInitializationHelper persistenceHelper) {
        initializeClassLoader(persistenceHelper);
        final Set pars = PersistenceUnitProcessor.findPersistenceArchives(initializationClassloader);
        for (Archive archive: pars) {
            AbstractSessionLog.getLog().log(SessionLog.FINER, "cmp_init_initialize", archive);
            initPersistenceUnits(WrapperArchive.createWrapperArchive(archive), m, persistenceHelper);
        }

    }
}

where we override the initialize method. The content is copy pasted from the original implementation except that we make sure that we create the WrapperArchives here. Also, again, thanks to the protected variables, we change the static reference to the JavaSECMPInitializer singleton to our dynamic version.

At the end of the day its not much code to achieve the runtime manipulation and we can do something like this

...
String[] classes = new String[]{Person.class.getName()};
EntityManagerFactory emf = new DynamicPersistenceProvider(classes)
    .createEntityManagerFactory(persistenceUnitName, loadConfiguration());

and end up with a persistence unit that contains a Person entity.
There is only one little drawback that I discovered so far. When using dynamic weaving ( -javaagent:eclipselink.jar ), the JavaSECMPInitializer singleton seems to be initialized somewhere else. My workaround for now, I close the EntityManagerFactory immediately after the first creation and recreate a new one. That way lazy loading works and I am happy for the moment. I have to digg a bit deeper to find out why we need to do that, but for the moment I am happy :-)

Here is the source code.

Tags: , ,

Monday, February 9th, 2009 Other

4 Comments to Set JPA entities at runtime

  1. Did you try setting the persistence.xml property false? That setting should work in SE in EclipseLink as well.
    –Gordon

  2. Gordon Yorke on February 9th, 2009
  3. It seems I should not use xml syntax in the comments. The above comment should read:
    Did you try setting the persistence.xml property “exclude-unlisted-classes” to false? This setting should work in SE mode as well.
    –Gordon

  4. Gordon Yorke on February 9th, 2009
  5. Gordon,

    yes, I tried that, but it didn’t work (eclipselink 1.0.2). And even if it would work, in this project, classes in classpath might be marked with @Entity but not registered in the environment that I am using, so I need more control about which classes being treated as entities at initialization time.

    Thasso

  6. Thasso on February 9th, 2009
  7. Hi Thasso,

    I spent almost a whole day for struggling with this issue, and yours seems the only working solution; but I could not managed to compile your code in my environment.

    I use EclipseLink 2.3.2 version (2.3.2.v20111125-r10461 specifically) and classes you depend in the code seem have changed. It has been a long time since you write the blog but I’d like to ask whether if you could find a more generic way or do you have any idea how can I do the same in my version.

    Thank you.

  8. Ender on March 19th, 2012

Leave a comment