com.android.tools.idea.gradle.editor.parser.GradleEditorModelParserFacade.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.gradle.editor.parser.GradleEditorModelParserFacade.java

Source

/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.idea.gradle.editor.parser;

import com.android.ide.common.repository.GradleCoordinate;
import com.android.tools.idea.gradle.editor.entity.GradleEditorEntity;
import com.android.tools.idea.gradle.editor.entity.GradleEditorEntityGroup;
import com.android.tools.idea.gradle.editor.entity.VersionGradleEditorEntity;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.io.Files;
import com.intellij.lang.ASTNode;
import com.intellij.lang.LanguageParserDefinitions;
import com.intellij.lang.ParserDefinition;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import org.jetbrains.plugins.groovy.GroovyLanguage;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.GroovyRecursiveElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrApplicationStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrAssignmentExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

import static com.android.tools.idea.gradle.editor.parser.GradleEditorModelParseContext.*;

/**
 * Entry point for functionality of {@link #parse(VirtualFile, Project) building} {@link GradleEditorEntity} objects from
 * the target <code>build.gradle</code> file.
 */
public class GradleEditorModelParserFacade {

    private static final List<GradleEditorModelParser> ourParsers = Lists
            .<GradleEditorModelParser>newArrayList(new GradleEditorModelParserV1());

    private static final Logger LOG = Logger.getInstance(GradleEditorModelParserFacade.class);

    @NotNull
    public List<GradleEditorEntityGroup> parse(@NotNull VirtualFile virtualFile, @NotNull Project project) {
        PsiManager psiManager = PsiManager.getInstance(project);
        PsiFile psiFile = psiManager.findFile(virtualFile);
        if (psiFile == null) {
            LOG.warn(String.format("Can't build PSI for the gradle config file '%s'",
                    virtualFile.getCanonicalPath()));
            return Collections.emptyList();
        }
        GradleEditorModelParseContext context = new GradleEditorModelParseContext(virtualFile, project);
        // This a two-steps process:
        //   1. Gradle config's PSI is parsed and the context is filled by assignments data;
        //   2. That data is given for further processing and actual building of model entities;
        fillContext(context, psiFile);
        for (VirtualFile dir = virtualFile.getParent(); dir != null; dir = dir.getParent()) {
            File settingsIoFile = new File(dir.getCanonicalPath(), GradleConstants.SETTINGS_FILE_NAME);
            if (!settingsIoFile.isFile()) {
                // Go up if there is no settings.gradle file in the current dir
                continue;
            }
            if (isParentProject(settingsIoFile, virtualFile)) {
                File parentIoFile = new File(dir.getCanonicalPath(), GradleConstants.DEFAULT_SCRIPT_NAME);
                VirtualFile parentVFile = LocalFileSystem.getInstance().findFileByIoFile(parentIoFile);
                if (parentVFile != null) {
                    parentVFile.refresh(false, false);
                    PsiFile parentPsiFile = psiManager.findFile(parentVFile);
                    if (parentPsiFile != null) {
                        context.onChangeFile(parentVFile);
                        fillContext(context, parentPsiFile);
                    }
                }
            }
            break;
        }
        return buildEntities(context);
    }

    private static boolean isParentProject(@NotNull File settingsFile, @NotNull VirtualFile targetConfigFile) {
        try {
            ImmutableList<String> lines = Files.asCharSource(settingsFile, Charset.forName("UTF-8")).readLines();
            String startLineMarker = "include ";
            for (String line : lines) {
                if (!line.startsWith(startLineMarker)) {
                    continue;
                }
                List<String> subProjects = Lists.newArrayList();
                for (String s : Splitter.on(",").trimResults().omitEmptyStrings()
                        .split(line.substring(startLineMarker.length()))) {
                    // Sub-projects are defined as strings with leading colon, e.g. include ':app'.
                    s = GradleEditorModelUtil.unquote(s);
                    if (s.startsWith(":")) {
                        s = s.substring(1);
                    }
                    subProjects.add(s);
                }
                List<String> dirs = Lists.newArrayList();
                LocalFileSystem fileSystem = LocalFileSystem.getInstance();
                VirtualFile rootDir = fileSystem.refreshAndFindFileByIoFile(settingsFile.getParentFile());
                if (rootDir == null) {
                    return false;
                }
                for (VirtualFile dir = targetConfigFile.getParent(); dir != null; dir = dir.getParent()) {
                    if (rootDir.equals(dir)) {
                        break;
                    }
                    dirs.add(dir.getName());
                }
                Collections.reverse(dirs);
                int i = 0;
                for (String subProject : subProjects) {
                    if (i >= dirs.size() || !subProject.equals(dirs.get(i++))) {
                        return false;
                    }
                }
                return true;
            }
        } catch (IOException e) {
            LOG.warn("Unexpected exception occurred on attempt to read contents of file "
                    + settingsFile.getAbsolutePath());
        }
        return false;
    }

