| /* |
| * Copyright (C) 2013 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.parser; |
| |
| import com.android.SdkConstants; |
| import com.android.tools.idea.gradle.facet.AndroidGradleFacet; |
| import com.android.tools.idea.gradle.util.GradleUtil; |
| import com.google.common.base.Function; |
| import com.google.common.base.Joiner; |
| import com.google.common.base.Predicates; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.FluentIterable; |
| import com.google.common.collect.Iterables; |
| import com.google.common.collect.Maps; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.module.Module; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.vfs.VfsUtil; |
| import com.intellij.openapi.vfs.VfsUtilCore; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiType; |
| import com.intellij.util.PathUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.arguments.GrArgumentList; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.*; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrLiteral; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * GradleSettingsFile uses PSI to parse settings.gradle files and provides high-level methods to read and mutate the file. |
| * |
| * Note that if you do any mutations on the PSI structure you must be inside a write action. See |
| * {@link com.intellij.util.ActionRunner#runInsideWriteAction}. |
| */ |
| public class GradleSettingsFile extends GradleGroovyFile { |
| private static final Logger LOG = Logger.getInstance(GradleGroovyFile.class.getName()); |
| |
| public static final String INCLUDE_METHOD = "include"; |
| public static final String CUSTOM_LOCATION_FORMAT = "project('%1$s').projectDir = new File('%2$s')"; |
| private static final Iterable<String> EMPTY_ITERABLE = Arrays.asList(new String[] {}); |
| |
| /** |
| * Returns a handle to settings.gradle in project root or creates a new file if one does not already exist. |
| * <p/> |
| * This function should be called from within a write action even if the file exists. Use |
| * {@link #get(com.intellij.openapi.project.Project)} to open a file for reading. |
| */ |
| @NotNull |
| public static GradleSettingsFile getOrCreate(Project project) throws IOException { |
| ApplicationManager.getApplication().assertWriteAccessAllowed(); |
| if (project.isDefault()) { |
| throw new IOException("Not a real project"); |
| } |
| final VirtualFile baseDir = project.getBaseDir(); |
| assert baseDir != null; |
| VirtualFile settingsFile = baseDir.findFileByRelativePath(SdkConstants.FN_SETTINGS_GRADLE); |
| if (settingsFile == null) { |
| settingsFile = baseDir.createChildData(project, SdkConstants.FN_SETTINGS_GRADLE); |
| VfsUtil.saveText(settingsFile, ""); |
| } |
| return new GradleSettingsFile(settingsFile, project); |
| } |
| |
| @Nullable |
| public static GradleSettingsFile get(Project project) { |
| if (project.isDefault()) { |
| return null; |
| } |
| VirtualFile baseDir = project.getBaseDir(); |
| assert baseDir != null; |
| VirtualFile settingsFile = baseDir.findFileByRelativePath(SdkConstants.FN_SETTINGS_GRADLE); |
| if (settingsFile != null) { |
| return new GradleSettingsFile(settingsFile, project); |
| } else { |
| LOG.warn("Unable to find settings.gradle file for project " + project.getName()); |
| return null; |
| } |
| } |
| |
| public GradleSettingsFile(@NotNull VirtualFile file, @NotNull Project project) { |
| super(file, project); |
| } |
| |
| /** |
| * Adds a reference to the module to the settings file, if there is not already one. Must be run inside a write action. |
| */ |
| public void addModule(@NotNull Module module) { |
| checkInitialized(); |
| String moduleGradlePath = getModuleGradlePath(module); |
| if (moduleGradlePath != null) { |
| VirtualFile moduleFile = module.getModuleFile(); |
| assert moduleFile != null; |
| addModule(moduleGradlePath, VfsUtilCore.virtualToIoFile(moduleFile.getParent())); |
| } |
| } |
| |
| /** |
| * Adds a reference to the module to the settings file, if there is not already one. The module path must be colon separated, with a |
| * leading colon, e.g. ":project:subproject". Must be run inside a write action. |
| * |
| * If the file does not match the default module location, this method will override the location. |
| */ |
| public void addModule(@NotNull String modulePath, @NotNull File location) { |
| checkInitialized(); |
| commitDocumentChanges(); |
| for (GrMethodCall includeStatement : getMethodCalls(myGroovyFile, INCLUDE_METHOD)) { |
| for (GrLiteral lit : getLiteralArguments(includeStatement)) { |
| if (modulePath.equals(lit.getValue())) { |
| return; |
| } |
| } |
| } |
| GrMethodCall includeStatement = getMethodCall(myGroovyFile, INCLUDE_METHOD); |
| if (includeStatement != null) { |
| GrArgumentList argList = includeStatement.getArgumentList(); |
| GrLiteral literal = GroovyPsiElementFactory.getInstance(myProject).createLiteralFromValue(modulePath); |
| argList.addAfter(literal, argList.getLastChild()); |
| } else { |
| GrStatement statement = |
| GroovyPsiElementFactory.getInstance(myProject).createStatementFromText(INCLUDE_METHOD + " '" + modulePath + "'"); |
| myGroovyFile.add(statement); |
| } |
| // We get location relative to this file parent |
| VirtualFile parent = getFile().getParent(); |
| File defaultLocation = GradleUtil.getModuleDefaultPath(parent, modulePath); |
| if (!FileUtil.filesEqual(defaultLocation, location)) { |
| final String path; |
| File parentFile = VfsUtilCore.virtualToIoFile(parent); |
| if (FileUtil.isAncestor(parentFile, location, true)) { |
| path = PathUtil.toSystemIndependentName(FileUtil.getRelativePath(parentFile, location)); |
| } |
| else { |
| path = PathUtil.toSystemIndependentName(location.getAbsolutePath()); |
| } |
| String locationAssignment = String.format(CUSTOM_LOCATION_FORMAT, modulePath, path); |
| GrStatement locationStatement = GroovyPsiElementFactory.getInstance(myProject).createStatementFromText(locationAssignment); |
| myGroovyFile.add(locationStatement); |
| } |
| } |
| |
| /** |
| * Removes the reference to the module from the settings file, if present. Must be run inside a write action. |
| */ |
| public void removeModule(@NotNull Module module) { |
| checkInitialized(); |
| String moduleGradlePath = getModuleGradlePath(module); |
| if (moduleGradlePath != null) { |
| removeModule(moduleGradlePath); |
| } |
| } |
| |
| /** |
| * Removes the reference to the module from the settings file, if present. The module path must be colon separated, with a |
| * leading colon, e.g. ":project:subproject". Must be run inside a write action. |
| */ |
| public void removeModule(@NotNull String modulePath) { |
| checkInitialized(); |
| commitDocumentChanges(); |
| boolean removedAnyIncludes = false; |
| for (GrMethodCall includeStatement : getMethodCalls(myGroovyFile, INCLUDE_METHOD)) { |
| for (GrLiteral lit : getLiteralArguments(includeStatement)) { |
| if (modulePath.equals(lit.getValue())) { |
| lit.delete(); |
| removedAnyIncludes = true; |
| if (getArguments(includeStatement).length == 0) { |
| includeStatement.delete(); |
| // If this happens we will fall through both for loops before we get into iteration trouble. We want to keep iterating in |
| // case the module is added more than once (via hand-editing of the file). |
| } |
| } |
| } |
| } |
| if (removedAnyIncludes) { |
| for (Pair<String, GrAssignmentExpression> pair : getAllProjectLocationStatements()) { |
| if (modulePath.equals(pair.first)) { |
| pair.second.delete(); |
| } |
| } |
| } |
| } |
| |
| private Iterable<Pair<String, GrAssignmentExpression>> getAllProjectLocationStatements() { |
| List<PsiElement> allStatements = Arrays.asList(myGroovyFile.getChildren()); |
| Iterable<GrAssignmentExpression> assignments = Iterables.filter(allStatements, GrAssignmentExpression.class); |
| return FluentIterable.from(assignments).transform(new Function<GrAssignmentExpression, Pair<String, GrAssignmentExpression>>() { |
| @Override |
| public Pair<String, GrAssignmentExpression> apply(GrAssignmentExpression assignment) { |
| String projectName = getProjectName(assignment.getLValue()); |
| return projectName == null ? null : Pair.create(projectName, assignment); |
| } |
| }).filter(Predicates.notNull()); |
| } |
| |
| /** |
| * Returns all of the literal-typed arguments of all include statements in the file. |
| */ |
| public Iterable<String> getModules() { |
| checkInitialized(); |
| return Iterables.concat(Iterables.transform(getMethodCalls(myGroovyFile, INCLUDE_METHOD), |
| new Function<GrMethodCall, Iterable<String>>() { |
| @Override |
| public Iterable<String> apply(@Nullable GrMethodCall input) { |
| if (input != null) { |
| return Iterables.transform(getLiteralArgumentValues(input), new Function<Object, String>() { |
| @Override |
| public String apply(@Nullable Object input) { |
| if (input == null) { |
| return null; |
| } |
| String value = input.toString(); |
| // We treat all paths in settings.gradle as being absolute. |
| if (!value.startsWith(SdkConstants.GRADLE_PATH_SEPARATOR)) { |
| value = SdkConstants.GRADLE_PATH_SEPARATOR + value; |
| } |
| return value; |
| } |
| }); |
| } else { |
| return EMPTY_ITERABLE; |
| } |
| } |
| })); |
| } |
| |
| /** |
| * Parses settings.gradle and obtains module locations. Paths are not validated (e.g. may point to non-existing location or to file |
| * instead of directory) and may be relative or absolute. |
| * |
| * @return a {@link java.util.Map} mapping module names to module locations. |
| */ |
| @NotNull |
| public Map<String, File> getModulesWithLocation() { |
| checkInitialized(); |
| Map<String, File> moduleLocations = Maps.newHashMap(); |
| for (String module : getModules()) { |
| Iterable<String> segments = Splitter.on(SdkConstants.GRADLE_PATH_SEPARATOR).omitEmptyStrings().split(module); |
| String defaultLocation = Joiner.on(File.separator).join(segments); |
| moduleLocations.put(module, new File(defaultLocation)); |
| } |
| for (Pair<String, GrAssignmentExpression> pair : getAllProjectLocationStatements()) { |
| if (moduleLocations.containsKey(pair.first)) { |
| GrExpression value = pair.second.getRValue(); |
| File location = getProjectLocation(value); |
| if (location != null) { |
| moduleLocations.put(pair.first, location); |
| } |
| } |
| } |
| return moduleLocations; |
| } |
| |
| /** |
| * Obtains custom module location from the Gradle script. Currently it only recognizes File ctor invocation with a string constant. |
| */ |
| @Nullable |
| private static File getProjectLocation(@Nullable GrExpression rValue) { |
| if (rValue instanceof GrNewExpression) { |
| PsiType type = rValue.getType(); |
| String typeName = type != null ? type.getCanonicalText() : null; |
| if (File.class.getName().equals(typeName) |
| || File.class.getSimpleName().equals(typeName)) { |
| String path = getSingleStringArgumentValue(((GrNewExpression)rValue)); |
| return path == null ? null : new File(path); |
| } |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static String getProjectName(GrExpression lValue) { |
| if (lValue instanceof GrReferenceExpression) { |
| GrReferenceExpression reference = (GrReferenceExpression)lValue; |
| if ("projectDir".equals(reference.getCanonicalText())) { |
| GrExpression qualifier = reference.getQualifier(); |
| if (qualifier instanceof GrMethodCall) { |
| GrMethodCall methodCall = (GrMethodCall)qualifier; |
| if ("project".equals(getMethodCallName(methodCall))) { |
| return getSingleStringArgumentValue(methodCall); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Given a module, returns that module's path in Gradle colon-delimited format. |
| */ |
| @Nullable |
| public static String getModuleGradlePath(@NotNull Module module) { |
| AndroidGradleFacet androidGradleFacet = AndroidGradleFacet.getInstance(module); |
| if (androidGradleFacet == null) { |
| return null; |
| } |
| return androidGradleFacet.getConfiguration().GRADLE_PROJECT_PATH; |
| } |
| |
| /** |
| * Given a module path in Gradle colon-delimited format, returns the build.gradle file for that module |
| * if it can be found, or null if it cannot. |
| */ |
| @Nullable |
| public GradleBuildFile getModuleBuildFile(@NotNull String moduleGradlePath) { |
| Module module = GradleUtil.findModuleByGradlePath(myProject, moduleGradlePath); |
| if (module != null) { |
| VirtualFile buildFile = GradleUtil.getGradleBuildFile(module); |
| if (buildFile != null) { |
| return new GradleBuildFile(buildFile, myProject); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns true if there exists a build.gradle file for a module identified by the given module path. |
| */ |
| public boolean hasBuildFile(@NotNull String moduleGradlePath) { |
| Module module = GradleUtil.findModuleByGradlePath(myProject, moduleGradlePath); |
| if (module == null) { |
| return false; |
| } |
| VirtualFile gradleBuildFile = GradleUtil.getGradleBuildFile(module); |
| return gradleBuildFile != null && gradleBuildFile.exists(); |
| } |
| } |