eu.numberfour.n4js.runner.RuntimeEnvironmentsHelper.java Source code

Java tutorial

Introduction

Here is the source code for eu.numberfour.n4js.runner.RuntimeEnvironmentsHelper.java

Source

/**
 * Copyright (c) 2016 NumberFour 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
 *
 * Contributors:
 *   NumberFour AG - Initial API and implementation
 */
package eu.numberfour.n4js.runner;

import static com.google.common.collect.FluentIterable.from;
import static org.apache.log4j.Logger.getLogger;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.apache.log4j.Logger;

import com.google.common.base.Optional;
import com.google.inject.Inject;

import eu.numberfour.n4js.n4mf.ProjectType;
import eu.numberfour.n4js.projectModel.IN4JSArchive;
import eu.numberfour.n4js.projectModel.IN4JSCore;
import eu.numberfour.n4js.projectModel.IN4JSProject;
import eu.numberfour.n4js.projectModel.IN4JSSourceContainerAware;
import eu.numberfour.n4js.runner.exceptions.DependencyCycleDetectedException;
import eu.numberfour.n4js.runner.exceptions.InsolvableRuntimeEnvironmentException;
import eu.numberfour.n4js.runner.extension.RuntimeEnvironment;
import eu.numberfour.n4js.validation.helper.SoureContainerAwareDependencyTraverser;

/**
 * Helper that resolves Runtime Environments required to execute given {@link IN4JSProject}.
 */
public class RuntimeEnvironmentsHelper {

    private static final Logger LOGGER = getLogger(RuntimeEnvironmentsHelper.class);

    @Inject
    private IN4JSCore in4jscore;

    /**
     * Returns the project in the current workspace for the given runtime environment ID.
     */
    public Optional<IN4JSProject> findRuntimeEnvironmentProject(RuntimeEnvironment runtimeEnvironment) {
        return from(getAllProjects()).filter(p -> isRuntimeEnvironemnt(p))
                .filter(p -> runtimeEnvironment.getArtifactId().equals(p.getArtifactId())).first();
    }

    /**
     * Analyzes the (transitive) dependencies of the provided {@link IN4JSProject} to check which of the runtime
     * environments defined in the workspace can be used to run that project and returns their IDs in the form of
     * literals from the {@link RuntimeEnvironment} enumeration.
     * <p>
     * More precisely, let P be an N4JS project and RL<sub>P</sub> the set of all runtime libraries that are directly or
     * indirectly <em>required</em> by P or any of its direct or indirect dependencies. Then, a runtime environment RE
     * with RL<sub>RE</sub> being the set of all runtime libraries directly or indirectly <em>provided</em> by RE
     * (including those provided by runtime environments extended by RE) is assumed to support running project P iff RL
     * <sub>P</sub> is a (not necessarily true) subset of RL<sub>RE</sub>. This method will return all runtime
     * environments defined in the workspace that support running the given project.
     *
     * Note: this returned set contains directly compatible environments, and environments they extend.
     *
     * @param project
     *            to analyze
     * @return set of runtime environments defined in the workspace that may be used to run the given project.
     * @throws InsolvableRuntimeEnvironmentException
     *             when called on runtime environment or runtime library.
     * @throws DependencyCycleDetectedException
     *             if the corresponding project has a dependency cycle.
     */
    public Set<RuntimeEnvironment> findCompatibleRuntimeEnvironments(IN4JSProject project) {
        if (isRuntimeEnvironemnt(project) || isRuntimeLibrary(project)) {
            throw new InsolvableRuntimeEnvironmentException(project);
        }
        List<IN4JSProject> reqRuntiemLibraries = collectRequiredRuntimeLibraries(project);
        return from(getAllProjects()).filter(p -> isRuntimeEnvironemnt(p))
                .transform(p -> new AbstractMap.SimpleEntry<>(p, getProjectProvidedRuntimeLibraries(p)))
                .filter(e -> e.getValue().containsAll(reqRuntiemLibraries)).transform(e -> e.getKey())
                .transformAndConcat(re -> getEnvironemntWithAncestors(re))
                .transform(rRE -> RuntimeEnvironment.fromArtifactId(rRE.getProjectName()))
                .filter(rRE -> rRE != null).toSet();
    }

