eu.numberfour.n4js.external.EclipseExternalLibraryWorkspace.java Source code

Java tutorial

Introduction

Here is the source code for eu.numberfour.n4js.external.EclipseExternalLibraryWorkspace.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.external;

import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Predicates.notNull;
import static com.google.common.collect.FluentIterable.from;
import static com.google.common.collect.Iterables.addAll;
import static com.google.common.collect.Iterators.emptyIterator;
import static com.google.common.collect.Iterators.unmodifiableIterator;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Maps.newTreeMap;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.newHashSet;
import static com.google.common.collect.Sets.newLinkedHashSet;
import static eu.numberfour.n4js.internal.N4JSSourceContainerType.PROJECT;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableCollection;
import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace;
import static org.eclipse.core.runtime.SubMonitor.convert;

import java.io.File;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import org.apache.log4j.Logger;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.xtext.util.Pair;

import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import eu.numberfour.n4js.internal.N4JSSourceContainerType;
import eu.numberfour.n4js.n4mf.ProjectDescription;
import eu.numberfour.n4js.n4mf.ProjectReference;
import eu.numberfour.n4js.preferences.ExternalLibraryPreferenceStore;
import eu.numberfour.n4js.preferences.ExternalLibraryPreferenceStore.ExternalProjectLocationsProvider;
import eu.numberfour.n4js.preferences.ExternalLibraryPreferenceStore.StoreUpdatedListener;
import eu.numberfour.n4js.utils.Procedure;
import eu.numberfour.n4js.utils.resources.ExternalProject;
import eu.numberfour.n4js.utils.resources.IExternalResource;

/**
 * The Eclipse based implementation of the external library workspace. This class assumes a running {@link Platform
 * platform}.
 */
@Singleton
public class EclipseExternalLibraryWorkspace extends ExternalLibraryWorkspace implements StoreUpdatedListener {

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

    @Inject
    private ExternalProjectCacheLoader cacheLoader;

    @Inject
    private ProjectStateChangeListener projectStateChangeListener;

    @Inject
    private ExternalLibraryBuilderHelper builderHelper;

    @Inject
    private ExternalProjectsCollector collector;

    @Inject
    private RebuildWorkspaceProjectsScheduler scheduler;

    private LoadingCache<URI, Optional<Pair<ExternalProject, ProjectDescription>>> projectCache;

    private Map<String, ExternalProject> projectMapping;

    private final Collection<java.net.URI> locations;

    /**
     * Creates a new external library workspace instance with the preference store that provides the configured library
     * location.
     *
     * @param preferenceStore
     *            the preference store to get the registered external library locations.
     */
    @Inject
    public EclipseExternalLibraryWorkspace(final ExternalLibraryPreferenceStore preferenceStore) {
        locations = newHashSet(preferenceStore.getLocations());
        preferenceStore.addListener(this);
    }

    /**
     * Initializes the backing cache with the cache loader and registers a {@link ProjectStateChangeListener} into the
     * workspace.
     */
    @Inject
    public void init() {
        projectCache = CacheBuilder.newBuilder().build(cacheLoader);
        if (Platform.isRunning()) {
            getWorkspace().addResourceChangeListener(projectStateChangeListener);
        }
    }

    @Override
    public ProjectDescription getProjectDescription(URI location) {
        final Pair<ExternalProject, ProjectDescription> pair = get(location);
        return null == pair ? null : pair.getSecond();
    }

    @Override
    public URI getLocation(URI projectURI, ProjectReference reference,
            N4JSSourceContainerType expectedN4JSSourceContainerType) {

        if (PROJECT.equals(expectedN4JSSourceContainerType)) {

            final String name = reference.getProject().getArtifactId();
            final ExternalProject project = getProjectMapping().get(name);

            if (null == project) {
                return null;
            }

            final File referencedProject = new File(project.getLocationURI());
            final URI referencedLocation = URI.createFileURI(referencedProject.getAbsolutePath());
            final Pair<ExternalProject, ProjectDescription> pair = get(referencedLocation);
            if (null != pair) {
                return referencedLocation;
            }

        }

        return null;
    }

