eu.numberfour.n4js.scoping.utils.ProjectImportEnablingScope.java Source code

Java tutorial

Introduction

Here is the source code for eu.numberfour.n4js.scoping.utils.ProjectImportEnablingScope.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.scoping.utils;

import static eu.numberfour.n4js.N4JSGlobals.JS_FILE_EXTENSION;
import static eu.numberfour.n4js.N4JSGlobals.N4JSD_FILE_EXTENSION;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.naming.QualifiedName;
import org.eclipse.xtext.resource.EObjectDescription;
import org.eclipse.xtext.resource.IEObjectDescription;
import org.eclipse.xtext.scoping.IScope;
import org.eclipse.xtext.xbase.lib.IterableExtensions;

import com.google.common.base.Optional;
import com.google.common.collect.Iterables;

import eu.numberfour.n4js.n4JS.ImportDeclaration;
import eu.numberfour.n4js.projectModel.IN4JSCore;
import eu.numberfour.n4js.projectModel.IN4JSProject;
import eu.numberfour.n4js.validation.IssueCodes;
import eu.numberfour.n4js.n4mf.ProjectDescription;
import eu.numberfour.n4js.xtext.scoping.IEObjectDescriptionWithError;

/**
 * Normally we import from a module by only supplying the module specifier without the artifact ID of the containing
 * project:
 *
 * <pre>
 * import * as N from "some/path/to/Module"
 * </pre>
 *
 * By using this wrapping scope, we obtain support for two additional forms of import (called "project imports"):
 *
 * <pre>
 * import * as N from "artifactId/some/path/to/Module"
 * </pre>
 *
 * and
 *
 * <pre>
 * import * as N from "artifactId"
 * </pre>
 *
 * In both cases, <code>artifactId</code>> stands for the artifact ID of the project containing the module to import
 * from. The last case is only allowed if the containing project has defined the <code>MainModule</code> property in its
 * manifest and means that this main module will then be imported.
 *
 *
 * Since there may exist multiple <code>MainModule</code> instances in the workspace with the same name (but not in one
 * project!) this scope uses two external sources of elements. In case of imports not specifying target project
 * artifactID we use provided <code>parent</code> as source of elements in scope. In case of imports specifying target
 * project artefactID we use <code>delegate</code> scope as source of elements. That distinction is necessary as
 * {@link IScope#getElements(EObject)} surprisingly performs filtering of elements with the same name. We assume that
 * provided <code>delegate</code> is not doing that, at least not for the main modules.
 */
public class ProjectImportEnablingScope implements IScope {

    private final IN4JSCore n4jsCore;
    private final IN4JSProject contextProject;
    private final ImportDeclaration importDeclaration;
    private final IScope parent;
    private final IScope delegate;

    /**
     * Wraps the given parent scope to enable project imports (see {@link ProjectImportEnablingScope} for details).
     * <p>
     * To support tests that use multiple projects without properly setting up IN4JSCore, we simply return 'parent' in
     * such cases; however, project imports will not be available in such tests.
     */
    public static IScope create(IN4JSCore n4jsCore, ImportDeclaration importDecl, IScope parent, IScope delegate) {
        if (n4jsCore == null || importDecl == null || parent == null) {
            throw new IllegalArgumentException("none of the arguments may be null");
        }
        final Resource resource = importDecl.eResource();
        if (resource == null) {
            throw new IllegalArgumentException("given import declaration must be contained in a resource");
        }
        final Optional<? extends IN4JSProject> contextProject = n4jsCore.findProject(resource.getURI());
        if (!contextProject.isPresent()) {
            // we failed to obtain an IN4JSProject for the project containing 'importDecl'
            // -> it would be best to throw an exception in this case, but we have many tests that use multiple projects
            // without properly setting up the IN4JSCore; to not break those tests, we return 'parent' here
            return parent;
        }
        return new ProjectImportEnablingScope(n4jsCore, contextProject.get(), importDecl, parent, delegate);
    }

