All Downloads are FREE. Search and download functionalities are using the official Maven repository.

spoon.IncrementalLauncher Maven / Gradle / Ivy

Go to download

Spoon is a tool for meta-programming, analysis and transformation of Java programs.

There is a newer version: 11.1.1-beta-14
Show newest version
/*
 * SPDX-License-Identifier: (MIT OR CECILL-C)
 *
 * Copyright (C) 2006-2023 INRIA and contributors
 *
 * Spoon is available either under the terms of the MIT License (see LICENSE-MIT.txt) or the Cecill-C License (see LICENSE-CECILL-C.txt). You as the user are entitled to choose the terms under which to adopt Spoon.
 */
package spoon;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.codehaus.plexus.util.CollectionUtils;

import spoon.reflect.cu.CompilationUnit;
import spoon.reflect.declaration.CtPackage;
import spoon.reflect.declaration.CtType;
import spoon.reflect.factory.Factory;
import spoon.reflect.reference.CtTypeReference;
import spoon.support.SerializationModelStreamer;

/**
 * Create a Spoon launcher for incremental build
 */
public class IncrementalLauncher extends Launcher {

	private static class CacheInfo implements Serializable {
		/** Cache version */
		public static final long serialVersionUID = 1L; //TODO: Spoon version
		/** Timestamp of the last model build */
		public long lastBuildTime;
		/** Map of input source files and corresponding binary files */
		public Map> inputSourcesMap;
	}

	private final Set mInputSources;
	private final File mIncrementalCacheDirectory;
	private final File mModelFile;
	private final File mCacheInfoFile;
	private final File mClassFilesDir;
	private final boolean mChangesPresent;
	private Set mSourceClasspath;
	private Set mRemovedSources = new HashSet<>();
	private Set mAddedSources = new HashSet<>();
	private Set mCommonSources = new HashSet<>();
	private CacheInfo mCacheInfo = null;

	private static CacheInfo loadCacheInfo(File file) throws InvalidClassException {
		try (FileInputStream fileStream = new FileInputStream(file);
			ObjectInputStream objectStream = new ObjectInputStream(new BufferedInputStream(fileStream))) {
			return (CacheInfo) objectStream.readObject();
		} catch (InvalidClassException e) {
			throw e;
		} catch (ClassNotFoundException | IOException e) {
			throw new SpoonException("unable to load cache info");
		}
	}

	private static void saveCacheInfo(CacheInfo cacheInfo, File file) {
		try (FileOutputStream fileStream = new FileOutputStream(file);
			ObjectOutputStream objectStream = new ObjectOutputStream(new BufferedOutputStream(fileStream))) {
			objectStream.writeObject(cacheInfo);
			objectStream.flush();
		} catch (IOException e) {
			throw new SpoonException("unable to save cache info");
		}
	}

	private static Factory loadFactory(File file) {
		try {
			return new SerializationModelStreamer().load(new FileInputStream(file));
		} catch (IOException e) {
			throw new SpoonException("unable to load factory from cache");
		}
	}

	private static void saveFactory(Factory factory, File file) {
		try (FileOutputStream fileStream = new FileOutputStream(file)) {
			new SerializationModelStreamer().save(factory, fileStream);
		} catch (IOException e) {
			throw new SpoonException("unable to save factory");
		}
	}

	private static Set getAllJavaFiles(Set resources) {
		Set javaFiles = new HashSet<>();
		for (File e : resources) {
			if (e.isDirectory()) {
				Collection files = FileUtils.listFiles(e, new SuffixFileFilter(".java"), TrueFileFilter.INSTANCE);
				files.forEach(f -> {
					try {
						javaFiles.add(f.getCanonicalFile());
					} catch (IOException e1) {
						throw new SpoonException("unable to locate input source file: " + f);
					}
				});
			} else if (e.isFile() && e.getName().endsWith(".java")) {
				try {
					javaFiles.add(e.getCanonicalFile());
				} catch (IOException e1) {
					throw new SpoonException("unable to locate input source file: " + e);
				}
			}
		}
		return javaFiles;
	}