    @Override
    public Iterator<URI> getArchiveIterator(URI archiveLocation, String archiveRelativeLocation) {
        return emptyIterator();
    }

    @Override
    public Iterator<URI> getFolderIterator(URI folderLocation) {

        final URI findProjectWith = findProjectWith(folderLocation);
        if (null != findProjectWith) {
            final String projectName = findProjectWith.lastSegment();
            final ExternalProject project = getProjectMapping().get(projectName);
            if (null != project) {
                String projectPath = new File(project.getLocationURI()).getAbsolutePath();
                String folderPath = folderLocation.toFileString();
                final IContainer container = projectPath.equals(folderPath) ? project
                        : project.getFolder(folderPath.substring(projectPath.length() + 1));
                final Collection<URI> result = Lists.newLinkedList();
                try {
                    container.accept(resource -> {
                        if (resource instanceof IFile) {
                            final String path = new File(resource.getLocationURI()).getAbsolutePath();
                            result.add(URI.createFileURI(path));
                        }
                        return true;
                    });
                    return unmodifiableIterator(result.iterator());
                } catch (CoreException e) {
                    return unmodifiableIterator(result.iterator());
                }
            }
        }

        return emptyIterator();
    }

    @Override
    public URI findArtifactInFolder(URI folderLocation, String folderRelativePath) {

        final IResource folder = getResource(folderLocation);
        if (folder instanceof IFolder) {
            final IFile file = ((IFolder) folder).getFile(folderRelativePath);
            if (file instanceof IExternalResource) {
                final File externalResource = ((IExternalResource) file).getExternalResource();
                return URI.createFileURI(externalResource.getAbsolutePath());
            }
        }

        return null;
    }

    @Override
    public URI findProjectWith(URI nestedLocation) {

        final String path = nestedLocation.toFileString();
        if (null == path) {
            return null;
        }

        final File nestedResource = new File(path);
        if (!nestedResource.exists()) {
            return null;
        }

        final Path nestedResourcePath = nestedResource.toPath();

        final Iterable<URI> registeredProjectUris = projectCache.asMap().keySet();
        for (final URI projectUri : registeredProjectUris) {
            if (projectUri.isFile()) {
                final File projectRoot = new File(projectUri.toFileString());
                final Path projectRootPath = projectRoot.toPath();
                if (nestedResourcePath.startsWith(projectRootPath)) {
                    return projectUri;
                }
            }
        }

        return null;
    }

    @Override
    public void storeUpdated(final ExternalLibraryPreferenceStore store, final IProgressMonitor monitor) {

        final Set<java.net.URI> oldLocations = newLinkedHashSet(locations);
        final Set<java.net.URI> newLocation = newLinkedHashSet(store.getLocations());
        final Collection<java.net.URI> removedLocations = difference(oldLocations, newLocation);
        final Collection<java.net.URI> addedLocations = difference(newLocation, oldLocations);

        final SubMonitor subMonitor = convert(monitor, 3);

        final Iterable<IProject> projectsToClean = getProjects(removedLocations);
        final Collection<IProject> workspaceProjectsToRebuild = newHashSet(
                collector.collectProjectsWithDirectExternalDependencies(projectsToClean));

        // Clean projects.
        if (!Iterables.isEmpty(projectsToClean)) {
            builderHelper.clean(projectsToClean, subMonitor.newChild(1));
        }
        subMonitor.worked(1);

        // Update internal state.
        locations.clear();
        locations.addAll(store.getLocations());
        updateState();

        // Rebuild whole external workspace. Filter out projects that are present in the Eclipse workspace (if any).
        final Collection<String> eclipseWorkspaceProjectNames = getAllEclipseWorkspaceProjectNames();
        final Predicate<String> eclipseWorkspaceProjectNamesFilter = Predicates.in(eclipseWorkspaceProjectNames);

        final Iterable<IProject> projectsToBuild = from(getProjects(addedLocations))
                .filter(p -> !eclipseWorkspaceProjectNamesFilter.apply(p.getName()));

        // Build recently added projects that do not exist in workspace.
        // XXX akitta: consider filtering out external projects that exists in index already. (@ higher priority level)
        if (!Iterables.isEmpty(projectsToBuild)) {
            builderHelper.build(projectsToBuild, subMonitor.newChild(1));
        }
        subMonitor.worked(1);

        addAll(workspaceProjectsToRebuild,
                collector.collectProjectsWithDirectExternalDependencies(projectsToBuild));
        scheduler.scheduleBuildIfNecessary(workspaceProjectsToRebuild);

    }

