Bug 413862 - Restore "forced exports" mechanism
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/.classpath b/plugins/org.eclipse.objectteams.otequinox.turbo/.classpath
new file mode 100644
index 0000000..098194c
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/.classpath
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
+	<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/.project b/plugins/org.eclipse.objectteams.otequinox.turbo/.project
new file mode 100644
index 0000000..6a04c29
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>org.eclipse.objectteams.otequinox.turbo</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.pde.ManifestBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.pde.SchemaBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.pde.PluginNature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/.settings/org.eclipse.jdt.core.prefs b/plugins/org.eclipse.objectteams.otequinox.turbo/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..c2a6c9e
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,96 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled
+org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=error
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.source=1.7
+org.eclipse.objectteams.otdt.compiler.option.pure_java=enabled
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/META-INF/MANIFEST.MF b/plugins/org.eclipse.objectteams.otequinox.turbo/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..72d34a2
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/META-INF/MANIFEST.MF
@@ -0,0 +1,9 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: OT/Equinox Turbo
+Bundle-SymbolicName: org.eclipse.objectteams.otequinox.turbo
+Bundle-Version: 1.0.0.qualifier
+Bundle-Vendor: Eclipse.org - Object Teams
+Fragment-Host: org.eclipse.osgi;bundle-version="[3.10.1,4.0.0)"
+Bundle-RequiredExecutionEnvironment: JavaSE-1.7
+Export-Package: org.eclipse.objectteams.otequinox.turbo
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/build.properties b/plugins/org.eclipse.objectteams.otequinox.turbo/build.properties
new file mode 100644
index 0000000..40a1314
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/build.properties
@@ -0,0 +1,5 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+               .,\
+               hookconfigurators.properties
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/hookconfigurators.properties b/plugins/org.eclipse.objectteams.otequinox.turbo/hookconfigurators.properties
new file mode 100644
index 0000000..20cc368
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/hookconfigurators.properties
@@ -0,0 +1 @@
+hook.configurators=org.eclipse.objectteams.otequinox.turbo.OTStorageHook
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/AspectPermission.java b/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/AspectPermission.java
new file mode 100644
index 0000000..cab247b
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/AspectPermission.java
@@ -0,0 +1,28 @@
+/**********************************************************************
+ * This file is part of "Object Teams Development Tooling"-Software
+ * 
+ * Copyright 2009, 2014 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
+ * 
+ * Please visit http://www.eclipse.org/objectteams for updates and contact.
+ * 
+ * Contributors:
+ * Technical University Berlin - Initial API and implementation
+ **********************************************************************/
+package org.eclipse.objectteams.otequinox.turbo;
+
+/**
+ * Copy of corresponding type in org.eclipse.objectteams.otequinox (inaccessible to this fragment).
+ */
+public enum AspectPermission {
+	/** Not influencing negotiation between other parties. */
+	UNDEFINED,
+	/** A permission is granted unless someone else denies it. */
+	GRANT, 
+	/** A permission is explicitly denied. Cannot be overridden. */
+	DENY;
+}
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/ForcedExportsRegistry.java b/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/ForcedExportsRegistry.java
new file mode 100644
index 0000000..eb67a16
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/ForcedExportsRegistry.java
@@ -0,0 +1,309 @@
+/**********************************************************************
+ * This file is part of "Object Teams Development Tooling"-Software
+ * 
+ * Copyright 2014 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.otequinox.turbo;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.osgi.framework.log.FrameworkLog;
+import org.eclipse.osgi.framework.log.FrameworkLogEntry;
+
+/**
+ * Reads forced exports from various locations (granted/denied).
+ * 
+ * All members are static to facilitate reflexive access from org.eclipse.objectteams.otequinox.
+ */
+public class ForcedExportsRegistry {
+	// property names:
+	private static final String FORCED_EXPORTS_DENIED  = "otequinox.forced.exports.denied"; // //$NON-NLS-1$
+	private static final String FORCED_EXPORTS_GRANTED = "otequinox.forced.exports.granted"; // //$NON-NLS-1$
+	
+	// terminal tokens:
+	private static final String XFRIENDS = "x-friends:="; //$NON-NLS-1$
+
+	private static HashMap<String, String> grantedForcedExports = null;
+	private static HashMap<String, String> deniedForcedExports = null;
+
+	static FrameworkLog fwLog;
+
+	public static void install(FrameworkLog log) {
+		fwLog = log;
+		readForcedExports();
+	}
+
+	static void logError(String message, Throwable e) {
+		fwLog.log(new FrameworkLogEntry(ForcedExportsRegistry.class.getName(),
+									    message,
+									    FrameworkLogEntry.ERROR,
+									    e, null));
+	}
+
+	static void logError(String message) {
+		fwLog.log(new FrameworkLogEntry(ForcedExportsRegistry.class.getName(),
+									    message,
+									    FrameworkLogEntry.ERROR,
+									    null, null));
+	}
+
+	private static void readForcedExports() {
+		// read forced exports from config.ini (system properties), or file(s)
+		deniedForcedExports= new HashMap<String, String>();
+		grantedForcedExports= new HashMap<String, String>();
+		readForcedExportsFromProperty(FORCED_EXPORTS_DENIED, AspectPermission.DENY);
+		readForcedExportsFromProperty(FORCED_EXPORTS_GRANTED, AspectPermission.GRANT);
+	}
+
+	private static void readForcedExportsFromProperty(String propKey, AspectPermission perm) {
+		String value= System.getProperty(propKey);
+		if (value != null) {
+			if (value.length() > 0 && value.charAt(0) == '@') {
+				// follows: comma-separated list for filenames to read from:
+				int pos = 1;
+				while (pos < value.length()) {
+					int comma = value.indexOf(',', pos);
+					String fileName;
+					if (comma > -1) {
+						fileName = value.substring(pos, comma);
+						pos = comma+1;
+					} else {
+						fileName = value.substring(pos);
+						pos = value.length();
+					}
+					parseForcedExportsFile(new File(fileName), perm);
+				}
+			} else {
+				parseForcedExports(value, perm);
+			}
+		}
+	}
+	/**
+	 * API:
+	 * Adds the definitions from a given file to the set of granted forced exports.
+	 * @param file file to read from
+	 * @param perm either ALLOW or DENY (via their ordinal()),
+	 * 	 determines how the found forced export decls are interpreted.
+	 */
+	public static void parseForcedExportsFile(File file, int perm) {
+		parseForcedExportsFile(file, AspectPermission.values()[perm]);
+	}
+	private static synchronized void parseForcedExportsFile(File file, AspectPermission perm) {
+		StringBuffer newVal = new StringBuffer();
+		try {
+			// read content of one file:
+			FileInputStream fis = new FileInputStream(file);
+			try (BufferedReader br = new BufferedReader(new InputStreamReader(fis, "UTF8"))) { //$NON-NLS-1$
+				String line;
+				boolean afterClosingBrace = false;
+				while ((line = br.readLine()) != null) {
+					line = line.trim();
+					if (line.length() > 0) {
+						if (line.charAt(0) == '#') continue;
+						// may need to replace linebreak after ']' with ',' - to match one-line syntax in config.ini
+						char lastReadChar = line.charAt(line.length()-1);
+						if (afterClosingBrace && lastReadChar != ',')
+							newVal.append(',');
+						afterClosingBrace = lastReadChar == ']';
+							
+						newVal.append(line);
+					}
+				}
+				if (newVal.length() > 0)
+					parseForcedExports(newVal.toString(), perm);
+			}
+		} catch (IOException e) {
+			fwLog.log(new FrameworkLogEntry(OTStorageHook.class.getName(),
+						    "failed to read forcedExports file "+file.getName(), //$NON-NLS-1$
+						    FrameworkLogEntry.ERROR, e, null));
+		}							
+	}
+	/* Adds the given definitions to the set of granted forced exports. */
+	private static void parseForcedExports(String value, AspectPermission perm) {
+		HashMap<String, String> map = getForcedExportsMap(perm);
+		if (map == null) return; // DONT_CARE
+		int pos = 0;
+		String[] values = new String[2]; // { BaseBundleID , PackageExports }
+		while (true) {
+			pos = getForcedExportForOneBase(value, pos, values);
+			String plugin= values[0];
+			String pack= values[1];
+			if (map.containsKey(plugin)) {
+				String oldPack = map.get(plugin);
+				pack = oldPack+','+pack;  // append to existing definition
+			}
+			map.put(plugin, pack);
+			if (pos >= value.length())
+				break; // eot: done without errors
+			if (value.charAt(pos) != ',')
+				throwSyntaxError(value, pos, "missing ','"); //$NON-NLS-1$
+			pos++; // skip the ','
+		}
+	}
+	/**
+	 * fetch one chunk of forced exports specification from `spec`:
+	 * <ul>
+	 * <li>my.base.plugin[<em>specification-of-force-exported-packages</em>]<em>tail</em></li>
+	 * </ul>
+	 * is split into:
+	 * <ul>
+	 * <li>my.base.plugin</li>
+	 * <li><em>specification-of-force-exported-packages</em></li>
+	 * </ul>
+	 * and return value points to <em>tail</em>
+	 * @param spec where to read from
+	 * @param start where to start reading (within spec)
+	 * @param value store result chunks in this array: [0] = base plugin, [1] = packages specification
+	 * @return position after what has been interpreted so far
+	 */
+	private static int getForcedExportForOneBase(String spec, int start, String[] values) {
+		int open = spec.indexOf('[', start);
+		if (open == -1)
+			throwSyntaxError(spec, start, "missing '['"); //$NON-NLS-1$
+		int close = spec.indexOf(']', start);
+		if (close == -1)
+			throwSyntaxError(spec, open, "missing ']'"); //$NON-NLS-1$
+		values[0] = spec.substring(start, open);
+		values[1] = spec.substring(open+1, close);
+		return close+1;
+	}
+
+	private static void throwSyntaxError(String spec, int pos, String string) {
+		throw new RuntimeException("Illegal syntax in 'forcedExports' directive at position "+pos+" (not counting whitespace): "+string+"\n value is:\n"+spec); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$		
+	}
+
+	private static HashMap<String, String> getForcedExportsMap(AspectPermission perm) {
+		switch (perm) {
+			case GRANT:     return grantedForcedExports;
+			case DENY:      return deniedForcedExports;
+			case UNDEFINED: return null; 
+//			default: // not implementing a default case, want to be warned when new enum-constants are added
+		}
+		// for binary compatibility; see also https://bugs.eclipse.org/bugs/show_bug.cgi?id=265744
+		throw new IncompatibleClassChangeError("enum "+AspectPermission.class.getName()+" has changed unexpectedly."); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	/** 
+	 * API for {@link OTStorageHook.StorageHookImpl}.
+	 * Query whether any forced-exports are declared for the given base bundle.
+	 * @param baseBundleId
+	 * @param the permission we are asking for
+	 * @return comma-separated package names
+	 */ 
+	public static String getGrantedForcedExportsByBase(String baseBundleId) {
+
+		// can be queried before we had a chance to initialize our data structures
+		synchronized (OTStorageHook.class) {
+			if (grantedForcedExports == null)
+				readForcedExports();
+		}
+
+		String exports = grantedForcedExports.get(baseBundleId);
+		if (exports != null) {
+			// yes, we need to add forced exports:
+			if (!exports.contains(XFRIENDS)) { // FIXME: check once for each package?
+				// invalid directive:
+				grantedForcedExports.remove(baseBundleId); 
+				logError("config.ini: missing x-friends directive in forced export of "+exports); //$NON-NLS-1$
+				return null; // don't install illegal forced exports
+			}
+		}
+		return exports;
+	}
+
+	/**
+	 * API for org.eclipse.objectteams.otequinox (as a service and using reflection).
+	 * Query whether any forced-exports are declared in config.ini (or other location) 
+	 * for the given aspect bundle.
+	 * @param aspectBundleId
+	 * @param the permission we are asking for
+	 * @return list of pairs (baseBundleId x packageName)
+	 */ 
+	public static /*@NonNull*/ List<String[]> getForcedExportsByAspect(String aspectBundleId, int perm) 
+	{
+		// can be queried before we had a chance to initialize our data structures
+		synchronized (OTStorageHook.class) {
+			if (grantedForcedExports == null)
+				readForcedExports();
+		}
+
+		ArrayList<String[]> result= new ArrayList<String[]>(5);
+		
+		HashMap<String, String> map = getForcedExportsMap(AspectPermission.values()[perm]);
+		if (map == null) 
+			return result; // DONT_CARE: useless query.
+		
+		for (Map.Entry<String,String> entry: map.entrySet()) {
+			String export= entry.getValue();
+			int start = 0;
+			while (start >= 0 && start < export.length()) {
+				if (start > 0) {
+					// skip separator after previous entry
+					if (export.charAt(start) == ',')
+						start++;
+					else
+						logError("Error parsing forced exports: "+export+", comma expected at position "+start); //$NON-NLS-1$ //$NON-NLS-2$
+				}
+				int pos= export.indexOf(';'+XFRIENDS, start);
+				if (pos == -1)
+					break;
+				String packageName = export.substring(start, pos);
+				List<String> aspectBundles = new ArrayList<String>(); 
+				start = scanAspectBundles(export, pos+XFRIENDS.length()+1, aspectBundles);
+				for (String aspect : aspectBundles) {
+					if (aspect.equals(aspectBundleId)) {
+						result.add(new String[]{entry.getKey(), packageName});
+					}
+				}
+			}
+		}
+		return result;
+	}
+
+	private static int scanAspectBundles(String export, int pos, List<String> result) {
+		String termChars = ",]"; //$NON-NLS-1$
+		if (export.charAt(pos) == '"') {
+			pos++;
+			termChars = "\""; //$NON-NLS-1$
+		}
+		int start = pos;
+		while (pos < export.length()) {
+			char c = export.charAt(pos);
+			switch (c) {
+			case ',':
+			case ']':
+			case '"':
+				String next = export.substring(start, pos);
+				result.add(next);
+				start = pos+1;
+				if (termChars.indexOf(c) != -1)
+					return start;
+				break;
+			}
+			pos++;
+		}
+		logError("Unterminated forced exports: "+export); //$NON-NLS-1$
+		if (pos > start)
+			result.add(export.substring(start));
+		return export.length();
+	}
+
+}
diff --git a/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/OTStorageHook.java b/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/OTStorageHook.java
new file mode 100644
index 0000000..8cb54af
--- /dev/null
+++ b/plugins/org.eclipse.objectteams.otequinox.turbo/src/org/eclipse/objectteams/otequinox/turbo/OTStorageHook.java
@@ -0,0 +1,122 @@
+/**********************************************************************
+ * This file is part of "Object Teams Development Tooling"-Software
+ * 
+ * Copyright 2014 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.otequinox.turbo;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Dictionary;
+
+import org.eclipse.osgi.framework.log.FrameworkLog;
+import org.eclipse.osgi.framework.log.FrameworkLogEntry;
+import org.eclipse.osgi.framework.util.Headers;
+import org.eclipse.osgi.internal.hookregistry.HookConfigurator;
+import org.eclipse.osgi.internal.hookregistry.HookRegistry;
+import org.eclipse.osgi.internal.hookregistry.StorageHookFactory;
+import org.eclipse.osgi.storage.BundleInfo.Generation;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+
+/** This class intercepts each bundle manifest and checks if forced exports must be injected. */
+public class OTStorageHook extends StorageHookFactory<Object, Object, OTStorageHook.StorageHookImpl> implements HookConfigurator {
+	// HINTS FOR DEBUGGING:
+	// - This fragment must be colocated with org.eclipse.osgi - perhaps a symbolic link helps to establish this.
+	// - This class must be accessible by its qualified name with no leading "src", again a symbolic link my help:
+	//   $ ln -s bin/org org
+
+	private FrameworkLog fwLog;
+
+	class StorageHookImpl extends StorageHookFactory.StorageHook<Object,Object> {
+
+		public StorageHookImpl(Generation generation) {
+			super(generation, OTStorageHook.class);
+		}
+
+		@Override
+		public void initialize(Dictionary<String, String> manifest) throws BundleException {
+			// when initializing, intercept the manifest and conditionally inject forced exports
+			String[] id = manifest.get(Constants.BUNDLE_SYMBOLICNAME).split(";");
+			if (id.length > 0) {
+				String packages = ForcedExportsRegistry.getGrantedForcedExportsByBase(id[0]);
+				if (packages != null && !packages.isEmpty()) {
+					String exportedPackages = manifest.get(Constants.EXPORT_PACKAGE);
+					if (exportedPackages != null && !exportedPackages.isEmpty())
+						exportedPackages = exportedPackages+','+packages;
+					else
+						exportedPackages = packages;
+					putHeader(id[0], Constants.EXPORT_PACKAGE, exportedPackages, packages);
+				}
+			}
+		}
+		/** Reflexively perform our (unusual) work. */
+		void putHeader(String id, String header, String value, String added) {
+			try {
+				Generation gen = getGeneration();
+				Method getRawHeaders = gen.getClass().getDeclaredMethod("getRawHeaders", new Class<?>[0]);
+				getRawHeaders.setAccessible(true);
+				@SuppressWarnings("unchecked")
+				Headers<String,String> headers = (Headers<String, String>) getRawHeaders.invoke(gen, new Object[0]);
+				Field readOnly = headers.getClass().getDeclaredField("readOnly");
+				readOnly.setAccessible(true);
+				readOnly.set(headers, false);
+				// pay-load:
+				headers.put(header, value);
+				// restore:
+				readOnly.setAccessible(false);
+				getRawHeaders.setAccessible(false);
+				fwLog.log(new FrameworkLogEntry(OTStorageHook.class.getName(), "OT/Equinox Turbo: added forced export into base bundle "+id+":\n\t"+added, FrameworkLogEntry.INFO, null, null));
+			} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchFieldException e) {
+				fwLog.log(new FrameworkLogEntry(OTStorageHook.class.getName(), "Unable to inject forced exports", FrameworkLogEntry.ERROR, e, null));
+			}
+		}
+
+		@Override
+		public void load(Object loadContext, DataInputStream is) throws IOException {
+			// nop
+		}
+
+		@Override
+		public void save(Object saveContext, DataOutputStream os) throws IOException {
+			// nop
+		}
+	}
+
+	@Override
+	public void addHooks(HookRegistry hookRegistry) {
+		hookRegistry.addStorageHookFactory(this);
+		fwLog = hookRegistry.getContainer().getLogServices().getFrameworkLog();
+		ForcedExportsRegistry.install(fwLog);
+	}
+	
+	@Override
+	protected StorageHookImpl createStorageHook(Generation generation) {
+		return new StorageHookImpl(generation);
+	}
+
+	@Override
+	public int getStorageVersion() {
+		return 0;
+	}
+
+	@Override
+	public boolean isCompatibleWith(int version) {
+		return false; // FIXME: forcing to re-init every time, is this OK?
+	}
+
+}