    /**
     * For a given environment returns set containing given environment and all environments it extends. If provided
     * project is not of type {@link ProjectType#RUNTIME_ENVIRONMENT} then returns empty set.
     */
    public Set<IN4JSProject> getEnvironemntWithAncestors(IN4JSProject project) {
        if (!project.getProjectType().equals(ProjectType.RUNTIME_ENVIRONMENT)) {
            return null;
        }
        Set<IN4JSProject> environemtns = new HashSet<>();
        environemtns.add(project);
        getEnvironemntWithAncestorsRecursive(project, environemtns);
        return environemtns;
    }

    private void getEnvironemntWithAncestorsRecursive(IN4JSProject project, Set<IN4JSProject> environemtns) {
        if (!project.getProjectType().equals(ProjectType.RUNTIME_ENVIRONMENT)) {
            return;
        }
        Optional<IN4JSSourceContainerAware> maybePArent = project.getExtendedRuntimeEnvironment();
        if (maybePArent.isPresent()) {
            IN4JSProject parent = (IN4JSProject) maybePArent.get();
            environemtns.add(parent);
            getEnvironemntWithAncestorsRecursive(parent, environemtns);
        }
        return;
    }

    private Iterable<IN4JSProject> getAllProjects() {
        return in4jscore.findAllProjects();
    }

    /**
     * Analyzes all transitive dependencies of the provided provided {@link IN4JSProject} . Collects all dependencies
     * that are of type {@link ProjectType#RUNTIME_LIBRARY}. Resulting collection contains no duplicates.
     *
     * @param project
     *            to be analyzed
     * @return list of transitive dependencies of type runtime library, no duplicates
     */
    private List<IN4JSProject> collectRequiredRuntimeLibraries(IN4JSProject project) {
        if (new SoureContainerAwareDependencyTraverser(project).getResult().hasCycle()) {
            throw new DependencyCycleDetectedException(project);
        }
        Set<IN4JSProject> depsRuntimeLibraries = new HashSet<>();
        recursiveDependencyCollector(project, depsRuntimeLibraries, p -> isRuntimeLibrary(p));
        return new ArrayList<>(depsRuntimeLibraries);
    }

    /**
     * Collects dependencies of the provided source container, by analyzing direct dependencies of the container and
     * recursively their dependencies. Dependencies in form of {@link IN4JSSourceContainerAware} are mapped to
     * {@link IN4JSProject}s, that is instances of {@link IN4JSProject} project are returned as they are, while
     * instances of {@link IN4JSArchive} have contained project extracted.
     *
     * Discovered dependencies are collected only if they pass test specified by provided predicate.
     *
     * @param sourceContainer
     *            whose dependencies will be collected
     * @param collection
     *            where dependencies are collected
     * @param predicate
     *            to test if given dependency should be collected
     */
    private void recursiveDependencyCollector(IN4JSSourceContainerAware sourceContainer,
            Collection<IN4JSProject> collection, Predicate<IN4JSProject> predicate) {

        IN4JSProject project = (extractProject(sourceContainer));

        if (predicate.test(project))
            collection.add(project);

        sourceContainer.getAllDirectDependencies()
                .forEach(dep -> recursiveDependencyCollector(dep, collection, predicate));
    }

    /**
     * Analyzes passed project {@link IN4JSProject#getProvidedRuntimeLibraries()}. All provided project of type
     * {@link ProjectType#RUNTIME_LIBRARY} are stored in transitive list. If project type of passed project is not
     * {@link ProjectType#RUNTIME_ENVIRONMENT} then returns empty list.
     *
     * @param runtimeEnvironment
     *            project to be examined
     * @return transitive list of provided runtime libraries (might be empty, but not null)
     */
    private List<IN4JSProject> getProjectProvidedRuntimeLibraries(IN4JSProject runtimeEnvironment) {
        Set<IN4JSProject> providedRuntimeLibraries = new HashSet<>();

        if (isRuntimeEnvironemnt(runtimeEnvironment))
            recursiveProvidedRuntimeLibrariesCollector(runtimeEnvironment.getProvidedRuntimeLibraries(),
                    providedRuntimeLibraries, p -> isRuntimeLibrary(p));

        // include RLs provided by extended REs
        recursiveCollectRlFromChain(runtimeEnvironment, providedRuntimeLibraries);

        return new ArrayList<>(providedRuntimeLibraries);
    }