	/**
	 * Creates a {@link Launcher} for incremental build.
	 * @param inputResources Resources to be parsed to build the spoon model.
	 * @param sourceClasspath Source classpath of the spoon model.
	 * @param cacheDirectory The directory to store all incremental information. If it's empty, full rebuild will be performed.
	 * @param forceRebuild Force to perform full rebuild, ignoring incremental cache.
	 * @throws IllegalArgumentException
	 * @throws SpoonException
	 */
	public IncrementalLauncher(Set inputResources, Set sourceClasspath, File cacheDirectory, boolean forceRebuild) {
		if (cacheDirectory == null) {
			throw new IllegalArgumentException("unable to create incremental launcher with null cache directory");
		}

		mInputSources = getAllJavaFiles(inputResources);
		mSourceClasspath = new HashSet<>(sourceClasspath);
		mIncrementalCacheDirectory = cacheDirectory;
		mModelFile = new File(cacheDirectory, "model");
		mCacheInfoFile = new File(cacheDirectory, "cache-info");
		mClassFilesDir = new File(cacheDirectory, "class-files");

		if (!mIncrementalCacheDirectory.exists() || !mModelFile.exists() || !mCacheInfoFile.exists() || !mClassFilesDir.exists()) {
			forceRebuild = true;
		} else {
			try {
				mCacheInfo = loadCacheInfo(mCacheInfoFile);
			} catch (InvalidClassException | SpoonException e) {
				// Incompatible cache version or unable to load cache. So force rebuild.
				forceRebuild = true;
			}
		}

		if (!mIncrementalCacheDirectory.exists() && !mIncrementalCacheDirectory.mkdirs()) {
				throw new SpoonException("unable to create cache directory");
		}

		if (!mClassFilesDir.exists() && !mClassFilesDir.mkdirs()) {
				throw new SpoonException("unable to create class files directory");
		}

		if (forceRebuild) {
			// Build model from scratch.
			factory = createFactory();
			processArguments();
			mInputSources.forEach(f -> addInputResource(f.getPath()));
			mChangesPresent = true;
			setBinaryOutputDirectory(mClassFilesDir);
		} else {
			// Load model from cache.
			Factory oldFactory = loadFactory(mModelFile);
			oldFactory.getModel().setBuildModelIsFinished(false);

			// Build model incrementally.
			mRemovedSources = new HashSet<>(CollectionUtils.subtract(mCacheInfo.inputSourcesMap.keySet(), mInputSources));
			mAddedSources = new HashSet<>(CollectionUtils.subtract(mInputSources, mCacheInfo.inputSourcesMap.keySet()));
			mCommonSources = new HashSet<>(CollectionUtils.intersection(mCacheInfo.inputSourcesMap.keySet(), mInputSources));

			Set incrementalSources = new HashSet<>(mAddedSources);
			for (File e : mCommonSources) {
				if (e.lastModified() >= mCacheInfo.lastBuildTime) {
					incrementalSources.add(e);
				}
			}

			List> oldTypes = oldFactory.Type().getAll();

			Set> changedTypes = new HashSet<>();
			for (CtType type : oldTypes) {
				File typeFile = type.getPosition().getFile();
				if (incrementalSources.contains(typeFile)) {
					changedTypes.add(type);
				}
			}

			for (CtType type : oldTypes) {
				File typeFile = type.getPosition().getFile();
				if (mRemovedSources.contains(typeFile)) {
					type.delete();
					continue;
				}
				// If a type refers to some changed type, we should rebuild it.
				Set> referencedTypes = type.getReferencedTypes();
				for (CtType changedType : changedTypes) {
					if (referencedTypes.contains(changedType.getReference())) {
						incrementalSources.add(typeFile);
						type.delete();
						break;
					}
				}
			}

			try {
				mSourceClasspath.add(mClassFilesDir.getCanonicalPath());
			} catch (IOException e2) {
				throw new SpoonException("unable to locate class files dir: " + mClassFilesDir);
			}

			Collection oldPackages = oldFactory.Package().getAll();
			for (CtPackage pkg : oldPackages) {
				if (pkg.isEmpty() && !pkg.isUnnamedPackage()) {
					pkg.delete();
				}
			}

			factory = oldFactory;
			processArguments();
			incrementalSources.forEach(f ->  addInputResource(f.getPath()));
			mChangesPresent = !mRemovedSources.isEmpty() || !mAddedSources.isEmpty() || !incrementalSources.isEmpty();
			setBinaryOutputDirectory(mClassFilesDir);
		}

		getEnvironment().setSourceClasspath(mSourceClasspath.toArray(new String[0]));

		this.getEnvironment().setShouldCompile(true);
	}

	/**
	 * Creates a {@link Launcher} for incremental build.
	 * @param inputResources Resources to be parsed to build the spoon model.
	 * @param sourceClasspath Source classpath of the spoon model.
	 * @param cacheDirectory The directory to store all incremental information. If it's empty, full rebuild will be performed.
	 * @throws IllegalArgumentException
	 * @throws SpoonException
	 */
	public IncrementalLauncher(Set inputResources, Set sourceClasspath, File cacheDirectory) {
		this(inputResources, sourceClasspath, cacheDirectory, false);
	}

	/** Returns true, if any source code changes after previous build are present, and false otherwise. */
	public boolean changesPresent() {
		return mChangesPresent;
	}

	/** Caches current spoon model and binary files. Should be called only after model is built. */
	public void saveCache() {
		if (mIncrementalCacheDirectory == null) {
			throw new SpoonException("incremental cache directory is null");
		}

		Factory factory = getFactory();
		if (factory == null) {
			throw new SpoonException("factory is null");
		}

		if (this.getEnvironment().shouldCompile()) {
			getModelBuilder().compile(SpoonModelBuilder.InputType.FILES);
		}

		saveFactory(factory, mModelFile);

		CacheInfo newCacheInfo = new CacheInfo();
		newCacheInfo.lastBuildTime = System.currentTimeMillis();
		Map> newSourcesMap = new HashMap<>();
		for (Entry e : factory.CompilationUnit().getMap().entrySet()) {
			newSourcesMap.put(new File(e.getKey()), new HashSet<>(e.getValue().getBinaryFiles()));
		}

		if (mCacheInfo != null) {
			newSourcesMap.putAll(mCacheInfo.inputSourcesMap);
			for (File r : mRemovedSources) {
				newSourcesMap.get(r).forEach(File::delete); // Removes corresponding .class files
				newSourcesMap.remove(r);
			}
		}

		// Removes all empty directories
		Collection dirs = FileUtils.listFilesAndDirs(mClassFilesDir, DirectoryFileFilter.INSTANCE, TrueFileFilter.INSTANCE);
		dirs.stream()
			.filter(d -> d.exists() && FileUtils.listFiles(d, TrueFileFilter.INSTANCE, TrueFileFilter.INSTANCE).isEmpty())
			.forEach(FileUtils::deleteQuietly);

		newCacheInfo.inputSourcesMap = newSourcesMap;
		saveCacheInfo(newCacheInfo, mCacheInfoFile);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy