| /* |
| * Copyright (C) 2014 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.npw; |
| |
| import com.android.annotations.VisibleForTesting; |
| import com.android.tools.idea.gradle.project.ModuleToImport; |
| import com.android.tools.idea.gradle.util.GradleUtil; |
| import com.google.common.base.Function; |
| import com.google.common.base.Functions; |
| import com.google.common.base.Objects; |
| import com.google.common.base.Predicate; |
| import com.google.common.collect.*; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.ui.MessageType; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.Collection; |
| import java.util.Map; |
| import java.util.Queue; |
| import java.util.Set; |
| |
| /** |
| * Manages list of modules. |
| */ |
| public final class ModuleListModel { |
| @Nullable private final Project myProject; |
| private Map<ModuleToImport, ModuleValidationState> myModules; |
| private Multimap<ModuleToImport, ModuleToImport> myRequiredModules; |
| @Nullable private VirtualFile mySelectedDirectory; |
| private Map<ModuleToImport, String> myNameOverrides = Maps.newHashMap(); |
| private ModuleToImport myPrimaryModule; |
| private Map<ModuleToImport, Boolean> myExplicitSelection = Maps.newHashMap(); |
| |
| public ModuleListModel(@Nullable Project project) { |
| myProject = project; |
| } |
| |
| @Nullable |
| private static ModuleToImport findPrimaryModule(@Nullable VirtualFile directory, @NotNull Iterable<ModuleToImport> modules) { |
| if (directory == null) { |
| return null; |
| } |
| for (ModuleToImport module : modules) { |
| if (Objects.equal(module.location, directory)) { |
| return module; |
| } |
| } |
| return null; |
| } |
| |
| private static boolean isValidModuleName(String moduleName) { |
| if (StringUtil.isEmpty(moduleName)) { |
| return false; |
| } |
| int previousSegmentStart = 0; |
| for (int segmentSeparator = moduleName.indexOf(":", previousSegmentStart); |
| segmentSeparator >= 0; |
| segmentSeparator = moduleName.indexOf(":", previousSegmentStart)) { |
| if (!isValidPathSegment(moduleName, previousSegmentStart, segmentSeparator)) { |
| return false; |
| } |
| previousSegmentStart = segmentSeparator + 1; |
| } |
| return isValidPathSegment(moduleName, previousSegmentStart, moduleName.length()); |
| } |
| |
| private static boolean isValidPathSegment(String string, int segmentStart, int segmentEnd) { |
| if (segmentEnd == segmentStart) { |
| return segmentStart == 0; // Only allowed at string start to allow for absolute paths |
| } |
| String segment = string.substring(segmentStart, segmentEnd); |
| return !StringUtil.isEmpty(segment) && GradleUtil.isValidGradlePath(segment) < 0; |
| } |
| |
| private static String getNameErrorMessage(String moduleName) { |
| if (StringUtil.isEmptyOrSpaces(moduleName)) { |
| return "Module name is empty"; |
| } |
| else { |
| return "Module name is not valid"; |
| } |
| } |
| |
| private Multimap<ModuleToImport, ModuleToImport> computeRequiredModules(Set<ModuleToImport> modules) { |
| Map<String, ModuleToImport> namesToModules = Maps.newHashMapWithExpectedSize(modules.size()); |
| // We only care about modules we are actually going to import. |
| for (ModuleToImport module : modules) { |
| namesToModules.put(module.name, module); |
| } |
| Multimap<ModuleToImport, ModuleToImport> requiredModules = LinkedListMultimap.create(); |
| Queue<ModuleToImport> queue = Lists.newLinkedList(); |
| |
| for (ModuleToImport module : modules) { |
| if (Objects.equal(module, myPrimaryModule) || !isUnselected(module, false)) { |
| queue.add(module); |
| } |
| } |
| while (!queue.isEmpty()) { |
| ModuleToImport moduleToImport = queue.remove(); |
| for (ModuleToImport dep : Iterables.transform(moduleToImport.getDependencies(), Functions.forMap(namesToModules, null))) { |
| if (dep != null) { |
| if (!requiredModules.containsKey(dep)) { |
| queue.add(dep); |
| } |
| requiredModules.put(dep, moduleToImport); |
| } |
| } |
| } |
| return requiredModules; |
| } |
| |
| private boolean isUnselected(ModuleToImport module, boolean isSelected) { |
| if (module.location == null) { |
| return true; |
| } |
| else if (Objects.equal(myPrimaryModule, module)) { |
| return false; |
| } |
| else if (myModules.get(module) == ModuleValidationState.ALREADY_EXISTS) { |
| return !Objects.equal(true, myExplicitSelection.get(module)); |
| } |
| else { |
| return !isSelected && isExplicitlyUnselected(module); |
| } |
| } |
| |
| private ModuleValidationState validateModule(ModuleToImport module) { |
| VirtualFile location = module.location; |
| if (location == null || !location.exists()) { |
| return ModuleValidationState.NOT_FOUND; |
| } |
| String moduleName = getModuleName(module); |
| if (!isValidModuleName(moduleName)) { |
| return ModuleValidationState.INVALID_NAME; |
| } |
| else if (GradleUtil.hasModule(myProject, moduleName, true)) { |
| return ModuleValidationState.ALREADY_EXISTS; |
| } |
| else { |
| return ModuleValidationState.OK; |
| } |
| } |
| |
| public void setContents(@Nullable VirtualFile selectedDirectory, @NotNull Iterable<ModuleToImport> modules) { |
| mySelectedDirectory = selectedDirectory; |
| myPrimaryModule = findPrimaryModule(selectedDirectory, modules); |
| revalidate(modules); |
| } |
| |
| private void checkForDuplicateNames() { |
| Collection<ModuleToImport> modules = getSelectedModules(); |
| ImmutableMultiset<String> names = ImmutableMultiset.copyOf(Iterables.transform(modules, new Function<ModuleToImport, String>() { |
| @Override |
| public String apply(@Nullable ModuleToImport input) { |
| return input == null ? null : getModuleName(input); |
| } |
| })); |
| for (ModuleToImport module : modules) { |
| ModuleValidationState state = myModules.get(module); |
| if (state == ModuleValidationState.OK) { |
| if (names.count(getModuleName(module)) > 1) { |
| myModules.put(module, ModuleValidationState.DUPLICATE_MODULE_NAME); |
| } |
| } |
| } |
| } |
| |
| public Set<ModuleToImport> getSelectedModules() { |
| return ImmutableSet.copyOf(Iterables.filter(myModules.keySet(), new Predicate<ModuleToImport>() { |
| @Override |
| public boolean apply(@Nullable ModuleToImport input) { |
| assert input != null; |
| return isSelected(input); |
| } |
| })); |
| } |
| |
| public boolean hasPrimary() { |
| return myPrimaryModule != null; |
| } |
| |
| public String getModuleName(ModuleToImport module) { |
| if (myNameOverrides.containsKey(module) && !isRequiredModule(module)) { |
| return myNameOverrides.get(module); |
| } |
| return module.name; |
| } |
| |
| @VisibleForTesting |
| public ModuleValidationState getModuleState(ModuleToImport module) { |
| if (module == null) { |
| return ModuleValidationState.NULL; |
| } |
| ModuleValidationState state = myModules.get(module); |
| if (state == ModuleValidationState.OK && isRequiredModule(module)) { |
| return ModuleValidationState.REQUIRED; |
| } |
| else { |
| return state; |
| } |
| } |
| |
| private boolean isRequiredModule(ModuleToImport module) { |
| return myRequiredModules.containsKey(module); |
| } |
| |
| private Map<ModuleToImport, ModuleValidationState> validateModules(Iterable<ModuleToImport> modules) { |
| Map<ModuleToImport, ModuleValidationState> result = Maps.newHashMap(); |
| for (ModuleToImport module : modules) { |
| result.put(module, validateModule(module)); |
| } |
| return result; |
| } |
| |
| public void setSelected(ModuleToImport module, boolean isSelected) { |
| myExplicitSelection.put(module, isSelected); |
| revalidate(myModules.keySet()); |
| } |
| |
| private void revalidate(Iterable<ModuleToImport> modules) { |
| myModules = validateModules(modules); |
| myRequiredModules = computeRequiredModules(myModules.keySet()); |
| for (ModuleToImport module : myRequiredModules.keySet()) { |
| myNameOverrides.remove(module); |
| } |
| checkForDuplicateNames(); |
| } |
| |
| public void setModuleName(ModuleToImport module, @Nullable String newName) { |
| if (!isExplicitlyUnselected(module)) { |
| if (newName == null) { |
| myNameOverrides.remove(module); |
| } |
| else { |
| myNameOverrides.put(module, newName); |
| } |
| revalidate(myModules.keySet()); |
| } |
| } |
| |
| private boolean isExplicitlyUnselected(ModuleToImport module) { |
| return Objects.equal(false, myExplicitSelection.get(module)); |
| } |
| |
| @Nullable |
| public MessageType getStatusSeverity(ModuleToImport module) { |
| ModuleValidationState state = getModuleState(module); |
| switch (state) { |
| case OK: |
| case NULL: |
| return null; |
| case NOT_FOUND: |
| case DUPLICATE_MODULE_NAME: |
| case INVALID_NAME: |
| return MessageType.ERROR; |
| case ALREADY_EXISTS: |
| return getSelectedModules().contains(module) ? MessageType.ERROR : MessageType.WARNING; |
| case REQUIRED: |
| return MessageType.INFO; |
| } |
| throw new IllegalArgumentException(state.name()); |
| } |
| |
| @Nullable |
| public String getStatusDescription(@NotNull ModuleToImport module) { |
| ModuleValidationState state = getModuleState(module); |
| switch (state) { |
| case OK: |
| case NULL: |
| return null; |
| case NOT_FOUND: |
| return "Module sources not found"; |
| case ALREADY_EXISTS: |
| if (isSelected(module) && isRequiredModule(module)) { |
| return "Cannot rename module required by another"; |
| } |
| return "Project already contains module with this name"; |
| case DUPLICATE_MODULE_NAME: |
| return "More then one module with this name is selected"; |
| case REQUIRED: |
| Iterable<String> requiredBy = Iterables.transform(myRequiredModules.get(module), new Function<ModuleToImport, String>() { |
| @Override |
| public String apply(ModuleToImport input) { |
| return "'" + getModuleName(input) + "'"; |
| } |
| }); |
| return ImportUIUtil.formatElementListString(requiredBy, "Required by module %s", "Required by modules %s and %s", |
| "Required by modules %s and %d more"); |
| case INVALID_NAME: |
| return getNameErrorMessage(getModuleName(module)); |
| } |
| throw new IllegalStateException(state.name()); |
| } |
| |
| @Nullable |
| public ModuleToImport getPrimary() { |
| return myPrimaryModule; |
| } |
| |
| @Nullable |
| public VirtualFile getCurrentPath() { |
| return mySelectedDirectory; |
| } |
| |
| public Collection<ModuleToImport> getAllModules() { |
| return ImmutableList.copyOf(myModules.keySet()); |
| } |
| |
| public boolean isSelected(@NotNull ModuleToImport module) { |
| return !isUnselected(module, isRequiredModule(module)); |
| } |
| |
| public boolean canToggleModuleSelection(ModuleToImport module) { |
| ModuleValidationState state = getModuleState(module); |
| return state != ModuleValidationState.NOT_FOUND && !isRequiredModule(module); |
| } |
| |
| public boolean canRename(ModuleToImport module) { |
| return !isRequiredModule(module) && isSelected(module); |
| } |
| |
| @VisibleForTesting |
| public enum ModuleValidationState { |
| OK, NULL, NOT_FOUND, ALREADY_EXISTS, REQUIRED, DUPLICATE_MODULE_NAME, INVALID_NAME |
| } |
| } |