blob: 8f6ef99ac58be1eb03066a04cb3f88b2849907fa [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.openjpa.enhance;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.BytecodeWriter;
import org.apache.openjpa.lib.util.Files;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.Localizer.Message;
import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.Exceptions;
import org.apache.openjpa.util.GeneratedClasses;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.MetaDataException;
import org.apache.openjpa.util.UserException;
import serp.bytecode.BCClass;
/**
* Redefines the method bodies of existing unenhanced classes to make them
* notify state managers of mutations.
*
* @since 1.0.0
*/
public class ManagedClassSubclasser {
private static final Localizer _loc = Localizer.forPackage(
ManagedClassSubclasser.class);
/**
* For each element in <code>classes</code>, creates and registers a
* new subclass that implements {@link PersistenceCapable}, and prepares
* OpenJPA to handle new instances of the unenhanced type. If this is
* invoked in a Java 6 environment, this method will redefine the methods
* for each class in the argument list such that field accesses are
* intercepted in-line. If invoked in a Java 5 environment, this
* redefinition is not possible; in these contexts, when using field
* access, OpenJPA will need to do state comparisons to detect any change
* to any instance at any time, and when using property access, OpenJPA
* will need to do state comparisons to detect changes to newly inserted
* instances after a flush has been called.
*
* @return the new subclasses, or <code>null</code> if <code>classes</code>
* is <code>null</code>.
* @throws UserException if <code>conf</code> requires build-time
* enhancement and <code>classes</code> includes unenhanced types.
*
* @since 1.0.0
*/
public static List<Class<?>> prepareUnenhancedClasses(
final OpenJPAConfiguration conf,
final Collection<? extends Class<?>> classes,
final ClassLoader envLoader) {
if (classes == null)
return null;
if (classes.size() == 0)
return Collections.emptyList();
Log log = conf.getLog(OpenJPAConfiguration.LOG_ENHANCE);
if (conf.getRuntimeUnenhancedClassesConstant() != RuntimeUnenhancedClassesModes.SUPPORTED) {
Collection<Class<?>> unenhanced = new ArrayList<>();
for (Class<?> cls : classes)
if (!PersistenceCapable.class.isAssignableFrom(cls))
unenhanced.add(cls);
if (unenhanced.size() > 0) {
if (PCEnhancerAgent.getLoadSuccessful()) {
// This means that the enhancer has been ran but we
// have some unenhanced classes. This can happen if an
// entity is loaded by the JVM before the EntityManger
// was created. Warn the user.
if (log.isWarnEnabled()) {
log.warn(_loc.get("entities-loaded-before-em"));
}
if (log.isTraceEnabled()) {
log.trace(ManagedClassSubclasser.class.getName()
+ ".prepareUnenhancedClasses()"
+ " - The following classes are unenhanced "
+ unenhanced.toString());
}
}
Message msg = _loc.get("runtime-optimization-disabled", Exceptions.toClassNames(unenhanced));
if (conf.getRuntimeUnenhancedClassesConstant() == RuntimeUnenhancedClassesModes.WARN) {
log.warn(msg);
} else {
throw new UserException(msg);
}
}
return null;
}
boolean redefine = ClassRedefiner.canRedefineClasses(log);
if (redefine) {
log.info(_loc.get("enhance-and-subclass-and-redef-start", classes));
} else {
log.warn(_loc.get("enhance-and-subclass-no-redef-start", classes));
}
final Map<Class<?>, byte[]> map = new HashMap<>();
final List<Class<?>> subs = new ArrayList<>(classes.size());
final List<Class<?>> ints = new ArrayList<>(classes.size());
Set<Class<?>> unspecified = null;
for (Class<?> cls : classes) {
final Class<?> c = cls;
final PCEnhancer enhancer = new PCEnhancer(conf, cls);
enhancer.setBytecodeWriter(new BytecodeWriter() {
@Override
public void write(BCClass bc) throws IOException {
ManagedClassSubclasser.write(bc, enhancer, map, c, subs, ints);
}
});
if (redefine) {
enhancer.setRedefine(true);
}
// we need to create subclasses because class retransform doesn't allow to change the interfaces of a previously loaded class
enhancer.setCreateSubclass(true);
enhancer.setAddDefaultConstructor(true);
// set this before enhancement as well as after since enhancement
// uses a different metadata repository, and the metadata config
// matters in the enhancement contract. In order to avoid a
// NullPointerException, check for no metadata and throw an
// exception if none exists. Otherwise, don't do any warning here,
// since we'll issue warnings when we do the final metadata
// reconfiguration at the end of this method.
ClassMetaData meta = enhancer.getMetaData();
if (meta == null) {
throw new MetaDataException(_loc.get("no-meta", cls)).setFatal(true);
}
configureMetaData(meta, conf, redefine, false);
unspecified = collectRelatedUnspecifiedTypes(enhancer.getMetaData(), classes, unspecified);
int runResult = enhancer.run();
if (runResult == PCEnhancer.ENHANCE_PC) {
try {
enhancer.record();
} catch (IOException e) {
// our impl of BytecodeWriter doesn't throw IOException
throw new InternalException(e);
}
}
}
if (unspecified != null && !unspecified.isEmpty())
throw new UserException(_loc.get("unspecified-unenhanced-types", Exceptions.toClassNames(classes),
unspecified));
ClassRedefiner.redefineClasses(conf, map);
for (Class<?> cls : map.keySet()) {
setIntercepting(conf, envLoader, cls);
configureMetaData(conf, envLoader, cls, redefine);
}
for (Class<?> cls : subs)
configureMetaData(conf, envLoader, cls, redefine);
for (Class<?> cls : ints)
setIntercepting(conf, envLoader, cls);
return subs;
}
private static Set<Class<?>> collectRelatedUnspecifiedTypes(ClassMetaData meta,
Collection<? extends Class<?>> classes, Set<Class<?>> unspecified) {
unspecified = collectUnspecifiedType(meta.getPCSuperclass(), classes,
unspecified);
for (FieldMetaData fmd : meta.getFields()) {
if (fmd.isTransient())
continue;
if (fmd.isTypePC())
unspecified = collectUnspecifiedType(fmd.getType(), classes,
unspecified);
if (fmd.getElement() != null && fmd.getElement().isTypePC())
unspecified = collectUnspecifiedType(fmd.getElement().getType(),
classes, unspecified);
if (fmd.getKey() != null && fmd.getKey().isTypePC())
unspecified = collectUnspecifiedType(fmd.getKey().getType(),
classes, unspecified);
if (fmd.getValue() != null && fmd.getValue().isTypePC())
unspecified = collectUnspecifiedType(fmd.getValue().getType(),
classes, unspecified);
}
return unspecified;
}
private static Set<Class<?>> collectUnspecifiedType(Class<?> cls,
Collection<? extends Class<?>> classes, Set<Class<?>> unspecified) {
if (cls != null && !classes.contains(cls)
&& !ImplHelper.isManagedType(null, cls)
&& !cls.isInterface()) {
if (unspecified == null)
unspecified = new HashSet<>();
unspecified.add(cls);
}
return unspecified;
}
private static void configureMetaData(OpenJPAConfiguration conf,
ClassLoader envLoader, Class<?> cls, boolean redefineAvailable) {
ClassMetaData meta = conf.getMetaDataRepositoryInstance()
.getMetaData(cls, envLoader, true);
configureMetaData(meta, conf, redefineAvailable, true);
}
private static void configureMetaData(ClassMetaData meta,
OpenJPAConfiguration conf, boolean redefineAvailable, boolean warn) {
setDetachedState(meta);
// If warn & (implicit field access | mixed access) & noredef
if (warn && ((AccessCode.isField(meta.getAccessType())
&& !meta.isMixedAccess()) || meta.isMixedAccess())
&& !redefineAvailable) {
// only warn about declared fields; superclass fields will be
// warned about when the superclass is handled
for (FieldMetaData fmd : meta.getDeclaredFields()) {
if (AccessCode.isProperty(fmd.getAccessType()))
continue;
switch (fmd.getTypeCode()) {
case JavaTypes.COLLECTION:
case JavaTypes.MAP:
// we can lazily load these, since we own the
// relationship container
break;
default:
if (!fmd.isInDefaultFetchGroup()
&& !(fmd.isVersion() || fmd.isPrimaryKey())) {
Log log = conf.getLog(
OpenJPAConfiguration.LOG_ENHANCE);
log.warn(_loc.get("subclasser-fetch-group-override",
meta.getDescribedType().getName(),
fmd.getName()));
fmd.setInDefaultFetchGroup(true);
}
}
}
}
}
private static void write(BCClass bc, PCEnhancer enhancer,
Map<Class<?>, byte[]> map, Class<?> cls, List<Class<?>> subs, List<Class<?>> ints)
throws IOException {
if (bc == enhancer.getManagedTypeBytecode()) {
// if it was already defined, don't put it in the map,
// but do set the metadata accordingly.
if (enhancer.isAlreadyRedefined())
ints.add(bc.getType());
else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
AsmAdaptor.write(bc, baos);
map.put(bc.getType(), baos.toByteArray());
debugBytecodes(bc);
}
} else {
if (!enhancer.isAlreadySubclassed()) {
debugBytecodes(bc);
// this is the new subclass
ClassLoader loader = GeneratedClasses.getMostDerivedLoader(
cls, PersistenceCapable.class);
subs.add(GeneratedClasses.loadBCClass(bc, loader));
}
}
}
public static void debugBytecodes(BCClass bc) throws IOException {
// Write the bytecodes to disk for debugging purposes.
if ("true".equals(System.getProperty(
ManagedClassSubclasser.class.getName() + ".dumpBytecodes")))
{
File tmp = new File(System.getProperty("java.io.tmpdir"));
File dir = new File(tmp, "openjpa");
dir = new File(dir, "pcsubclasses");
dir.mkdirs();
dir = Files.getPackageFile(dir, bc.getPackageName(), true);
File f = new File(dir, bc.getClassName() + ".class");
// START - ALLOW PRINT STATEMENTS
System.err.println("Writing to " + f);
// STOP - ALLOW PRINT STATEMENTS
AsmAdaptor.write(bc, f);
}
}
private static void setIntercepting(OpenJPAConfiguration conf,
ClassLoader envLoader, Class<?> cls) {
ClassMetaData meta = conf.getMetaDataRepositoryInstance()
.getMetaData(cls, envLoader, true);
meta.setIntercepting(true);
}
/**
* If the metadata is configured to use a synthetic
* detached state, reset it to not use a detached
* state field, since we can't add fields when redefining.
*/
private static void setDetachedState(ClassMetaData meta) {
if (ClassMetaData.SYNTHETIC.equals(meta.getDetachedState()))
meta.setDetachedState(null);
}
}