    @Override
    public void registerProjects(NpmProjectAdaptionResult result, IProgressMonitor monitor) {

        checkState(result.isOK(), "Expected OK result: " + result);

        final SubMonitor subMonitor = convert(monitor, 3);

        final Iterable<IProject> projectsToClean = from(result.getToBeBuilt().getToBeDeleted())
                .transform(uri -> getProject(new File(uri).getName())).filter(notNull());

        final Collection<IProject> workspaceProjectsToRebuild = newHashSet(
                collector.collectProjectsWithDirectExternalDependencies(projectsToClean));

        // Clean projects.
        if (!Iterables.isEmpty(projectsToClean)) {
            builderHelper.clean(projectsToClean, subMonitor.newChild(1));
        }
        subMonitor.worked(1);

        // Update internal state.
        updateState();

        // Rebuild whole external workspace. Filter out projects that are present in the Eclipse workspace (if any).
        final Collection<String> eclipseWorkspaceProjectNames = getAllEclipseWorkspaceProjectNames();
        final Predicate<String> eclipseWorkspaceProjectNamesFilter = Predicates.in(eclipseWorkspaceProjectNames);

        final Iterable<IProject> projectsToBuild = from(result.getToBeBuilt().getToBeUpdated())
                .transform(uri -> getProject(new File(uri).getName())).filter(notNull())
                .filter(p -> !eclipseWorkspaceProjectNamesFilter.apply(p.getName()));

        // Build recently added projects that do not exist in workspace.
        // XXX akitta: consider filtering out external projects that exists in index already. (@ higher priority level)
        if (!Iterables.isEmpty(projectsToBuild)) {
            builderHelper.build(projectsToBuild, subMonitor.newChild(1));
        }
        subMonitor.worked(1);

        addAll(workspaceProjectsToRebuild,
                collector.collectProjectsWithDirectExternalDependencies(projectsToBuild));
        scheduler.scheduleBuildIfNecessary(workspaceProjectsToRebuild);

    }

    @Override
    public Iterable<IProject> getProjects() {
        final Map<String, ExternalProject> projects = getProjectMapping();
        return unmodifiableCollection(projects.values());
    }

    @Override
    public Iterable<IProject> getProjects(java.net.URI rootLocation) {

        final File rootFolder = new File(rootLocation);

        final Map<String, IProject> projectsMapping = newTreeMap();
        final URI rootUri = URI.createFileURI(rootFolder.getAbsolutePath());

        for (Entry<URI, Optional<Pair<ExternalProject, ProjectDescription>>> entry : projectCache.asMap()
                .entrySet()) {
            final URI projectLocation = entry.getKey();
            if (rootUri.equals(projectLocation.trimSegments(1))) {
                final Pair<ExternalProject, ProjectDescription> pair = entry.getValue().orNull();
                if (null != pair && null != pair.getFirst()) {
                    final ExternalProject project = pair.getFirst();
                    projectsMapping.put(project.getName(), project);
                }
            }
        }

        return unmodifiableCollection(projectsMapping.values());

    }

    @Override
    public IProject getProject(final String projectName) {
        return getProjectMapping().get(projectName);
    }

