/*
* 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 me.tatarka.androidunittest.idea;
import com.android.SdkConstants;
import com.android.tools.idea.gradle.customizer.ModuleCustomizer;
import com.android.tools.idea.gradle.messages.Message;
import com.android.tools.idea.gradle.messages.ProjectSyncMessages;
import com.google.common.collect.Lists;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.vfs.StandardFileSystems;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.util.io.URLUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static com.android.SdkConstants.FD_RES;
import static com.android.SdkConstants.FN_ANNOTATIONS_ZIP;
import static java.io.File.separatorChar;
/**
* This is a modified version from Android Studio that doesn't remove the current dependencies.
*
* @param <T>
*/
public abstract class AbstractDependenciesModuleCustomizer<T> implements ModuleCustomizer<T> {
@Override
public void customizeModule(@NotNull Module module, @NotNull Project project, @Nullable T model) {
if (model == null) {
return;
}
List<Message> errorsFound = Lists.newArrayList();
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
ModifiableRootModel rootModel = moduleRootManager.getModifiableModel();
try {
setUpDependencies(rootModel, model, errorsFound);
} finally {
rootModel.commit();
}
notifyUser(errorsFound, module);
}
protected abstract void setUpDependencies(@NotNull ModifiableRootModel rootModel, @NotNull T model, @NotNull List<Message> errorsFound);
private static void notifyUser(@NotNull List<Message> errorsFound, @NotNull Module module) {
if (!errorsFound.isEmpty()) {
ProjectSyncMessages messages = ProjectSyncMessages.getInstance(module.getProject());
for (Message error : errorsFound) {
messages.add(error);
}
}
}
protected void setUpLibraryDependency(@NotNull ModifiableRootModel model,
@NotNull String libraryName,
@NotNull DependencyScope scope,
@NotNull Collection<String> binaryPaths,
@NotNull DependencyOrder order) {
Collection<String> empty = Collections.emptyList();
setUpLibraryDependency(model, libraryName, scope, binaryPaths, empty, empty, order);
}
protected void setUpLibraryDependency(@NotNull ModifiableRootModel model,
@NotNull String libraryName,
@NotNull DependencyScope scope,
@NotNull Collection<String> binaryPaths,
@NotNull Collection<String> sourcePaths,
@NotNull Collection<String> documentationPaths,
@NotNull DependencyOrder order) {
LibraryTable libraryTable = ProjectLibraryTable.getInstance(model.getProject());
Library library = libraryTable.getLibraryByName(libraryName);
if (library == null) {
// Create library.
LibraryTable.ModifiableModel libraryTableModel = libraryTable.getModifiableModel();
try {
library = libraryTableModel.createLibrary(libraryName);
updateLibraryBinaryPaths(library, binaryPaths);
} finally {
libraryTableModel.commit();
}
}
// It is common that the same dependency is used by more than one module. Here we update the "sources" and "documentation" paths if they
// were not set before.
// Example:
// In a multi-project project, there are 2 modules: 'app'' (an Android app) and 'util' (a Java lib.) Both of them depend on Guava. Since
// Android artifacts do not support source attachments, the 'app' module may not indicate where to find the sources for Guava, but the
// 'util' method can, since it is a plain Java module.
// If the 'Guava' library was already defined when setting up 'app', it won't have source attachments. When setting up 'util' we may
// have source attachments, but the library may have been already created. Here we just add the "source" paths if they were not already
// set.
updateLibrarySourcesIfAbsent(library, sourcePaths, OrderRootType.SOURCES);
updateLibrarySourcesIfAbsent(library, documentationPaths, OrderRootType.DOCUMENTATION);
// Add external annotations.
// TODO: Add this to the model instead!
for (String binaryPath : binaryPaths) {
if (binaryPath.endsWith(FD_RES) && binaryPath.length() > FD_RES.length() &&
binaryPath.charAt(binaryPath.length() - FD_RES.length() - 1) == separatorChar) {
File annotations = new File(binaryPath.substring(0, binaryPath.length() - FD_RES.length()),
FN_ANNOTATIONS_ZIP);
if (annotations.isFile()) {
updateLibrarySourcesIfAbsent(library, Collections.singletonList(annotations.getPath()), AnnotationOrderRootType.getInstance());
}
}
}
LibraryOrderEntry orderEntry = model.addLibraryEntry(library);
orderEntry.setScope(scope);
orderEntry.setExported(true);
if (order == DependencyOrder.TOP) {
OrderEntry[] entries = model.getOrderEntries();
OrderEntry newTop = entries[entries.length-1];
System.arraycopy(entries, 0, entries, 1, entries.length-1);
entries[0] = newTop;
model.rearrangeOrderEntries(entries);
}
}
private static void updateLibraryBinaryPaths(@NotNull Library library, @NotNull Collection<String> binaryPaths) {
Library.ModifiableModel libraryModel = library.getModifiableModel();
try {
for (String path : binaryPaths) {
String url = pathToUrl(path);
libraryModel.addRoot(url, OrderRootType.CLASSES);
}
} finally {
libraryModel.commit();
}
}
public static void updateLibrarySourcesIfAbsent(@NotNull Library library,
@NotNull Collection<String> paths,
@NotNull OrderRootType pathType) {
if (paths.isEmpty() || library.getFiles(pathType).length > 0) {
return;
}
// We only update paths if the library does not have any already defined.
Library.ModifiableModel libraryModel = library.getModifiableModel();
try {
for (String path : paths) {
libraryModel.addRoot(pathToUrl(path), pathType);
}
} finally {
libraryModel.commit();
}
}
@NotNull
public static String pathToUrl(@NotNull String path) {
File file = new File(path);
String name = file.getName();
boolean isJarFile = FileUtilRt.extensionEquals(name, SdkConstants.EXT_JAR) ||
FileUtilRt.extensionEquals(name, SdkConstants.EXT_ZIP);
// .jar files require an URL with "jar" protocol.
String protocol = isJarFile ? StandardFileSystems.JAR_PROTOCOL : StandardFileSystems.FILE_PROTOCOL;
String filePath = FileUtil.toSystemIndependentName(file.getPath());
String url = VirtualFileManager.constructUrl(protocol, filePath);
if (isJarFile) {
url += URLUtil.JAR_SEPARATOR;
}
return url;
}
}