blob: 7be230da28c462f690fe5ba9103935c293b87032 [file] [log] [blame]
/**
* This file is part of "Object Teams Development Tooling"-Software.
*
* Copyright 2013 GK Software AG.
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Please visit http://www.eclipse.org/objectteams for updates and contact.
*
* Contributors:
* Stephan Herrmann - Initial API and implementation.
*/
package org.eclipse.objectteams.builder;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.IllegalClassFormatException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.generic.ClassGen;
import org.apache.maven.plugin.MojoFailureException;
import org.eclipse.objectteams.otre.ObjectTeamsTransformation;
import org.eclipse.objectteams.otre.RepositoryAccess;
import org.eclipse.objectteams.otre.jplis.JPLISEnhancer;
import org.eclipse.objectteams.otre.jplis.ObjectTeamsTransformer;
/**
* Bridge between a build tool like Maven and the Object Teams weaver/transformer.
* This class invokes the transformer in two phases:
* <ul>
* <li>First all specified team classes (and referenced teams/roles) are <em>scanned</em>
* in order to record all weaving instructions from byte code attributes</li>
* <li>Only in a second phase we perform actual transformation, at which point we
* are sure, we don't miss any weaving instructions.</li>
* </ul>
*/
public class BuildTimeWeaver {
private ILogger logger;
private Set<String> scannedClasses; // to avoid double scanning
private Set<String> wovenClasses; // to avoid double weaving
private int numMissing = 0;
/**
* Create a build time weaver wired to the given logger.
* @param logger
*/
public BuildTimeWeaver(ILogger logger) {
this.logger = logger;
}
/**
* Perform weaving for the given classes, reading classfiles using 'loader' and passing woven class bytes to the 'requestor'
* @param teamClasses known team classes, if not listed here, bindings are not guaranteed to be applied correctly.
* @param mainClass if a team config file is specified in property ot.teamconfig, then this class will get the activation code injected.
* @param loader use this loader for reading class files
* @param requestor invoke this for every class where transformation actually changed the class bytes.
* @throws IOException
* @throws IllegalClassFormatException
* @throws MojoFailureException if some class files could not be read.
*/
public void weave(String[] teamClasses, String mainClass, ClassLoader loader, Requestor requestor)
throws IOException, IllegalClassFormatException, MojoFailureException
{
// prepare and instantiate the transformer:
System.setProperty("ot.equinox", "true"); // to prevent transitive class loading in JPLISEnhancer, which would NPE
RepositoryAccess.setClassLoader(loader);
ObjectTeamsTransformer transformer = new ObjectTeamsTransformer();
// prepare sets of classes
// - translate team class names into file names:
List<String> teamFileNames = new ArrayList<>();
for (String teamClassName : teamClasses)
teamFileNames.add(toClassFileName(teamClassName));
// - empty collections:
wovenClasses = new HashSet<>();
scannedClasses = new HashSet<>();
Set<String> moreClassesToWeave = new HashSet<>();
// 1st round: scan all team classes so we know what to weave (stored in CallinBindingManager)
logger.info("==== Scanning OT classes ====");
Collection<String> current = teamFileNames;
while (!current.isEmpty()) {
Set<String> otClasses = new HashSet<>();
for (String classFileName : current) {
scannedClasses.add(classFileName);
logger.info("Scanning OT class: "+classFileName);
try (InputStream stream = loader.getResourceAsStream(classFileName)) {
if (stream == null) {
logger.error("Failed to read class "+classFileName);
numMissing++;
} else {
Collection<String> adaptedBases = readOTAttributes(transformer, stream, classFileName, loader, otClasses);
moreClassesToWeave.addAll(adaptedBases);
}
}
moreClassesToWeave.addAll(otClasses);
}
current = otClasses; // iterate over (transitively) referenced roles and teams
}
// 2.a: if a main class is specified weave it first to insert team activations:
if (mainClass != null) {
logger.info("==== Weaving main class ====");
weaveClass(transformer, loader, toClassFileName(mainClass), requestor);
}
logger.info("==== Weaving teams ====");
// 2.b: weave the teams:
for (String teamClassFile : teamFileNames)
weaveClass(transformer, loader, teamClassFile, requestor);
// 2.c: weave all other classes:
logger.info("==== Weaving other classes ====");
for (String anyClass : moreClassesToWeave)
weaveClass(transformer, loader, anyClass, requestor);
logger.info("==== Number of woven classes: "+wovenClasses.size()+" ====");
if (numMissing > 0)
throw new MojoFailureException("Could not read "+numMissing+" class files");
}
private void weaveClass(ObjectTeamsTransformer transformer, ClassLoader loader, String classFile, Requestor requestor)
throws IOException, IllegalClassFormatException
{
if (!wovenClasses.add(classFile))
return;
try (InputStream stream = loader.getResourceAsStream(classFile)) {
if (stream == null) {
logger.error("Failed to read class "+classFile);
numMissing++;
} else {
int avail = stream.available();
byte[] classBytes = new byte[avail];
int real = 0;
while (real < avail)
real += stream.read(classBytes, real, avail-real);
byte[] newBytes = transformer.transform(loader, classFile, null, null, classBytes);
if (newBytes != classBytes) {
requestor.accept(classFile, newBytes);
logger.debug("Has woven class: "+classFile);
}
}
}
}
/**
* Modified version of original OTRE API.
* @param transformer
* @param file content of the class file to scan
* @param fileName of the class file to scan
* @param loader class loader to use for loading required classes
* @param otClassFileNames here we collect the file names of roles and referenced teams of a currently scanned OT class
* @return adapted base classes
*/
private Collection<String> readOTAttributes(ObjectTeamsTransformer transformer, InputStream file, String fileName,
ClassLoader loader, final Set<String> otClassFileNames)
throws IOException
{
ClassParser cp = new ClassParser(file, fileName);
ClassGen cg = new ClassGen(cp.parse());
JPLISEnhancer jpe = new JPLISEnhancer(cg, loader) {
@Override
public void loadClass(String className, ObjectTeamsTransformation client) {
// intercept referenced classes (roles) to add them to the queue
String classFileName = toClassFileName(className);
if (!scannedClasses.contains(classFileName)) {
logger.debug("\tQueueing OT class: "+classFileName);
otClassFileNames.add(classFileName);
// also add role interfaces (by deleting the __OT__ prefix back to front):
int lastDollar = classFileName.lastIndexOf('$');
while (lastDollar != -1 && classFileName.substring(lastDollar+1).startsWith("__OT__")) {
classFileName = classFileName.substring(0, lastDollar+1)+classFileName.substring(lastDollar+7);
if (!scannedClasses.contains(classFileName)) {
logger.debug("\tQueueing OT class: "+classFileName);
otClassFileNames.add(classFileName);
}
lastDollar = classFileName.lastIndexOf('$');
}
}
super.loadClass(className, client);
}
};
ObjectTeamsTransformation firstTransformation = new ObjectTeamsTransformation(loader) {};
firstTransformation.checkReadClassAttributes(jpe, cg, cg.getClassName(), cg.getConstantPool());
// transform to class file names:
Collection<String> adaptedBases = firstTransformation.fetchAdaptedBases();
Set<String> adaptedBaseFiles = new HashSet<>();
for (String base : adaptedBases) {
String baseFile = toClassFileName(base);
if (!this.scannedClasses.contains(baseFile)) // ... unless already scanned
adaptedBaseFiles.add(baseFile);
}
// no double recording of referenced classes as OT class and base:
adaptedBaseFiles.removeAll(otClassFileNames);
// log:
for (String baseClassFile : adaptedBaseFiles)
logger.debug("\tScheduling base: "+baseClassFile);
return adaptedBaseFiles;
}
static String toClassFileName(String className) {
return className.replace('.', '/') + ".class";
}
}