    /**
     *
     * @param contextProject
     *            the project containing the import declaration (not the project containing the module to import from)!
     */
    private ProjectImportEnablingScope(IN4JSCore n4jsCore, IN4JSProject contextProject,
            ImportDeclaration importDecl, IScope parent, IScope delegate) {
        if (n4jsCore == null || contextProject == null || parent == null) {
            throw new IllegalArgumentException("none of the arguments may be null");
        }
        this.n4jsCore = n4jsCore;
        this.contextProject = contextProject;
        this.parent = parent;
        this.importDeclaration = importDecl;
        this.delegate = delegate;
    }

    @Override
    public IEObjectDescription getSingleElement(QualifiedName name) {
        final Iterable<IEObjectDescription> result = getElements(name);
        int size = Iterables.size(result);
        if (size == 1) {
            return result.iterator().next();
        }

        // Special case handling when we have a definition and a pure JS file in the score.
        // In such cases we return with the description that corresponds to the definition file.
        if (size == 2) {

            final IEObjectDescription first = Iterables.get(result, 0);
            final IEObjectDescription second = Iterables.get(result, 1);

            final String firstExtension = first.getEObjectURI().fileExtension();
            final String secondExtension = second.getEObjectURI().fileExtension();

            if (JS_FILE_EXTENSION.equals(firstExtension) && N4JSD_FILE_EXTENSION.equals(secondExtension)) {
                return second;
            }

            if (N4JSD_FILE_EXTENSION.equals(firstExtension) && JS_FILE_EXTENSION.equals(secondExtension)) {
                return first;
            }

        }

        // handle error cases to help user fix the issue
        StringBuilder sbErrrorMessage = new StringBuilder("Cannot resolve import target ::");

        ImportType importType = computeImportType(name, this.contextProject);
        switch (importType) {
        case PROJECT_IMPORT:
            sbErrrorMessage.append(" resolving project import :");
            break;
        case COMPLETE_IMPORT:
            sbErrrorMessage.append(" resolving full module import :");
            break;
        case SIMPLE_IMPORT:
            sbErrrorMessage.append(" resolving simple module import :");
            break;
        case PROJECT_IMPORT_NO_MAIN:
            sbErrrorMessage.append(" no main module in target project");
            break;
        default:
            sbErrrorMessage.append(" unrecognized import structure :");
            break;
        }

        if (!importType.equals(ImportType.PROJECT_IMPORT_NO_MAIN)) {
            if (size == 0) {
                sbErrrorMessage.append(" found no matching modules");
            } else {
                sbErrrorMessage.append(" found multiple matching modules ");
                sbErrrorMessage.append(IterableExtensions.join(result, ","));
            }
        }

        return new InvalidImportTargetModuleDescription(
                EObjectDescription.create("impDecl", this.importDeclaration), sbErrrorMessage.toString(),
                IssueCodes.IMP_UNRESOLVED);

    }

    @Override
    public Iterable<IEObjectDescription> getElements(QualifiedName name) {

        switch (computeImportType(name, this.contextProject)) {
        case PROJECT_IMPORT:
            final String firstSegment = name.getFirstSegment();
            final IN4JSProject targetProject = findProject(firstSegment, contextProject);
            return getElementsWithDesiredProjectID(getMainModuleOfProject(targetProject), name.getFirstSegment());
        case COMPLETE_IMPORT:
            return getElementsWithDesiredProjectID(name.skipFirst(1), name.getFirstSegment());
        case SIMPLE_IMPORT:
            return parent.getElements(name);

        default:
            return Collections.emptyList();
        }
    }

    @Override
    public IEObjectDescription getSingleElement(EObject object) {
        return parent.getSingleElement(object);
    }

    @Override
    public Iterable<IEObjectDescription> getElements(EObject object) {
        return parent.getElements(object);
    }

    @Override
    public Iterable<IEObjectDescription> getAllElements() {
        return parent.getAllElements();
    }

    /**
     * Internal Distinction of different import types.
     *
     * Although in the AST we don't make distinction, internally we need to handle different types of import.
     */
    private static enum ImportType {
        /** import specifies only target project name, we expect to import from main module */
        PROJECT_IMPORT,
        /** import specifies target project name and concrete module, we expect to import from that particular module */
        COMPLETE_IMPORT,
        /** import specifies no target project name, we expect to import some module */
        SIMPLE_IMPORT,
        /**
         * Error case : it looked like {@link ImportType#PROJECT_IMPORT} but target project has no
         * {@link ProjectDescription#getMainModule()}
         */
        PROJECT_IMPORT_NO_MAIN;
    }

