blob: d5e67bb0973dab4c590e1f1fd286f783d6439681 [file] [log] [blame]
/**********************************************************************
* This file is part of "Object Teams Development Tooling"-Software
*
* Copyright 2013, 2015 GK Software AG
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Please visit http://www.eclipse.org/objectteams for updates and contact.
*
* Contributors:
* Stephan Herrmann - Initial API and implementation
**********************************************************************/
package org.eclipse.objectteams.internal.osgi.weaving;
import static org.eclipse.objectteams.otequinox.Constants.LIFTING_PARTICIPANT_EXTPOINT_ID;
import static org.eclipse.objectteams.otequinox.Constants.TRANSFORMER_PLUGIN_ID;
import static org.eclipse.objectteams.otequinox.Constants.ORG_OBJECTTEAMS_TEAM;
import static org.eclipse.objectteams.otequinox.TransformerPlugin.log;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.objectteams.internal.osgi.weaving.ASMByteCodeAnalyzer.ClassInformation;
import org.eclipse.objectteams.internal.osgi.weaving.AspectBinding.BaseBundle;
import org.eclipse.objectteams.internal.osgi.weaving.AspectBinding.TeamBinding;
import org.eclipse.objectteams.internal.osgi.weaving.Util.ProfileKind;
import org.eclipse.objectteams.internal.osgi.weaving.AspectPermissionManager;
import org.eclipse.objectteams.otequinox.Constants;
import org.eclipse.objectteams.otequinox.TransformerPlugin;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.hooks.weaving.WeavingHook;
import org.osgi.framework.hooks.weaving.WovenClass;
import org.osgi.framework.hooks.weaving.WovenClassListener;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleWiring;
import org.osgi.resource.Wire;
/**
* This class integrates the OT/J weaver into OSGi using the standard API {@link WeavingHook}.
* <p>
* Additionally, we listen to events of woven classes changing to state {@link WovenClass#DEFINED}:
* </p>
* <ul>
* <li>Given that {@link AspectBindingRegistry#addDeferredTeamClasses} was used to record
* teams that could not be instantiated due to some required class being reported
* as {@link NoClassDefFoundError}.</li>
* <li>Assuming further that this error happened because the required class was in the process
* of being loaded further down the call stack.</li>
* <li>If later one of the not-found classes has been defined we use that trigger to
* re-attempt instantiating the dependent team(s).</li>
* </ul>
*/
public class OTWeavingHook implements WeavingHook, WovenClassListener {
// TODO: this master-switch, which selects the weaver, should probably be replaced by s.t. else?
boolean useDynamicWeaver = "dynamic".equals(System.getProperty("ot.weaving"));
// TODO: temporary switch to fall back to coarse grain checking:
boolean skipBaseClassCheck = "skip".equals(System.getProperty("otequinox.baseClassChecks"));
// for installing a lifting participant:
private static final String LIFTING_PARTICIPANT_FIELD = "_OT$liftingParticipant";
enum WeavingReason { None, Aspect, Base, Thread }
/** Interface to data about aspectBinding extensions. */
private @NonNull AspectBindingRegistry aspectBindingRegistry = new AspectBindingRegistry();
/** Map of trip wires to be fired when a particular base bundle is loaded. */
private @NonNull HashMap<String, BaseBundleLoadTrigger> baseTripWires = new HashMap<>();
/** Set of classes for which processing has started but which are not yet defined in the class loader. */
private @NonNull Set<String> beingDefined = new HashSet<>();
/** Records of teams that have been deferred due to unresolved class dependencies: */
private @NonNull List<WaitingTeamRecord> deferredTeams = new ArrayList<>();
private @NonNull ASMByteCodeAnalyzer byteCodeAnalyzer = new ASMByteCodeAnalyzer();
private AspectPermissionManager permissionManager;
/** A registered lifting participant is directly handled by us. */
private @Nullable IConfigurationElement liftingParticipantConfig;
private @Nullable Class<?> ooTeam;
/** Call-back once the extension registry is up and running. */
public void activate(BundleContext bundleContext, ServiceReference<IExtensionRegistry> serviceReference) {
loadAspectBindingRegistry(bundleContext, serviceReference);
TransformerPlugin activator = TransformerPlugin.getDefault();
activator.registerAspectBindingRegistry(this.aspectBindingRegistry);
activator.registerAspectPermissionManager(this.permissionManager);
}
// ====== Aspect Bindings & Permissions: ======
@SuppressWarnings("deprecation")
private void loadAspectBindingRegistry(BundleContext context, ServiceReference<IExtensionRegistry> serviceReference) {
org.osgi.service.packageadmin.PackageAdmin packageAdmin = null;
ServiceReference<?> ref= context.getServiceReference(org.osgi.service.packageadmin.PackageAdmin.class.getName());
if (ref!=null)
packageAdmin = (org.osgi.service.packageadmin.PackageAdmin)context.getService(ref);
else
log(IStatus.ERROR, "Failed to load PackageAdmin service. Will not be able to handle fragments.");
IExtensionRegistry extensionRegistry = context.getService(serviceReference);
if (extensionRegistry == null) {
log(IStatus.ERROR, "Failed to acquire ExtensionRegistry service, cannot load aspect bindings.");
} else {
permissionManager = new AspectPermissionManager(context.getBundle(), packageAdmin); // known API
permissionManager.loadAspectBindingNegotiators(extensionRegistry);
aspectBindingRegistry.loadAspectBindings(extensionRegistry, packageAdmin, this);
loadLiftingParticipant(extensionRegistry);
}
}
public @NonNull AspectPermissionManager getAspectPermissionManager() {
AspectPermissionManager manager = this.permissionManager;
if (manager == null)
throw new NullPointerException("Missing AspectPermissionManager");
return manager;
}
/* Load extension for org.eclipse.objectteams.otequinox.liftingParticipant. */
private void loadLiftingParticipant(IExtensionRegistry extensionRegistry) {
IConfigurationElement[] liftingParticipantConfigs = extensionRegistry.getConfigurationElementsFor(
TRANSFORMER_PLUGIN_ID, LIFTING_PARTICIPANT_EXTPOINT_ID);
if (liftingParticipantConfigs.length != 1) {
if (liftingParticipantConfigs.length > 1)
log(IStatus.ERROR, "Cannot install more than one lifting participant.");
return;
}
this.liftingParticipantConfig = liftingParticipantConfigs[0];
installLiftingParticipant();
}
private void installLiftingParticipant() {
Class<?> teamClass = this.ooTeam;
IConfigurationElement config = this.liftingParticipantConfig;
if (teamClass != null && config != null) {
try {
Field field = teamClass.getDeclaredField(LIFTING_PARTICIPANT_FIELD); // field name cannot be mentioned in source
field.set(null, config.createExecutableExtension(Constants.CLASS));
log(IStatus.INFO, "Registered Lifting Participant from "+config.getContributor().getName());
} catch (Exception e) {
log(e, "Failed to install lifting participant from "+config.getContributor().getName());
}
this.liftingParticipantConfig = null; // signal done
}
}
// ====== Base Bundle Trip Wires: ======
/**
* Callback during AspectBindingRegistry#loadAspectBindings():
* Set-up a trip wire to fire when the mentioned base bundle is loaded.
*/
void setBaseTripWire(@SuppressWarnings("deprecation") @Nullable org.osgi.service.packageadmin.PackageAdmin packageAdmin,
@NonNull String baseBundleId, BaseBundle baseBundle)
{
if (!baseTripWires.containsKey(baseBundleId))
baseTripWires.put(baseBundleId, new BaseBundleLoadTrigger(baseBundleId, baseBundle, aspectBindingRegistry, packageAdmin));
}
/**
* Check if the given base bundle / base class mandate any loading/instantiation/activation of teams.
* @return true if all involved aspect bindings have been denied (permissions).
*/
boolean triggerBaseTripWires(@Nullable String bundleName, @NonNull WovenClass baseClass) {
BaseBundleLoadTrigger activation = baseTripWires.get(bundleName);
if (activation != null) {
activation.fire(baseClass, beingDefined, this);
if (activation.isDone())
baseTripWires.remove(bundleName);
return activation.areAllAspectsDenied();
}
return false;
}
// ====== Main Weaving Entry: ======
@Override
public void weave(WovenClass wovenClass) {
beingDefined.add(wovenClass.getClassName());
try {
BundleWiring bundleWiring = wovenClass.getBundleWiring();
String bundleName = bundleWiring.getBundle().getSymbolicName();
String className = wovenClass.getClassName();
if (bundleName.equals(Constants.TRANSFORMER_PLUGIN_ID)
|| bundleName.startsWith("org.eclipse.objectteams.otre") // incl. otredyn
|| bundleName.equals("org.objectweb.asm"))
return;
if (BCELPatcher.BCEL_PLUGIN_ID.equals(bundleName)) {
BCELPatcher.fixBCEL(wovenClass);
return;
}
byte[] bytes = wovenClass.getBytes();
WeavingReason reason = requiresWeaving(bundleWiring, className, bytes);
if (reason != WeavingReason.None) {
// do whatever is needed *before* loading this class:
boolean allAspectsAreDenied = triggerBaseTripWires(bundleName, wovenClass);
if (reason == WeavingReason.Base && allAspectsAreDenied) {
return; // don't weave for denied bindings
} else if (reason == WeavingReason.Thread) {
BaseBundle baseBundle = this.aspectBindingRegistry.getBaseBundle(bundleName);
BaseBundleLoadTrigger.addOTREImport(baseBundle, bundleName, wovenClass, this.useDynamicWeaver);
}
long time = 0;
DelegatingTransformer transformer = DelegatingTransformer.newTransformer(useDynamicWeaver, this, bundleWiring);
Class<?> classBeingRedefined = null; // TODO prepare for otre-dyn
try {
log(IStatus.OK, "About to transform class "+className);
time = 0;
if (Util.PROFILE) time= System.nanoTime();
byte[] newBytes = transformer.transform(bundleWiring.getBundle(),
className, classBeingRedefined, null/*protectionDomain*/, bytes);
if (newBytes != null && newBytes != bytes && !Arrays.equals(newBytes, bytes)) {
if (Util.PROFILE) Util.profile(time, ProfileKind.Transformation, className);
log(IStatus.INFO, "Transformation performed on "+className);
wovenClass.setBytes(newBytes);
if (reason == WeavingReason.Aspect)
recordBaseClasses(transformer, bundleName, className);
} else {
if (Util.PROFILE) Util.profile(time, ProfileKind.NoTransformation, className);
}
} catch (IllegalClassFormatException e) {
log(e, "Failed to transform class "+className);
}
}
} catch (ClassCircularityError cce) {
log(cce, "Weaver encountered a circular class dependency");
}
}
WeavingReason requiresWeaving(BundleWiring bundleWiring, String className, byte[] bytes) {
// 1. consult the aspect binding registry (for per-bundle info):
@SuppressWarnings("null")@NonNull // FIXME: org.eclipse.osgi.internal.resolver.BundleDescriptionImpl.getBundle() can return null!
Bundle bundle = bundleWiring.getBundle();
if (aspectBindingRegistry.getAdaptedBasePlugins(bundle) != null)
return WeavingReason.Aspect;
List<AspectBinding> aspectBindings = aspectBindingRegistry.getAdaptingAspectBindings(bundle.getSymbolicName());
if (aspectBindings != null && !aspectBindings.isEmpty()) {
// potential base class: look deeper:
for (AspectBinding aspectBinding : aspectBindings) {
if (!aspectBinding.hasScannedTeams && !aspectBinding.hasBeenDenied)
return WeavingReason.Base; // we may be first, go ahead and trigger the trip wire
}
if (isAdaptedBaseClass(aspectBindings, className, bytes, bundleWiring.getClassLoader()))
return WeavingReason.Base;
}
// 2. test for implementation of Runnable / Thread (per class):
long time = 0;
if (Util.PROFILE) time= System.nanoTime();
if (needsThreadNotificationCode(className, bytes, bundleWiring.getClassLoader()))
return WeavingReason.Thread;
if (Util.PROFILE) Util.profile(time, ProfileKind.SuperClassFetching, "");
return WeavingReason.None;
}
/** check need for weaving by finding an aspect binding affecting this exact base class or one of its supers. */
boolean isAdaptedBaseClass(List<AspectBinding> aspectBindings, String className, byte[] bytes, ClassLoader resourceLoader) {
if (skipBaseClassCheck) return true; // have aspect bindings, flag requests to transform *all* classes in this base bundle
if ("java.lang.Object".equals(className))
return false; // shortcut, not weavable nor do we have supers
long start = 0;
if (Util.PROFILE) start = System.nanoTime();
try {
for (AspectBinding aspectBinding : aspectBindings) {
if (aspectBinding.allBaseClassNames.contains(className) && !aspectBinding.hasBeenDenied)
return true;
}
// attempt recursion to superclass (not superInterfaces atm):
ClassInformation classInfo = null;
if (bytes != null) {
classInfo = this.byteCodeAnalyzer.getClassInformation(bytes, className);
} else {
try (InputStream is = resourceLoader.getResourceAsStream(className.replace('.', '/')+".class")) {
if (is != null)
classInfo = this.byteCodeAnalyzer.getClassInformation(is, className);
} catch (IOException e) {
return false;
}
}
if (classInfo != null && !classInfo.isInterface()) {
// TODO(performance): check common prefix to recognize when crossing the plugin-boundary?
return isAdaptedBaseClass(aspectBindings, classInfo.getSuperClassName(), null, resourceLoader);
}
return false;
} finally {
if (bytes != null && Util.PROFILE) { // only report at top invocation
Util.profile(start, ProfileKind.SuperClassFetching, className);
}
}
}
private void recordBaseClasses(DelegatingTransformer transformer, @NonNull String aspectBundle, String className) {
Collection<String> adaptedBases = transformer.fetchAdaptedBases();
if (adaptedBases == null || adaptedBases.isEmpty()) return;
List<AspectBinding> aspectBindings = aspectBindingRegistry.getAspectBindings(aspectBundle);
if (aspectBindings != null)
for (AspectBinding aspectBinding : aspectBindings)
if (!aspectBinding.hasScannedTeams)
for (TeamBinding team : aspectBinding.teams)
if (team.teamName.equals(className))
if (!team.hasScannedBases) {
for (TeamBinding equivalent : team.equivalenceSet) {
equivalent.addBaseClassNames(adaptedBases);
equivalent.hasScannedBases = true;
}
return; // done all equivalent teams
}
}
// ===== handling deferred teams: ======
/**
* Record the given team classes as waiting for instantiation/activation.
* Callback during {@link BaseBundleLoadTrigger#fire()}
*/
public void addDeferredTeamClasses(List<WaitingTeamRecord> teamClasses) {
synchronized (deferredTeams) {
deferredTeams.addAll(teamClasses);
}
}
/**
* Try to instantiate/activate any deferred teams that may be unblocked
* by the definition of the given trigger class.
*/
public void instantiateScheduledTeams(String triggerClassName) {
List<WaitingTeamRecord> scheduledTeams = null;
synchronized(deferredTeams) {
for (WaitingTeamRecord record : new ArrayList<>(deferredTeams)) {
if (record.notFoundClass.equals(triggerClassName)) {
if (scheduledTeams == null)
scheduledTeams = new ArrayList<>();
if (deferredTeams.remove(record))
scheduledTeams.add(record);
}
}
}
if (scheduledTeams == null) return;
for(WaitingTeamRecord record : scheduledTeams) {
if (record.team.isActivated)
continue;
String teamName = record.team.teamName;
log(IStatus.INFO, "Consider for instantiation/activation: team "+teamName);
try {
TeamLoader loader = new TeamLoader(deferredTeams, beingDefined, this.useDynamicWeaver);
// Instantiate (we only get here if activationKind != NONE)
loader.instantiateAndActivate(record.aspectBinding, record.team, record.activationKind); // may re-insert to deferredTeams
} catch (Exception e) {
log(e, "Failed to instantiate team "+teamName);
continue;
}
}
}
@Override
public void modified(WovenClass wovenClass) {
if (wovenClass.getState() == WovenClass.DEFINED) {
@NonNull String className = wovenClass.getClassName();
if (className.equals(ORG_OBJECTTEAMS_TEAM)) {
this.ooTeam = wovenClass.getDefinedClass();
installLiftingParticipant();
}
beingDefined.remove(className);
instantiateScheduledTeams(className);
TransformerPlugin.flushLog();
}
}
static boolean hasImport(WovenClass clazz, String packageName, String packageWithAttribute) {
List<String> imports = clazz.getDynamicImports();
for (String imp : imports)
if (imp.equals(packageName) || imp.equals(packageWithAttribute))
return true;
for (Wire wire : clazz.getBundleWiring().getRequiredResourceWires(PackageNamespace.PACKAGE_NAMESPACE)) {
Object packageValue = wire.getRequirement().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE);
if (packageName.equals(packageValue) || packageWithAttribute.equals(packageValue))
return true;
}
return false;
}
private boolean needsThreadNotificationCode(String className, byte[] bytes, ClassLoader resourceLoader) {
if ("java.lang.Object".equals(className))
return false; // shortcut, have no super
ClassInformation classInfo = null;
if (bytes != null) {
classInfo = this.byteCodeAnalyzer.getClassInformation(bytes, className);
} else {
try (InputStream is = resourceLoader.getResourceAsStream(className.replace('.', '/')+".class")) {
if (is != null) {
classInfo = this.byteCodeAnalyzer.getClassInformation(is, className);
}
} catch (IOException e) {
return false;
}
}
if (classInfo != null && !classInfo.isInterface()) {
String superClassName = classInfo.getSuperClassName();
if ("java.lang.Thread".equals(superClassName))
return true; // ensure TeamActivation will weave the calls to TeamThreadManager
String[] superInterfaceNames = classInfo.getSuperInterfaceNames();
if (superInterfaceNames != null)
for (int i = 0; i < superInterfaceNames.length; i++) {
if ("java.lang.Runnable".equals(superInterfaceNames[i]))
return true; // ensure TeamActivation will weave the calls to TeamThreadManager
}
}
return false;
}
}