blob: 38e425b08c3ddd9d85166f8abbaa3558af92a423 [file] [log] [blame]
/**********************************************************************
* This file is part of "Object Teams Development Tooling"-Software
*
* Copyright 2009 Germany and Technical University Berlin, Germany.
*
* 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
* $Id: AspectPermissionManager.java 23468 2010-02-04 22:34:27Z stephan $
*
* Please visit http://www.eclipse.org/objectteams for updates and contact.
*
* Contributors:
* Technical University Berlin - Initial API and implementation
**********************************************************************/
package org.eclipse.objectteams.otequinox.internal;
import static org.eclipse.objectteams.otequinox.hook.AspectPermission.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.eclipse.core.internal.runtime.InternalPlatform;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.RegistryFactory;
import org.eclipse.objectteams.otequinox.ActivationKind;
import org.eclipse.objectteams.otequinox.AspectBindingRequestAnswer;
import org.eclipse.objectteams.otequinox.Constants;
import org.eclipse.objectteams.otequinox.IAspectRequestNegotiator;
import org.eclipse.objectteams.otequinox.TransformerPlugin;
import org.eclipse.objectteams.otequinox.internal.MasterTeamLoader.TeamClassRecord;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.objectteams.otequinox.hook.AspectPermission;
import org.eclipse.objectteams.otequinox.hook.HookConfigurator;
import org.eclipse.objectteams.otequinox.hook.ILogger;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.service.packageadmin.PackageAdmin;
/**
* Manage permissions of aspect bundles requesting to apply aspectBindings and forcedExports.
* The following pieces of information are checked:
* <ul>
* <li>properties set in installation-wide config.ini or as command line args (handled by {@link HookConfigurator} (plus internal class OTStorageHook))</li>
* <li>defaults set per workspace (file negotiationDefaults.txt)</li>
* <li>individual GRANT/DENY per workspace (files grantedForcedExports.txt, deniedForcedExports.txt)</li>
* <li>answers from registered negotiators (extension point org.eclipse.objectteams.otequinox.aspectBindingNegotiators, see {@link IAspectRequestNegotiator})</li>
* </ul>
*
* <p>
* The final answer for a given request is combined from all sources where the priority of any {@link #DENY} answer is highest,
* of {@link #UNDEFINED} is lowest.
* </p>
* <p>
* If a negotiator has determined a decision and its answer has the <code>persistent</code> flag set,
* this particular aspect permission is stored as per-workspace configuration.
* </p>
* @author stephan
* @since 1.2.6
*/
@SuppressWarnings("restriction")
public class AspectPermissionManager {
// property names for default configuration:
private static final String FORCED_EXPORT_DEFAULT = "forced.export.default";
private static final String ASPECT_BINDING_DEFAULT = "aspect.binding.default";
// workspace files where negotiation configuration is stored:
private static final String NEGOTIATION_DEFAULTS_FILE = "negotiationDefaults.txt";
private static final String GRANTED_FORCED_EXPORTS_FILE = "grantedForcedExports.txt";
private static final String DENIED_FORCED_EXPORTS_FILE = "deniedForcedExports.txt";
// set of aspect plug-ins for which some permission has been denied:
private Set<String> deniedAspects = new HashSet<String>();
// default permission for aspect bindings:
private AspectPermission defaultAspectBindingPermission = GRANT;
// default permission for forced exports:
private AspectPermission defaultForcedExportPermission = UNDEFINED; // not yet granted, but open for receiving a GRANT
// for negotiation of aspect binding requests (incl. forced export):
private List<IAspectRequestNegotiator> negotiators = new ArrayList<IAspectRequestNegotiator>();
// collect all forced exports (denied/granted), granted should balance to an empty structure.
// structure is: aspect-id -> (base bundle x base package)*
private HashMap<String, ArrayList<String[]>> deniedForcedExportsByAspect= new HashMap<String, ArrayList<String[]>>();
private HashMap<String, ArrayList<String[]>> grantedForcedExportsByAspect= new HashMap<String, ArrayList<String[]>>();
// key is aspectId+"->"+baseId, value is array of team names
private HashMap<String, Set<String>> deniedTeamsByAspectBinding = new HashMap<String, Set<String>>();
private HashMap<String, Set<String>> grantedTeamsByAspectBinding = new HashMap<String, Set<String>>();
// the workspace directory for storing the state of this plugin
private IPath otequinoxState;
// back link needed for accessing the state location:
private Bundle transformerBundle;
// helper instance needed to stop bundles by name
private PackageAdmin packageAdmin;
// shared logger:
private ILogger log;
public AspectPermissionManager(ILogger log, Bundle bundle, PackageAdmin packageAdmin) {
this.log = log;
this.transformerBundle = bundle;
this.packageAdmin = packageAdmin;
}
/* local cache for isReady(): */
private boolean isWaitingForLocation = true;
/** Before using this permission manager a client must check whether we're ready (instance location set). */
public boolean isReady() {
if (!isWaitingForLocation)
return true;
try {
InternalPlatform platform = InternalPlatform.getDefault();
Location instanceLocation = platform.getInstanceLocation();
if (!instanceLocation.isSet())
return false; // not yet capable
this.isWaitingForLocation = false;
fetchAspectBindingPermssionsFromWorkspace();
} catch (NoClassDefFoundError ncdfe) {
this.log.log(ILogger.WARNING, "Optional class InternalPlatform not found, cannot access workspace location");
this.isWaitingForLocation = false;
return true;
}
if (!this.obligations.isEmpty())
for (Runnable job : this.obligations)
job.run();
return true;
}
/**
* Fetch stored permissions from this plugin's workspace state.
*
* @pre instance location should be set (see {@link #isReady()}),
* otherwise will silently return without accessing workspace settings.
*/
private void fetchAspectBindingPermssionsFromWorkspace()
{
try {
this.otequinoxState = InternalPlatform.getDefault().getStateLocation(this.transformerBundle, true);
} catch (NoClassDefFoundError ncdfe) {
this.log.log(ILogger.WARNING, "Optional class InternalPlatform not found, cannot access workspace location");
return;
}
// defaults:
IPath configFilePath = this.otequinoxState.append(NEGOTIATION_DEFAULTS_FILE);
File configFile = new File(configFilePath.toOSString());
if (configFile.exists()) {
Properties props = new Properties();
try {
boolean migrated = false; // TODO(SH): remove this migration support in 1.3.0
props.load(new FileInputStream(configFile));
String value = (String) props.get(ASPECT_BINDING_DEFAULT);
if (value != null)
try {
defaultAspectBindingPermission = AspectPermission.valueOf(value);
} catch (IllegalArgumentException iae) {
if ("DONT_CARE".equals(value)) {
defaultAspectBindingPermission = AspectPermission.UNDEFINED;
migrated = true;
} else {
// this code should remain even after 1.3.0:
defaultAspectBindingPermission = AspectPermission.DENY;
log(iae, "Cannot set default aspect permission from file "+NEGOTIATION_DEFAULTS_FILE+", assuming DENY.");
}
}
value = (String) props.get(FORCED_EXPORT_DEFAULT);
if (value != null)
try {
defaultForcedExportPermission = AspectPermission.valueOf(value);
} catch (IllegalArgumentException iae) {
if ("DONT_CARE".equals(value)) {
defaultForcedExportPermission = AspectPermission.UNDEFINED;
migrated = true;
} else {
// this code should remain even after 1.3.0:
defaultForcedExportPermission = AspectPermission.DENY;
log(iae, "Cannot set default forced exports permission from file "+NEGOTIATION_DEFAULTS_FILE+", assuming DENY.");
}
}
if (migrated)
writeNegotiationDefaults(configFile);
} catch (IOException ioex) {
log(ioex, "Failed to read configuration file "+configFilePath.toOSString());
}
} else {
try {
File stateDir = new File(this.otequinoxState.toOSString());
if (!stateDir.exists())
stateDir.mkdirs();
configFile.createNewFile();
writeNegotiationDefaults(configFile);
} catch (IOException ioex) {
log(ioex, "Failed to create configuration file "+configFilePath.toOSString());
}
}
// explicitly denied:
configFilePath = this.otequinoxState.append(DENIED_FORCED_EXPORTS_FILE);
configFile = new File(configFilePath.toOSString());
if (configFile.exists())
HookConfigurator.parseForcedExportsFile(configFile, DENY);
// explicitly granted:
configFilePath = this.otequinoxState.append(GRANTED_FORCED_EXPORTS_FILE);
configFile = new File(configFilePath.toOSString());
if (configFile.exists())
HookConfigurator.parseForcedExportsFile(configFile, GRANT);
}
private void writeNegotiationDefaults(File configFile)
throws IOException
{
FileWriter writer = new FileWriter(configFile);
writer.append(ASPECT_BINDING_DEFAULT+'='+defaultAspectBindingPermission.toString()+'\n');
writer.append(FORCED_EXPORT_DEFAULT+'='+defaultForcedExportPermission.toString()+'\n');
writer.flush();
writer.close();
log(ILogger.INFO, "Created aspect binding defaults file "+configFile.getCanonicalPath());
}
/** Load extensions for EP org.eclipse.objectteams.otequinox.aspectBindingNegotiators. */
public void loadAspectBindingNegotiators(BundleContext context) {
IConfigurationElement[] aspectBindingNegotiatorsConfigs = RegistryFactory.getRegistry().getConfigurationElementsFor(
Constants.TRANSFORMER_PLUGIN_ID, Constants.ASPECT_NEGOTIATOR_EXTPOINT_ID);
for (int i = 0; i < aspectBindingNegotiatorsConfigs.length; i++) {
IConfigurationElement currentNegotiatorConfig = aspectBindingNegotiatorsConfigs[i];
try {
Object negotiator = currentNegotiatorConfig.createExecutableExtension("class");
if (negotiator != null)
this.negotiators.add(((IAspectRequestNegotiator)negotiator));
} catch (CoreException e) {
log(e, "Failed to instantiate extension "+currentNegotiatorConfig);
}
}
}
/** Delegatee of internal API {@link TransformerPlugin#isDeniedAspectPlugin(String)}. */
public boolean isDeniedAspectPlugin(String symbolicName) {
return this.deniedAspects.contains(symbolicName);
}
/**
* Check whether a given aspect requests forced exports from base,
* and whether these requests are granted/denied by checking all available sources.
*
* Clients should ask {@link #isReady()} (ie., instance location is set) before calling this method,
* otherwise workspace settings have to be silently ignored (any error should be signaled by client).
*
* @param aspectId symbolic name of the aspect bundle
* @param baseBundleId symbolic name of the bound base bundle
* @param forcedExports any forced exports requested in this aspect binding.
* @return whether all requests (if any) have been granted
*/
public boolean checkForcedExports(String aspectId, String baseBundleId, IConfigurationElement[] forcedExports)
{
if (forcedExports == null || forcedExports.length == 0)
return true;
ArrayList<String[]> deniedForcedExports = getConfiguredForcedExports(aspectId, DENY, deniedForcedExportsByAspect);
ArrayList<String[]> grantedForcedExports= getConfiguredForcedExports(aspectId, GRANT, grantedForcedExportsByAspect);
// iterate all requested forcedExports to search for a matching permission:
for (IConfigurationElement forcedExport : forcedExports) { // [0..1] (as defined in the schema)
String forcedExportsRequest = forcedExport.getValue();
if (forcedExportsRequest == null)
continue;
for (String singleForcedExportRequest : forcedExportsRequest.split(","))
{
singleForcedExportRequest = singleForcedExportRequest.trim();
String[] listEntry;
boolean grantReported = false;
AspectPermission negotiatedPermission = this.defaultForcedExportPermission;
// DENY by default?
if (negotiatedPermission == DENY) {
log(ILogger.ERROR, "Default denial of forced export regarding package "+singleForcedExportRequest+
" from bundle "+baseBundleId+" as requested by bundle "+aspectId+"; bundle not activated");
this.deniedAspects.add(aspectId); // keep for answering the TransformerHook.
return false; // NOPE!
}
// DENY from configuration?
listEntry = findRequestInList(baseBundleId, singleForcedExportRequest, deniedForcedExports);
if (listEntry != null) {
log(ILogger.ERROR, "Explicit denial of forced export regarding package "+singleForcedExportRequest+
" from bundle "+baseBundleId+" as requested by bundle "+aspectId+"; bundle not activated");
this.deniedAspects.add(aspectId); // keep for answering the TransformerHook.
return false; // NOPE!
}
// GRANT from configuration?
listEntry = findRequestInList(baseBundleId, singleForcedExportRequest, grantedForcedExports);
if (listEntry != null) {
log(ILogger.INFO, "Forced export granted for "+aspectId+": "+singleForcedExportRequest+" (from bundle "+baseBundleId+")");
grantReported = true;
grantedForcedExports.remove(listEntry);
negotiatedPermission = GRANT;
}
// default and persistent configuration did not DENY, proceed to the negotiators:
boolean shouldPersist = false;
for (IAspectRequestNegotiator negotiator : this.negotiators) {
AspectBindingRequestAnswer answer = negotiator.checkForcedExport(aspectId, baseBundleId, singleForcedExportRequest, negotiatedPermission);
if (answer != null) {
if (answer.permission.compareTo(negotiatedPermission) > 0) // increasing priority of answer?
{
shouldPersist = answer.persistent;
negotiatedPermission = answer.permission;
// locally store as default for subsequent requests (not persistent, see below):
if (answer.allRequests)
this.defaultForcedExportPermission = negotiatedPermission;
if (negotiatedPermission == DENY)
break; // end of discussion.
}
}
}
// make decision persistent?
if (shouldPersist && negotiatedPermission != UNDEFINED)
// FIXME(SH): handle "allRequests":
persistForcedExportsAnswer(aspectId, baseBundleId, singleForcedExportRequest, negotiatedPermission);
// report:
if (negotiatedPermission == GRANT) {
if (!grantReported)
log(ILogger.INFO, "Negotiation granted forced export for "+aspectId+
": "+singleForcedExportRequest+" (from bundle "+baseBundleId+')');
} else {
String verb = "did not grant";
if (negotiatedPermission == DENY)
verb = "denied";
log(ILogger.ERROR, "Negotiation "+verb+" forced export for "+aspectId+
": "+singleForcedExportRequest+" (from bundle "+baseBundleId+")"+
". Aspect is not activated.");
this.deniedAspects.add(aspectId); // keep for answering the TransformerHook.
return false; // don't install illegal aspect
}
}
}
if (!grantedForcedExports.isEmpty())
reportUnmatchForcedExports(aspectId, grantedForcedExports);
return true;
}
/**
* Get the forced exports configured for a given aspect bundle with permission <code>perm</code>.
* Consult {@link HookConfigurator} and store the result in <code>map</code>.
*
* @param aspectId symbolic name of the aspect in focus
* @param perm are we asking about DENY or GRANT?
* @param map in/out param for storing results from OTStorageHook
* @return list of pairs (base bundle x base package)
*/
private ArrayList<String[]> getConfiguredForcedExports(String aspectId,
AspectPermission perm,
HashMap<String, ArrayList<String[]>> map)
{
ArrayList<String[]> forcedExports= map.get(aspectId);
if (forcedExports == null) {
// fetch declarations from config.ini or other locations.
forcedExports= HookConfigurator.getForcedExportsByAspect(aspectId, perm);
map.put(aspectId, forcedExports);
}
return forcedExports;
}
private String[] findRequestInList(String baseBundleId, String basePackage, ArrayList<String[]> list) {
if (list != null)
for (String[] singleExport : list)
if ( singleExport[0].equals(baseBundleId)
&& singleExport[1].equals(basePackage))
{
return singleExport;
}
return null;
}
/**
* If the structure of grantedForcedExports is not empty we have mismatches between forced-export declarations.
* Report these mismatches as warnings.
*/
void reportUnmatchForcedExports(String aspectId, ArrayList<String[]> unmatchedForcedExports)
{
for (String[] export: unmatchedForcedExports) {
String baseId = export[0];
String pack = export[1];
log(ILogger.WARNING, "Aspect "+aspectId+
" does not declare forced export of package "+
pack+" from bundle "+baseId+
" as declared in config.ini (or system property)");
}
}
/* Simple strategy to append a forced export to a file (existing or to be created). */
private void persistForcedExportsAnswer(String aspectId, String baseBundleId, String basePackage, AspectPermission negotiatedPermission)
{
if (this.otequinoxState == null) {
log(ILogger.ERROR, "Can't persist forcedExports permission, no workspace location accessable.");
return;
}
try {
String fileName = (negotiatedPermission == DENY) ? DENIED_FORCED_EXPORTS_FILE : GRANTED_FORCED_EXPORTS_FILE;
IPath forcedExportsPath = this.otequinoxState.append(fileName);
File forcedExportsFile = new File(forcedExportsPath.toOSString());
if (!forcedExportsFile.exists())
forcedExportsFile.createNewFile();
FileWriter writer = new FileWriter(forcedExportsFile, true); // FIXME(SH): consider merge (after decision about file format)
writer.append('\n');
writer.append(baseBundleId);
writer.append("\n[\n\t");
writer.append(basePackage);
writer.append(";x-friends:=\"");
writer.append(aspectId);
writer.append("\"\n]\n");
writer.flush();
writer.close();
} catch (IOException ioe) {
log(ioe, "Failed to persist negotiation result");
}
}
/**
* Check permission for the aspect binding of one specific team.
*
* Clients should ask {@link #isReady()} (ie., instance location is set) before calling this method,
* otherwise workspace settings have to be silently ignored (any error should be signaled by client).
*
* @param aspectBundleId
* @param baseBundleId
* @param teamClass
* @return whether this team is permitted to adapt classes from the given base bundle.
*/
boolean checkTeamBinding(String aspectBundleId, String baseBundleId, String teamClass)
{
boolean shouldReportGrant = false; // grant by default should not be reported
AspectPermission negotiatedPermission = this.defaultAspectBindingPermission;
// DENY by default?
if (negotiatedPermission == DENY) {
log(ILogger.ERROR, "Default denial of aspect binding regarding base bundle "+baseBundleId+
" as requested by bundle "+aspectBundleId+"; bundle not activated");
this.deniedAspects.add(aspectBundleId); // keep for answering the TransformerHook.
return false; // NOPE!
}
String key = aspectBundleId+"->"+baseBundleId;
// denied from configuration?
Set<String> deniedTeams = deniedTeamsByAspectBinding.get(key);
if (deniedTeams != null && !deniedTeams.isEmpty()) {
if (deniedTeams.contains(teamClass)) {
deniedAspects.add(aspectBundleId);
return false;
}
}
// granted from configuration?
Set<String> grantedTeams = grantedTeamsByAspectBinding.get(key);
if (grantedTeams != null && grantedTeams.contains(teamClass)) {
negotiatedPermission = GRANT;
shouldReportGrant = true;
}
// default and persistent configuration did not DENY, proceed to the negotiators:
boolean shouldPersist = false;
for (IAspectRequestNegotiator negotiator : this.negotiators) {
AspectBindingRequestAnswer answer = negotiator.checkAspectBinding(aspectBundleId, baseBundleId, teamClass, negotiatedPermission);
if (answer != null) {
if (answer.permission.compareTo(negotiatedPermission) > 0) // increasing priority of answer?
{
shouldPersist = answer.persistent;
negotiatedPermission = answer.permission;
shouldReportGrant = negotiatedPermission == GRANT;
// locally store as default for subsequent requests:
if (answer.allRequests)
this.defaultAspectBindingPermission = negotiatedPermission;
if (negotiatedPermission == DENY)
break; // end of discussion.
}
}
}
// make decision persistent?
if (shouldPersist && negotiatedPermission != UNDEFINED)
persistTeamBindingAnswer(aspectBundleId, baseBundleId, teamClass, negotiatedPermission);
// report:
if (negotiatedPermission == GRANT) {
if (shouldReportGrant)
log(ILogger.INFO, "Negotiation granted aspect binding for "+aspectBundleId+
" to base bundle "+baseBundleId+" by means of team "+teamClass+'.');
} else {
String verb = "did not grant";
if (negotiatedPermission == DENY)
verb = "denied";
log(ILogger.ERROR, "Negotiation "+verb+" aspect binding for "+aspectBundleId+
" to base bundle "+baseBundleId+" by means of team "+teamClass+
". Aspect is not activated.");
this.deniedAspects.add(aspectBundleId); // keep for answering the TransformerHook.
return false; // don't install illegal aspect
}
return true;
}
private void persistTeamBindingAnswer(String aspectBundleId, String baseBundleId, String teamClass, AspectPermission negotiatedPermission)
{
// FIXME(SH): implement persisting these!
}
void log (Throwable ex, String msg) {
msg = "OT/Equinox: "+msg;
this.log.log(Constants.TRANSFORMER_PLUGIN_ID, ex, msg);
}
void log(int status, String msg) {
if (status >= TransformerPlugin.WARN_LEVEL)
this.log.log(Constants.TRANSFORMER_PLUGIN_ID, status, "OT/Equinox: "+msg);
}
List<Runnable> obligations = new ArrayList<Runnable>();
public void addBaseBundleObligations(final List<Object> teamInstances, final ArrayList<TeamClassRecord> teamClasses, final Bundle baseBundle) {
schedule(new Runnable() {
public void run() {
List<TeamClassRecord> teamsToRevert = new ArrayList<TeamClassRecord>();
// aspect bindings:
for (TeamClassRecord teamClass : teamClasses)
if (!checkTeamBinding(teamClass.aspectBundle.getSymbolicName(), baseBundle.getSymbolicName(), teamClass.teamName))
teamsToRevert.add(teamClass);
if (!teamsToRevert.isEmpty())
revert(teamsToRevert);
}
void revert(List<TeamClassRecord> teamsToRevert) {
try {
Set<Bundle> bundlesToStop = new HashSet<Bundle>();
Class<?>[] deactivationArgumentTypes = new Class[]{Thread.class};
Object[] deactivationArguments = new Object[] {TeamClassRecord.get_ALL_THREADS()};
for (int i=0, c=0; c< teamsToRevert.size(); c++) {
TeamClassRecord teamClass = teamClasses.get(c);
if (teamClass.activation != ActivationKind.NONE) {
Object teamInstance = teamInstances.get(i++);
Method deactivationMethod = teamClass.clazz.getMethod("deactivate", deactivationArgumentTypes);
deactivationMethod.invoke(teamInstance, deactivationArguments);
// could also check if roles are present already ...
}
bundlesToStop.add(teamClass.aspectBundle);
}
for (Bundle bundle : bundlesToStop) {
log(ILogger.ERROR, "Stopping aspect bundle "+bundle.getSymbolicName()+" with denied aspect binding(s)");
bundle.stop();
}
} catch (Exception e) {
log(e, "Failed to revert aspect bundle with denied aspect bindings.");
}
}
});
}
public void addForcedExportsObligations(final List<AspectBinding> aspects, final Bundle baseBundle) {
schedule(new Runnable () {
public void run() {
for (AspectBinding aspectBinding : aspects)
if (!checkForcedExports(aspectBinding.aspectPlugin, baseBundle.getSymbolicName(), aspectBinding.forcedExports))
stopIllegalBundle(aspectBinding.aspectPlugin);
}
});
}
void schedule(Runnable job) {
if (isReady()) // became ready since last query?
job.run();
else
synchronized(obligations) {
obligations.add(job);
}
}
void stopIllegalBundle(String symbolicName) {
String msgCore = "stop bundle "+symbolicName+" whose requests for forced exports have been denied";
if (this.packageAdmin == null) {
log(ILogger.ERROR, "Needing to "+msgCore+" but package admin is not available");
} else {
Bundle[] bundles = this.packageAdmin.getBundles(symbolicName, null);
if (bundles == null)
log(ILogger.ERROR, "Needing to "+msgCore+" but bundle cannot be retrieved");
else
try {
bundles[0].stop();
} catch (BundleException e) {
log(e, "Failed to " + msgCore);
}
}
}
}