    @Override
    public IResource getResource(URI location) {

        final String path = location.toFileString();
        if (null == path) {
            return null;
        }
        final File nestedResource = new File(path);
        if (nestedResource.exists()) {
            final URI projectLocation = findProjectWith(location);
            if (null != projectLocation) {
                final String projectName = projectLocation.lastSegment();
                final IProject project = getProject(projectName);
                if (project instanceof ExternalProject) {
                    final File projectResource = new File(project.getLocationURI());
                    if (projectResource.exists() && projectResource.isDirectory()) {

                        final Path projectPath = projectResource.toPath();
                        final Path nestedPath = nestedResource.toPath();

                        if (projectPath.equals(nestedPath)) {
                            return project;
                        }

                        final Path relativePath = projectPath.relativize(nestedPath);
                        final IFile file = project.getFile(relativePath.toString());
                        if (null != file) {
                            return file;
                        }

                        return project.getFolder(relativePath.toString());
                    }
                }
            }
        }

        return null;
    }

    /**
     * Updates the internal state based on the available external project root locations.
     *
     * <p>
     * This cannot be done in construction time, because it might happen that N4MF is not initialized yet, hence not
     * available when injecting this instance.
     */
    @Override
    public void updateState() {
        projectCache.invalidateAll();
        visitAllExternalProjects(locations, new Procedure<File>() {

            @Override
            public void doApply(File projectRoot) {
                final URI location = URI.createFileURI(projectRoot.getAbsolutePath());
                final Pair<ExternalProject, ProjectDescription> pair = get(location);
                if (null == pair) { // Removed trash.
                    projectCache.invalidate(location);
                }
            }
        });
        final Map<String, ExternalProject> artifactIdProjectMap = newHashMap();
        final Map<URI, Optional<Pair<ExternalProject, ProjectDescription>>> availableProjects = projectCache
                .asMap();
        visitAllExternalProjects(locations, new Procedure<File>() {

            @Override
            public void doApply(File input) {
                final URI projectLocation = URI.createFileURI(input.getAbsolutePath());
                if (availableProjects.containsKey(projectLocation)) {
                    Pair<ExternalProject, ProjectDescription> pair = availableProjects.get(projectLocation)
                            .orNull();
                    if (null != pair) {
                        final ExternalProject project = pair.getFirst();
                        if (!artifactIdProjectMap.containsKey(project.getName())) {
                            artifactIdProjectMap.put(project.getName(), project);
                        }
                    }
                }
            }
        });
        projectMapping = Collections.unmodifiableMap(artifactIdProjectMap);
    }

    private Iterable<IProject> getProjects(final Iterable<java.net.URI> rootLocations) {
        return from(ExternalProjectLocationsProvider.INSTANCE.convertToProjectRootLocations(rootLocations))
                .transform(uri -> URI.createFileURI(new File(uri).getAbsolutePath())).transform(uri -> get(uri))
                .filter(pair -> null != pair).transform(pair -> pair.getFirst())
                .filter(project -> null != project && project.exists()).filter(IProject.class);
    }

    private Map<String, ExternalProject> getProjectMapping() {

        if (null == projectMapping) {
            synchronized (this) {
                if (null == projectMapping) {
                    updateState();
                }
            }
        }

        return projectMapping;
    }

    private void visitAllExternalProjects(Iterable<java.net.URI> rootLocations, Procedure<File> procedure) {
        for (java.net.URI projectRoot : ExternalProjectLocationsProvider.INSTANCE
                .convertToProjectRootLocations(rootLocations)) {
            procedure.apply(new File(projectRoot));
        }
    }

    private Pair<ExternalProject, ProjectDescription> get(URI location) {
        try {
            return projectCache.get(location).orNull();
        } catch (ExecutionException e) {
            final String message = "Error while getting external project with description for location: "
                    + location;
            LOGGER.error(message, e);
            return null;
        }

    }

    private Collection<String> getAllEclipseWorkspaceProjectNames() {
        if (Platform.isRunning()) {
            return from(Arrays.asList(getWorkspace().getRoot().getProjects())).filter(p -> p.isAccessible())
                    .transform(p -> p.getName()).toSet();
        }
        return emptyList();
    }

}