    private void recursiveCollectRlFromChain(IN4JSProject runtimeEnvironment, Collection<IN4JSProject> collection) {
        Optional<String> extended = runtimeEnvironment.getExtendedRuntimeEnvironmentName();
        if (extended.isPresent()) {
            String name = extended.get();
            List<IN4JSProject> extendedRE = from(getAllProjects()).filter(p -> name.equals(p.getProjectName()))
                    .toList();

            if (extendedRE.isEmpty()) {
                return;
            }

            if (extendedRE.size() > 1) {
                LOGGER.debug("multiple projects match name " + name);
                LOGGER.error(new RuntimeException("Cannot obtain transitive list of provided libraries"));
                return;
            }

            IN4JSProject extendedRuntimeEnvironemnt = extendedRE.get(0);
            recursiveProvidedRuntimeLibrariesCollector(extendedRuntimeEnvironemnt.getProvidedRuntimeLibraries(),
                    collection, p -> isRuntimeLibrary(p));

            recursiveCollectRlFromChain(extendedRuntimeEnvironemnt, collection);

        }
    }

    /**
     * Maps passed collection of {@link IN4JSSourceContainerAware} to list of {@link IN4JSProject}, that is instances of
     * {@link IN4JSProject} project are returned as they are, while instances of {@link IN4JSArchive} have contained
     * project extracted. For each result of that transformation, examines its
     * {@link IN4JSProject#getProvidedRuntimeLibraries()} to check if they pass predicate test. Instances that do are
     * stored in the passed collection.
     *
     * Calls itself recursively on each processed element, accumulating results in collection passed along call chains.
     *
     * @param runtimeLibraries
     *            list of source containers to analyze (usually of type {@link ProjectType#RUNTIME_LIBRARY})
     * @param collection
     *            where provided runtime libraries are collected
     * @param predicate
     *            to test if provided project is of type {@link ProjectType#RUNTIME_LIBRARY}
     */
    private void recursiveProvidedRuntimeLibrariesCollector(
            com.google.common.collect.ImmutableList<? extends IN4JSSourceContainerAware> runtimeLibraries,
            Collection<IN4JSProject> collection, Predicate<IN4JSProject> predicate) {

        runtimeLibraries.forEach(runtimeLibrary -> {
            IN4JSProject project = (extractProject(runtimeLibrary));
            if (predicate.test(project))
                collection.add(project);
            recursiveProvidedRuntimeLibrariesCollector(project.getProvidedRuntimeLibraries(), collection,
                    predicate);
        });
    }

    /**
     * Map provided source container to instance of {@link IN4JSProject}, that is instances of {@link IN4JSProject}
     * project are returned as they are, while instances of {@link IN4JSArchive} have contained project extracted.
     *
     * @param container
     *            that is mapped to project
     *
     * @return project resulting from mapping
     * @throws RuntimeException
     *             if mapping cannot be performed
     */

    private IN4JSProject extractProject(IN4JSSourceContainerAware container) {
        if (container instanceof IN4JSProject) {
            return (IN4JSProject) container;
        }
        if (container instanceof IN4JSArchive) {
            return ((IN4JSArchive) container).getProject();
        }
        throw new RuntimeException("Unknown instance type of container " + container.getClass().getName());
    }

