spoon.IncrementalLauncher.java Source code

Java tutorial

Introduction

Here is the source code for spoon.IncrementalLauncher.java

Source

/**
 * Copyright (C) 2006-2018 INRIA and contributors
 * Spoon - http://spoon.gforge.inria.fr/
 *
 * This software is governed by the CeCILL-C License under French law and
 * abiding by the rules of distribution of free software. You can use, modify
 * and/or redistribute the software under the terms of the CeCILL-C license as
 * circulated by CEA, CNRS and INRIA at http://www.cecill.info.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the CeCILL-C License for more details.
 *
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL-C license and that you accept its terms.
 */
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.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<File, Set<File>> inputSourcesMap;
    }

    private final Set<File> mInputSources;
    private final File mIncrementalCacheDirectory;
    private final File mModelFile;
    private final File mCacheInfoFile;
    private final File mClassFilesDir;
    private final boolean mChangesPresent;
    private Set<String> mSourceClasspath;
    private Set<File> mRemovedSources = new HashSet<>();
    private Set<File> mAddedSources = new HashSet<>();
    private Set<File> 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 {
            new SerializationModelStreamer().save(factory, new FileOutputStream(file));
        } catch (IOException e) {
            throw new SpoonException("unable to save factory");
        }
    }

    private static Set<File> getAllJavaFiles(Set<File> resources) {
        Set<File> javaFiles = new HashSet<>();
        for (File e : resources) {
            if (e.isDirectory()) {
                Collection<File> 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<File> inputResources, Set<String> 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<File> incrementalSources = new HashSet<>(mAddedSources);
            for (File e : mCommonSources) {
                if (e.lastModified() >= mCacheInfo.lastBuildTime) {
                    incrementalSources.add(e);
                }
            }

            List<CtType<?>> oldTypes = oldFactory.Type().getAll();

            Set<CtType<?>> 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;
                }
                for (CtType<?> changedType : changedTypes) {
                    // We should also rebuild types, that refer to changed types.
                    if (type.getReferencedTypes().contains(changedType.getReference())) {
                        incrementalSources.add(typeFile);
                        type.delete();
                    }
                }
            }

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

            Collection<CtPackage> oldPackages = oldFactory.Package().getAll();
            for (CtPackage pkg : oldPackages) {
                if (pkg.getTypes().isEmpty() && pkg.getPackages().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]));
    }

    /**
     * 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<File> inputResources, Set<String> 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");
        }

        getModelBuilder().compile(SpoonModelBuilder.InputType.FILES);

        saveFactory(factory, mModelFile);

        CacheInfo newCacheInfo = new CacheInfo();
        newCacheInfo.lastBuildTime = System.currentTimeMillis();
        Map<File, Set<File>> newSourcesMap = new HashMap<>();
        for (Entry<String, CompilationUnit> 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<File> 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);
    }
}