    /** Custom {@link IEObjectDescriptionWithError} for nicer user message */
    public static class InvalidImportTargetModuleDescription extends AbstractDescriptionWithError {
        private final String message;
        private final String issueCode;

        @SuppressWarnings("javadoc")
        public InvalidImportTargetModuleDescription(IEObjectDescription delegate, String message,
                String issueCode) {
            super(delegate);
            this.issueCode = issueCode;
            this.message = message;
        }

        @Override
        public String getMessage() {
            return this.message;
        }

        @Override
        public String getIssueCode() {
            return this.issueCode;
        }
    }

    /**
     * This method asks {@link #delegate} for elements matching provided <code>moduleSpecifier</code>. Returned results
     * are filtered by expected {@link IN4JSProject#getArtifactId()}.
     */
    private Collection<IEObjectDescription> getElementsWithDesiredProjectID(QualifiedName moduleSpecifier,
            String projectArtifactId) {
        final Iterable<IEObjectDescription> moduleSpecifierMatchesWithPossibleDuplicates = delegate
                .getElements(moduleSpecifier);

        // delegate may return multiple entries since it allows duplication (normal 'shadowing' of scopes is not
        // applied). We filter duplicates by uniqueness of target EObject URI.
        final Map<String, IEObjectDescription> result = new HashMap<>();
        for (IEObjectDescription desc : moduleSpecifierMatchesWithPossibleDuplicates) {
            final IN4JSProject containingProject = n4jsCore.findProject(desc.getEObjectURI()).orNull();
            if (projectArtifactId.equals(containingProject.getArtifactId())) {
                result.put(desc.getEObjectURI().toString(), desc);
            }
        }
        return result.values();
    }

    private IN4JSProject findProject(String artifactId, IN4JSProject project) {
        if (Objects.equals(project.getArtifactId(), artifactId)) {
            return project;
        }
        for (IN4JSProject p : project.getDependencies()) {
            if (Objects.equals(p.getArtifactId(), artifactId)) {
                return p;
            }
        }
        return null;
    }

    /** convininence method over {@link #computeImportType(QualifiedName, boolean, IN4JSProject)} */
    private ImportType computeImportType(QualifiedName name, IN4JSProject project) {
        final String firstSegment = name.getFirstSegment();
        final IN4JSProject targetProject = findProject(firstSegment, project);
        final boolean firstSegmentIsProjectId = targetProject != null;
        return computeImportType(name, firstSegmentIsProjectId, targetProject);
    }

    private ImportType computeImportType(QualifiedName name, boolean firstSegmentIsProjectId,
            IN4JSProject targetProject) {
        if (firstSegmentIsProjectId) {
            // PRIORITY 1: 'name' is a complete module specifier, i.e. projectId+'/'+moduleSpecifier
            // -> search all Xtext index entries that match moduleSpecifier and filter by projectId
            final QualifiedName moduleSpecifier;
            if (name.getSegmentCount() == 1) {
                // special case: no module specifier given (only a project ID), i.e. we have a pure project import
                // -> interpret this as an import of the target project's main module
                moduleSpecifier = getMainModuleOfProject(targetProject);
                if (moduleSpecifier == null) {
                    // error: we have a project import to a project that does not define a main module via
                    // the 'MainModule' property in the manifest -> unresolved reference error
                    return ImportType.PROJECT_IMPORT_NO_MAIN;
                } else {
                    return ImportType.PROJECT_IMPORT;
                }
            } else {
                return ImportType.COMPLETE_IMPORT;
            }
        }
        // PRIORITY 2: interpret 'name' as a plain module specifier (i.e. without project ID)
        // -> simplest case, because this is exactly how elements are identified within the Xtext index,
        // so we can simply forward this request to the parent scope
        return ImportType.SIMPLE_IMPORT;
    }

    private QualifiedName getMainModuleOfProject(IN4JSProject project) {
        if (project != null) {
            final String mainModuleSpec = project.getMainModule();
            if (mainModuleSpec != null) {
                final QualifiedName mainModuleQN = QualifiedName.create(mainModuleSpec.split("/"));
                return mainModuleQN;
            }
        }
        return null;
    }
}