    /**
     * Compares two provided lists of {@link IN4JSProject}. Assumes both contain instances of type
     * {@link ProjectType#RUNTIME_ENVIRONMENT}. Checks if either all elements of latter list are contained in first one,
     * or if all elements of latter one are compatible (are in extend chain) of elements of the first one. If this check
     * holds returns true, otherwise false (also when either of the lists is empty)
     *
     *
     * @param runnerEnvironments
     *            lists which must contain (might be indirectly via extend chain) elements of latter list
     * @param requiredEnvironments
     *            lists that is checked if it is supported by first one
     * @return true if all elements of latter list are (directly or indirectly) compatible with elements of the first
     *         list.
     */
    public boolean containsAllCompatible(List<RuntimeEnvironment> runnerEnvironments,
            List<RuntimeEnvironment> requiredEnvironments) {

        if (runnerEnvironments.isEmpty() || requiredEnvironments.isEmpty()) {
            LOGGER.debug("cannot compare empty runtime environments lists");
            return false;
        }

        if (runnerEnvironments.containsAll(requiredEnvironments))
            return true;

        // check compatible / extend feature

        boolean result = true;

        List<IN4JSProject> allRuntimeEnvironments = from(getAllProjects()).filter(p -> isRuntimeEnvironemnt(p))
                .toList();

        Map<IN4JSProject, List<String>> reExtendedEnvironments = allRuntimeEnvironments.stream()
                .map(re -> getExtendedRuntimeEnvironmentsNames(re, allRuntimeEnvironments))
                .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));

        // if runnerEnvironments (first param) would be single IN4JSProject (instead of collection)
        // code below could be simplified
        Iterator<RuntimeEnvironment> iterRuntimeEnvironment = runnerEnvironments.iterator();
        while (result && iterRuntimeEnvironment.hasNext()) {
            RuntimeEnvironment re = iterRuntimeEnvironment.next();
            List<IN4JSProject> listExtendedEnvironments = reExtendedEnvironments.keySet().stream()
                    .filter(p -> p.getProjectName().equals(re.getArtifactId())).collect(Collectors.toList());

            if (listExtendedEnvironments.size() != 1) {
                LOGGER.debug("Multiple projects with name " + re.getArtifactId() + " : "
                        + listExtendedEnvironments.stream().map(p -> p.getProjectName()).reduce(new String(),
                                (String r, String e) -> r += ", " + e));
                LOGGER.error("Cannot obtain project for name " + re.getArtifactId());
                return false;
            }

            IN4JSProject extendedRuntimeEnvironment = listExtendedEnvironments.get(0);
            List<String> listExtendedEnvironemntsNames = reExtendedEnvironments.get(extendedRuntimeEnvironment);
            result = result && requiredEnvironments.stream().map(bre -> {
                return bre.getArtifactId();
            }).allMatch(breName -> listExtendedEnvironemntsNames.contains(breName));
        }
        return result;
    }

    /**
     * Analyzes provided list of all runtime environments and creates {@link Entry} for mapping between particular
     * runtime environment and ones it extends.
     *
     * @param runtimeEnv
     *            RE for which mapping is created
     * @param allRuntimeEnv
     *            collection of REs for which are taken into account
     * @return map entry of mapping between RE and REs it extends
     */
    private AbstractMap.SimpleEntry<IN4JSProject, List<String>> getExtendedRuntimeEnvironmentsNames(
            IN4JSProject runtimeEnv, List<IN4JSProject> allRuntimeEnv) {
        Set<String> depsRuntimeLibraries = new HashSet<>();
        recursiveCompatibleEnvironemntCollector(runtimeEnv, depsRuntimeLibraries, p -> isRuntimeEnvironemnt(p),
                allRuntimeEnv);
        return new AbstractMap.SimpleEntry<>(runtimeEnv, new ArrayList<>(depsRuntimeLibraries));
    }

    /**
     * recursively searches given source container for provided runtime environments
     */
    private void recursiveCompatibleEnvironemntCollector(IN4JSSourceContainerAware sourceContainer,
            Collection<String> collection, Predicate<IN4JSProject> predicate, List<IN4JSProject> allRuntimeEnv) {

        IN4JSProject project = (extractProject(sourceContainer));

        if (predicate.test(project)) {
            com.google.common.base.Optional<String> oEextendedProjectName = project
                    .getExtendedRuntimeEnvironmentName();

            if (!oEextendedProjectName.isPresent()) {
                return;
            }

            String extendedProjectName = oEextendedProjectName.get();
            collection.add(extendedProjectName);
            allRuntimeEnv.stream().filter(p -> p.getProjectName().equals(extendedProjectName)).findFirst()
                    .ifPresent(exre -> recursiveCompatibleEnvironemntCollector(exre, collection, predicate,
                            allRuntimeEnv));

        }
    }

    private boolean isRuntimeEnvironemnt(IN4JSProject project) {
        return ProjectType.RUNTIME_ENVIRONMENT.equals(project.getProjectType());
    }

    private boolean isRuntimeLibrary(IN4JSProject project) {
        return ProjectType.RUNTIME_LIBRARY.equals(project.getProjectType());
    }
}