    /**
     * Processes given PSI file and fills given context
     * by {@link GradleEditorModelParseContext#getAssignments(Variable) corresponding assignments}.
     *
     * @param context  context to fill
     * @param psiFile  psi file to parse
     */
    private static void fillContext(@NotNull final GradleEditorModelParseContext context,
            @NotNull PsiFile psiFile) {
        psiFile.acceptChildren(new GroovyPsiElementVisitor(new GroovyRecursiveElementVisitor() {
            @Override
            public void visitMethodCallExpression(GrMethodCallExpression methodCallExpression) {
                Pair<String, TextRange> pair = GradleEditorValueExtractor.extractMethodName(methodCallExpression);
                GrClosableBlock[] closureArguments = methodCallExpression.getClosureArguments();
                if (pair == null || closureArguments.length > 1) {
                    super.visitMethodCallExpression(methodCallExpression);
                    return;
                }
                if (closureArguments.length == 0) {
                    if (methodCallExpression.getArgumentList().getAllArguments().length == 0) {
                        // This is a no-args method, so, we just register it for cases like 'mavenCentral()' or 'jcenter()'.
                        context.addCachedValue(NO_ARGS_METHOD_ASSIGNMENT_VALUE, TextRange.create(
                                pair.second.getEndOffset(), methodCallExpression.getTextRange().getEndOffset()));
                        context.registerAssignmentFromCachedData(pair.first, pair.second, methodCallExpression);
                    }
                    return;
                }

                context.onMethodEnter(pair.getFirst());
                try {
                    super.visitClosure(closureArguments[0]);
                } finally {
                    context.onMethodExit();
                }
            }

            @Override
            public void visitApplicationStatement(GrApplicationStatement applicationStatement) {
                Pair<String, TextRange> methodName = GradleEditorValueExtractor
                        .extractMethodName(applicationStatement);
                if (methodName == null) {
                    return;
                }
                GroovyPsiElement[] allArguments = applicationStatement.getArgumentList().getAllArguments();
                if (allArguments.length == 1) {
                    context.resetCaches();
                    extractValueOrVariable(allArguments[0], context);
                    context.registerAssignmentFromCachedData(methodName.getFirst(), methodName.getSecond(),
                            applicationStatement.getArgumentList());
                }
            }

            @Override
            public void visitAssignmentExpression(GrAssignmentExpression expression) {
                // General idea is to try to extract variable from the given expression and, in case of success, try to extract rvalue and
                // register corresponding assignment with them.
                context.resetCaches();
                extractValueOrVariable(expression.getLValue(), context);
                Multimap<Variable, Location> vars = context.getCachedVariables();
                if (vars.size() != 1) {
                    context.resetCaches();
                    return;
                }
                Map.Entry<Variable, Location> entry = vars.entries().iterator().next();
                Variable lVariable = entry.getKey();
                Location lVariableLocation = entry.getValue();
                context.resetCaches();

                GrExpression rValue = expression.getRValue();
                if (rValue == null) {
                    return;
                }
                extractValueOrVariable(rValue, context);
                if (context.getCachedValues().size() > 1) {
                    Value value = new Value("",
                            new Location(context.getCurrentFile(), GradleEditorModelUtil.interestedRange(rValue)));
                    context.setCachedValues(Collections.singletonList(value));
                }
                context.registerAssignmentFromCachedData(lVariable, lVariableLocation, rValue);
                context.resetCaches();
            }

            @Override
            public void visitVariable(GrVariable variable) {
                TextRange nameRange = null;
                boolean lookForInitializer = false;
                ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE
                        .findSingle(GroovyLanguage.INSTANCE);
                for (PsiElement e = variable.getFirstChild(); e != null; e = e.getNextSibling()) {
                    ASTNode node = e.getNode();
                    if (node == null) {
                        continue;
                    }
                    if (!lookForInitializer) {
                        if (node.getElementType() == GroovyTokenTypes.mIDENT) {
                            nameRange = e.getTextRange();
                        } else if (node.getElementType() == GroovyTokenTypes.mASSIGN) {
                            if (nameRange == null) {
                                return;
                            }
                            lookForInitializer = true;
                        }
                        continue;
                    }
                    if (node.getElementType() == GroovyTokenTypes.mNLS
                            || node.getElementType() == GroovyTokenTypes.mSEMI) {
                        break;
                    }
                    if (parserDefinition.getWhitespaceTokens().contains(node.getElementType())) {
                        continue;
                    }
                    extractValueOrVariable(e, context);
                    if (context.getCachedValues().size() > 1) {
                        Value value = new Value("",
                                new Location(context.getCurrentFile(), GradleEditorModelUtil.interestedRange(e)));
                        context.setCachedValues(Collections.singletonList(value));
                    }
                    if (context.registerAssignmentFromCachedData(variable.getName(), nameRange, e)) {
                        return;
                    }
                }
            }
        }));
    }

    /**
    * @see GradleEditorValueExtractor#extractValueOrVariable(PsiElement)
    */
    private static void extractValueOrVariable(@NotNull PsiElement element,
            @NotNull final GradleEditorModelParseContext context) {
        new GradleEditorValueExtractor(context).extractValueOrVariable(element);
    }

    @NotNull
    private static List<GradleEditorEntityGroup> buildEntities(@NotNull GradleEditorModelParseContext context) {
        VersionGradleEditorEntity entity = GradleEditorModelParserV1.buildGradlePluginVersion(context);
        GradleCoordinate androidGradlePluginVersion = null;
        if (entity != null) {
            String currentVersion = entity.getCurrentValue();
            if (!currentVersion.isEmpty()) {
                androidGradlePluginVersion = GradleCoordinate.parseVersionOnly(currentVersion);
            }
        }
        if (androidGradlePluginVersion == null) {
            androidGradlePluginVersion = GradleCoordinate.parseVersionOnly("0");
        }
        Comparator<GradleCoordinate> c = GradleCoordinate.COMPARE_PLUS_HIGHER;
        for (GradleEditorModelParser parser : ourParsers) {
            if (c.compare(androidGradlePluginVersion, parser.getMinSupportedAndroidGradlePluginVersion()) >= 0 && c
                    .compare(androidGradlePluginVersion, parser.getMaxSupportedAndroidGradlePluginVersion()) < 0) {
                return parser.buildEntities(context);
            }
        }
        return Collections.emptyList();
    }
}