| /* |
| * 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); |
| } |
| } |