diff options
Diffstat (limited to 'eclipse/plugins')
12 files changed, 1544 insertions, 15 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF index c221240..413c44c 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF +++ b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF @@ -115,6 +115,7 @@ Export-Package: com.android.assetstudiolib;x-friends:="com.android.ide.eclipse.t com.android.ide.eclipse.adt.internal.welcome;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.internal.wizards.actions;x-friends:="com.android.ide.eclipse.tests,com.android.ide.eclipse.adt.package", com.android.ide.eclipse.adt.internal.wizards.export;x-friends:="com.android.ide.eclipse.tests", + com.android.ide.eclipse.adt.internal.wizards.exportgradle;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.internal.wizards.newproject;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.internal.wizards.newxmlfile;x-friends:="com.android.ide.eclipse.tests", com.android.ide.eclipse.adt.internal.wizards.templates;x-friends:="com.android.ide.eclipse.tests", @@ -142,5 +143,7 @@ Export-Package: com.android.assetstudiolib;x-friends:="com.android.ide.eclipse.t org.kxml2.wap.wv;x-friends:="com.android.ide.eclipse.tests", org.xmlpull.v1;x-friends:="com.android.ide.eclipse.tests" Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Import-Package: com.ibm.icu.text, + org.eclipse.core.variables diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/gradle.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/gradle.png Binary files differnew file mode 100644 index 0000000..63ba4c2 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/gradle.png diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml index 5245ae5..5584914 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml +++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml @@ -606,6 +606,22 @@ name="Export Android Application"> </wizard> </extension> + <extension point="org.eclipse.ui.exportWizards"> + <category + id="com.android.ide.eclipse.wizards.category" + name="Android"> + </category> + <wizard + category="com.android.ide.eclipse.wizards.category" + class="com.android.ide.eclipse.adt.internal.wizards.exportgradle.GradleExportWizard" + icon="icons/gradle.png" + id="com.android.ide.eclipse.adt.project.ExportGradleWizard" + name="Generate Gradle build files"> + <selection + class="org.eclipse.jdt.core.IJavaProject"> + </selection> + </wizard> + </extension> <extension point="org.eclipse.ui.commands"> <command name="Debug Android Application" diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java index 9da519e..3b1c29f 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/AndroidNature.java @@ -118,10 +118,12 @@ public class AndroidNature implements IProjectNature { * * @param project An existing or new project to update * @param monitor An optional progress monitor. Can be null. + * @param addAndroidNature true if the Android Nature should be added to the project; false to + * add only the Java nature. * @throws CoreException if fails to change the nature. */ public static synchronized void setupProjectNatures(IProject project, - IProgressMonitor monitor) throws CoreException { + IProgressMonitor monitor, boolean addAndroidNature) throws CoreException { if (project == null || !project.isOpen()) return; if (monitor == null) monitor = new NullProgressMonitor(); @@ -131,7 +133,9 @@ public class AndroidNature implements IProjectNature { // Adding the java nature after the android one, would place the java builder before the // android builders. addNatureToProjectDescription(project, JavaCore.NATURE_ID, monitor); - addNatureToProjectDescription(project, AdtConstants.NATURE_DEFAULT, monitor); + if (addAndroidNature) { + addNatureToProjectDescription(project, AdtConstants.NATURE_DEFAULT, monitor); + } } /** diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java new file mode 100644 index 0000000..918233b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/BuildFileCreator.java @@ -0,0 +1,594 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.wizards.exportgradle; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.widgets.Shell; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.io.IFolderWrapper; +import com.android.io.IAbstractFile; +import com.android.sdklib.io.FileOp; +import com.android.utils.Pair; +import com.android.xml.AndroidManifest; +import com.google.common.base.Joiner; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Creates build.gradle and settings.gradle files for a set of projects. + * <p> + * Based on {@link org.eclipse.ant.internal.ui.datatransfer.BuildFileCreator} + */ +public class BuildFileCreator { + //Finds include ':module_name_1', ':module_name_2',... statements in settings.gradle files + private static final Pattern INCLUDE_PATTERN = + Pattern.compile("include +(':[^']+', *)*':[^']+'"); //$NON-NLS-1$ + private static final String BUILD_FILE = "build.gradle"; //$NON-NLS-1$ + private static final String SETTINGS_FILE = "settings.gradle"; //$NON-NLS-1$ + private static final String ANDROID_SUPPORT_JAR = "android-support-v4.jar"; //$NON-NLS-1$ + private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$ + private static final String GRADLE_WRAPPER_LOCATION = + "tools/templates/gradle/wrapper"; //$NON-NLS-1$ + static final String PLUGIN_CLASSPATH = + "classpath 'com.android.tools.build:gradle:0.4'"; //$NON-NLS-1$ + static final String MAVEN_REPOSITORY = "mavenCentral()"; //$NON-NLS-1$ + + private static final String[] GRADLE_WRAPPER_FILES = new String[] { + "gradlew", //$NON-NLS-1$ + "gradlew.bat", //$NON-NLS-1$ + "gradle/wrapper/gradle-wrapper.jar", //$NON-NLS-1$ + "gradle/wrapper/gradle-wrapper.properties" //$NON-NLS-1$ + }; + + private static final Comparator<IFile> FILE_COMPARATOR = new Comparator<IFile>() { + @Override + public int compare(IFile o1, IFile o2) { + return o1.toString().compareTo(o2.toString()); + } + }; + + private StringBuilder buildfile; + private IJavaProject project; + private String projectName; + private String projectRoot; + private Set<String> settingsFileEntries = new HashSet<String>(); + private boolean needsSettingsFile = false; + private String commonRoot = null; + + /** + * Create buildfile for the given projects. + * + * @param projects create buildfiles for these <code>IJavaProject</code> + * objects + * @param shell parent instance for dialogs + * @return project names for which buildfiles were created + * @throws InterruptedException thrown when user cancels task + */ + public static List<String> createBuildFiles(Set<IJavaProject> projects, Shell shell, + IProgressMonitor pm) + throws JavaModelException, IOException, CoreException, InterruptedException { + File gradleLocation = new File(Sdk.getCurrent().getSdkLocation(), GRADLE_WRAPPER_LOCATION); + List<String> res = new ArrayList<String>(); + SubMonitor localmonitor = null; + try { + // See if we have a Gradle wrapper in the SDK templates directory. If so, we can copy + // it over. + boolean hasGradleWrapper = true; + for (File wrapperFile : getGradleWrapperFiles(gradleLocation)) { + if (!wrapperFile.exists()) { + hasGradleWrapper = false; + } + } + // determine files to create/change + List<IFile> files = new ArrayList<IFile>(); + for (IJavaProject project : projects) { + // build.gradle file + IFile file = project.getProject().getFile(BuildFileCreator.BUILD_FILE); + files.add(file); + } + + // Locate the settings.gradle file and add it to the changed files list + for (IJavaProject project : projects) { + BuildFileCreator instance = new BuildFileCreator(project, shell); + instance.determineCommonRoot(); + IPath path = Path.fromOSString(instance.getGradleSettingsFile().getAbsolutePath()); + IFile file = project.getProject().getWorkspace().getRoot().getFile(path); + if (file != null) { + files.add(file); + } + // Gradle wrapper files + if (hasGradleWrapper) { + // See if there already wrapper files there and only mark nonexistent ones for + // creation. + for (File wrapperFile : getGradleWrapperFiles( + new File(project.getResource().getLocation().toString()))) { + if (!wrapperFile.exists()) { + path = Path.fromOSString(wrapperFile.getAbsolutePath()); + file = project.getProject().getWorkspace().getRoot().getFile(path); + files.add(file); + } + } + } + } + + // Trigger checkout of changed files + Set<IFile> confirmedFiles = validateEdit(shell, files); + + // Now iterate over all the projects and generate the build files. + localmonitor = SubMonitor.convert(pm, ExportMessages.PageTitle, + confirmedFiles.size()); + for (IJavaProject currentProject : projects) { + IFile file = currentProject.getProject().getFile(BuildFileCreator.BUILD_FILE); + if (!confirmedFiles.contains(file)) { + continue; + } + + localmonitor.setTaskName(NLS.bind(ExportMessages.FileStatusMessage, + currentProject.getProject().getName())); + + ProjectState projectState = Sdk.getProjectState(currentProject.getProject()); + BuildFileCreator instance = new BuildFileCreator(currentProject, shell); + instance.determineCommonRoot(); + if (projectState != null) { + // This is an Android project + instance.appendHeader(projectState.isLibrary()); + instance.appendDependencies(); + instance.startAndroidTask(projectState); + instance.appendDefaultConfig(); + instance.createAndroidSourceSets(); + instance.finishAndroidTask(); + if (instance.needsSettingsFile) { + instance.mergeGradleSettingsFile(); + if (hasGradleWrapper) { + copyGradleWrapper(gradleLocation, new File(instance.commonRoot)); + } + } + } else { + // This is a plain Java project + instance.appendJavaHeader(); + instance.createJavaSourceSets(); + } + + // Write the build file + String buildfile = instance.buildfile.toString(); + InputStream is = + new ByteArrayInputStream(buildfile.getBytes("UTF-8")); //$NON-NLS-1$ + if (file.exists()) { + file.setContents(is, true, true, null); + } else { + file.create(is, true, null); + } + if (localmonitor.isCanceled()) { + return res; + } + localmonitor.worked(1); + res.add(instance.projectName); + } + } finally { + if (localmonitor != null && !localmonitor.isCanceled()) { + localmonitor.done(); + } + if (pm != null) { + pm.done(); + } + } + return res; + } + + /** + * @param project create buildfile for this project + * @param shell parent instance for dialogs + */ + private BuildFileCreator(IJavaProject project, Shell shell) { + this.project = project; + this.projectName = project.getProject().getName(); + this.buildfile = new StringBuilder(); + this.projectRoot = project.getResource().getLocation().toString(); + } + + /** + * Return the files that comprise the Gradle wrapper as a collection of {@link File} instances. + * @param root + * @return + */ + private static List<File> getGradleWrapperFiles(File root) { + List<File> files = new ArrayList<File>(GRADLE_WRAPPER_FILES.length); + for (String file : GRADLE_WRAPPER_FILES) { + files.add(new File(root, file)); + } + return files; + } + + /** + * Copy the Gradle wrapper files from one directory to another. + */ + private static void copyGradleWrapper(File from, File to) throws IOException { + for (String file : GRADLE_WRAPPER_FILES) { + File dest = new File(to, file); + if (dest.exists()) { + // Don't clobber an existing file. The user may have modified it already. + continue; + } + File src = new File(from, file); + dest.getParentFile().mkdirs(); + new FileOp().copyFile(src, dest); + dest.setExecutable(src.canExecute()); + } + } + + /** + * Finds the common parent directory shared by this project and all its dependencies. + */ + private void determineCommonRoot() { + String currentProjectRoot = project.getResource().getLocation().toString(); + + if (commonRoot == null) { + commonRoot = new Path(currentProjectRoot).removeLastSegments(1).toString(); + } + + for (IClasspathEntry entry : project.readRawClasspath()) { + if (entry.getEntryKind() != IClasspathEntry.CPE_PROJECT) { + continue; + } + IJavaProject referencedProject = getJavaProjectByName(entry.getPath().toString()); + String referencedProjectRoot = referencedProject.getResource().getLocation().toString(); + commonRoot = findCommonRoot(commonRoot, referencedProjectRoot); + } + } + + /** + * Outputs boilerplate header information common to all Gradle build files. + */ + private void appendHeader(boolean isLibrary) { + buildfile.append("buildscript {\n"); //$NON-NLS-1$ + buildfile.append(" repositories {\n"); //$NON-NLS-1$ + buildfile.append(" " + MAVEN_REPOSITORY + "\n"); //$NON-NLS-1$ + buildfile.append(" }\n"); //$NON-NLS-1$ + buildfile.append(" dependencies {\n"); //$NON-NLS-1$ + buildfile.append(" " + PLUGIN_CLASSPATH + "\n"); //$NON-NLS-1$ + buildfile.append(" }\n"); //$NON-NLS-1$ + buildfile.append("}\n"); //$NON-NLS-1$ + if (isLibrary) { + buildfile.append("apply plugin: 'android-library'\n"); //$NON-NLS-1$ + } else { + buildfile.append("apply plugin: 'android'\n"); //$NON-NLS-1$ + } + buildfile.append("\n"); //$NON-NLS-1$ + } + + /** + * Outputs a block which sets up library and project dependencies. + */ + private void appendDependencies() { + if (project == null) { + AdtPlugin.log(IStatus.WARNING, "project is not loaded in workspace"); //$NON-NLS-1$ + return; + } + buildfile.append("dependencies {\n"); //$NON-NLS-1$ + + // We'll have to do a preliminary pass and pull out any projects that have or are + // dependencies. Then we need to identify the common parent of all of those projects, + // and merge all those projects into a settings.gradle file there. Then we need to build + // relative gradle paths to each library and populate those into the compile + // project(':a:b:library') entries output below. + + String currentProjectRoot = project.getResource().getLocation().toString(); + String path = getRelativeGradleProjectPath(currentProjectRoot, commonRoot); + settingsFileEntries.add("'" + path + "'"); //$NON-NLS-1$ + + for (IClasspathEntry entry : project.readRawClasspath()) { + String entryPath = entry.getPath().makeAbsolute().toString(); + switch(entry.getEntryKind()) { + case IClasspathEntry.CPE_PROJECT: + IJavaProject cpProject = getJavaProjectByName(entry.getPath().toString()); + String cpProjectRoot = cpProject.getResource().getLocation().toString(); + path = getRelativeGradleProjectPath(cpProjectRoot, commonRoot); + settingsFileEntries.add("'" + path + "'"); //$NON-NLS-1$ + needsSettingsFile = true; + buildfile.append(" compile project('" + path + "')\n"); //$NON-NLS-1$ + break; + case IClasspathEntry.CPE_LIBRARY: + if (entry.getPath().lastSegment().equals(ANDROID_SUPPORT_JAR)) { + // This jar gets added automatically by the Android Gradle plugin + continue; + } + path = getRelativePath(entryPath, projectRoot); + buildfile.append(" compile files('" + path + "')\n"); //$NON-NLS-1$ + break; + default: + break; + } + } + buildfile.append("}\n"); //$NON-NLS-1$ + buildfile.append("\n"); //$NON-NLS-1$ + } + + /** + * Given two filesystem paths, finds the parent directory of both of them. + */ + private String findCommonRoot(String path1, String path2) { + IPath f1 = new Path(path1); + IPath f2 = new Path(path2); + IPath result = (IPath) Path.ROOT.clone(); + for (int i = 0; i < Math.min(f1.segmentCount(), f2.segmentCount()); i++) { + if (f1.segment(i).equals(f2.segment(i))) { + result = result.append(Path.SEPARATOR + f1.segment(i)); + } + } + return result.toString(); + } + + /** + * Converts the given path to be relative to the given root path, and converts it to + * Gradle project notation, such as is used in the settings.gradle file. + */ + private String getRelativeGradleProjectPath(String path, String root) { + String relativePath = getRelativePath(path, root); + return ":" + relativePath.replaceAll("\\" + Path.SEPARATOR, ":"); //$NON-NLS-1$ + } + + /** + * Returns a path which is equivalent to the given location relative to the + * specified base path. + */ + private static String getRelativePath(String otherLocation, String basePath) { + + IPath location = new Path(otherLocation); + IPath base = new Path(basePath); + if ((location.getDevice() != null && !location.getDevice() + .equalsIgnoreCase(base.getDevice())) || !location.isAbsolute()) { + return otherLocation; + } + int baseCount = base.segmentCount(); + int count = base.matchingFirstSegments(location); + String temp = ""; //$NON-NLS-1$ + for (int j = 0; j < baseCount - count; j++) { + temp += "../"; //$NON-NLS-1$ + } + String relative = new Path(temp).append( + location.removeFirstSegments(count)).toString(); + if (relative.length() == 0) { + relative = "."; //$NON-NLS-1$ + } + + return relative; + } + + /** + * Finds the workspace project with the given name + */ + private static IJavaProject getJavaProjectByName(String name) { + try { + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + if (project.exists()) { + return JavaCore.create(project); + } + } catch (IllegalArgumentException iae) { + } + return null; + } + + /** + * Outputs the beginning of an Android task in the build file. + */ + private void startAndroidTask(ProjectState projectState) { + int buildApi = projectState.getTarget().getVersion().getApiLevel(); + buildfile.append("android {\n"); //$NON-NLS-1$ + buildfile.append(" compileSdkVersion " + buildApi + "\n"); //$NON-NLS-1$ + buildfile.append(" buildToolsVersion \"" + buildApi + "\"\n"); //$NON-NLS-1$ + buildfile.append("\n"); //$NON-NLS-1$ + } + + /** + * Outputs the defaultConfig block in the Android task. + */ + private void appendDefaultConfig() { + Pair<Integer, Integer> v = ManifestInfo.computeSdkVersions(project.getProject()); + int minApi = v.getFirst(); + int targetApi = v.getSecond(); + + buildfile.append(" defaultConfig {\n"); //$NON-NLS-1$ + buildfile.append(" minSdkVersion " + minApi + "\n"); //$NON-NLS-1$ + buildfile.append(" targetSdkVersion " + targetApi + "\n"); //$NON-NLS-1$ + buildfile.append(" }\n"); //$NON-NLS-1$ + } + + /** + * Outputs a sourceSets block to the Android task that locates all of the various source + * subdirectories in the project. + */ + private void createAndroidSourceSets() { + IFolderWrapper projectFolder = new IFolderWrapper(project.getProject()); + IAbstractFile mManifestFile = AndroidManifest.getManifest(projectFolder); + if (mManifestFile == null) { + return; + } + List<String> srcDirs = new ArrayList<String>(); + for (IClasspathEntry entry : project.readRawClasspath()) { + if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE || + SdkConstants.FD_GEN_SOURCES.equals(entry.getPath().lastSegment())) { + continue; + } + IPath path = entry.getPath().removeFirstSegments(1); + srcDirs.add("'" + path.toOSString() + "'"); //$NON-NLS-1$ + } + + buildfile.append(" sourceSets {\n"); //$NON-NLS-1$ + buildfile.append(" main {\n"); //$NON-NLS-1$ + buildfile.append(" manifest.srcFile '" + + getRelativePath(mManifestFile.getOsLocation(), projectRoot) + + "'\n"); //$NON-NLS-1$ + buildfile.append(" java.srcDirs = [" + + Joiner.on(",").join(srcDirs) + + "]\n"); //$NON-NLS-1$ + buildfile.append(" resources.srcDirs = ['src']\n"); //$NON-NLS-1$ + buildfile.append(" aidl.srcDirs = ['src']\n"); //$NON-NLS-1$ + buildfile.append(" renderscript.srcDirs = ['src']\n"); //$NON-NLS-1$ + buildfile.append(" res.srcDirs = ['res']\n"); //$NON-NLS-1$ + buildfile.append(" assets.srcDirs = ['assets']\n"); //$NON-NLS-1$ + buildfile.append(" }\n"); //$NON-NLS-1$ + buildfile.append("\n"); //$NON-NLS-1$ + buildfile.append(" instrumentTest.setRoot('tests')\n"); //$NON-NLS-1$ + buildfile.append(" }\n"); //$NON-NLS-1$ + } + + /** + * Outputs the completion of the Android task in the build file. + */ + private void finishAndroidTask() { + buildfile.append("}\n"); //$NON-NLS-1$ + } + + /** + * Outputs a boilerplate header for non-Android projects + */ + private void appendJavaHeader() { + buildfile.append("apply plugin: 'java'\n"); //$NON-NLS-1$ + } + + /** + * Outputs a sourceSets block for non-Android projects to locate the source directories. + */ + private void createJavaSourceSets() { + List<String> dirs = new ArrayList<String>(); + for (IClasspathEntry entry : project.readRawClasspath()) { + if (entry.getEntryKind() != IClasspathEntry.CPE_SOURCE) { + continue; + } + IPath path = entry.getPath().removeFirstSegments(1); + dirs.add("'" + path.toOSString() + "'"); //$NON-NLS-1$ + } + buildfile.append("sourceSets {\n"); //$NON-NLS-1$ + buildfile.append(" main.java.srcDirs = ["); //$NON-NLS-1$ + buildfile.append(Joiner.on(",").join(dirs)); //$NON-NLS-1$ + buildfile.append("]\n"); //$NON-NLS-1$ + buildfile.append("}\n"); //$NON-NLS-1$ + } + + /** + * Merges the new subproject dependencies into the settings.gradle file if it already exists, + * and creates one if it does not. + */ + private void mergeGradleSettingsFile() { + File file = getGradleSettingsFile(); + StringBuilder contents = new StringBuilder(); + if (file.exists()) { + contents.append(AdtPlugin.readFile(file)); + + for (String entry : settingsFileEntries) { + if (contents.indexOf(entry) != -1) { + continue; + } + Matcher matcher = INCLUDE_PATTERN.matcher(contents); + if (matcher.find()) { + contents.insert(matcher.end(), ", " + entry); //$NON-NLS-1$ + } else { + contents.insert(0, "include " + entry + "\n"); //$NON-NLS-1$ + } + } + } else { + contents.append("include "); + contents.append(Joiner.on(",").join(settingsFileEntries)); + contents.append("\n"); //$NON-NLS-1$ + } + + AdtPlugin.writeFile(file, contents.toString()); + } + + /** + * Returns the settings.gradle file (which may not exist yet). + */ + private File getGradleSettingsFile() { + return new File(commonRoot, SETTINGS_FILE); + } + + /** + * Request write access to given files. Depending on the version control + * plug-in opens a confirm checkout dialog. + * + * @param shell + * parent instance for dialogs + * @return <code>IFile</code> objects for which user confirmed checkout + * @throws CoreException + * thrown if project is under version control, but not connected + */ + static Set<IFile> validateEdit(Shell shell, List<IFile> files) throws CoreException { + Set<IFile> confirmedFiles = new TreeSet<IFile>(FILE_COMPARATOR); + if (files.size() == 0) { + return confirmedFiles; + } + IStatus status = (files.get(0)).getWorkspace().validateEdit( + files.toArray(new IFile[files.size()]), shell); + if (status.isMultiStatus() && status.getChildren().length > 0) { + for (int i = 0; i < status.getChildren().length; i++) { + IStatus statusChild = status.getChildren()[i]; + if (statusChild.isOK()) { + confirmedFiles.add(files.get(i)); + } + } + } else if (status.isOK()) { + confirmedFiles.addAll(files); + } + if (status.getSeverity() == IStatus.ERROR) { + // not possible to checkout files: not connected to version + // control plugin or hijacked files and made read-only, so + // collect error messages provided by validator and re-throw + StringBuffer message = new StringBuffer(status.getPlugin() + ": " //$NON-NLS-1$ + + status.getMessage() + NEWLINE); + if (status.isMultiStatus()) { + for (int i = 0; i < status.getChildren().length; i++) { + IStatus statusChild = status.getChildren()[i]; + message.append(statusChild.getMessage() + NEWLINE); + } + } + throw new CoreException(new Status(IStatus.ERROR, + AdtPlugin.PLUGIN_ID, 0, message.toString(), null)); + } + + return confirmedFiles; + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java new file mode 100644 index 0000000..c7d6c17 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.wizards.exportgradle; + +import org.eclipse.osgi.util.NLS; + +public class ExportMessages extends NLS { + private static final String BUNDLE_NAME = + "com.android.ide.eclipse.adt.internal.wizards.exportgradle.ExportMessages";//$NON-NLS-1$ + + public static String PageTitle; + public static String PageDescription; + public static String SelectProjects; + public static String ConfirmOverwrite; + public static String ConfirmOverwriteTitle; + public static String CyclicProjectsError; + public static String ExportFailedError; + public static String SelectAll; + public static String DeselectAll; + public static String NoProjectsError; + public static String StatusMessage; + public static String FileStatusMessage; + public static String WindowTitle; + + static { + // load message values from bundle file + NLS.initializeMessages(BUNDLE_NAME, ExportMessages.class); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties new file mode 100644 index 0000000..38d7771 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportMessages.properties @@ -0,0 +1,27 @@ +# Copyright (C) 2013 The Android Open Source Project +# +# Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php +# +# 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. + +PageTitle=Generate Gradle Buildfiles +PageDescription=Generates Gradle buildfiles based on the configuration of the Java projects +SelectProjects=Select the projects to use to &generate the Gradle buildfiles: +ConfirmOverwrite=Are you sure you want to overwrite the buildfiles for these projects? +ConfirmOverwriteTitle=Overwrite Buildfiles? +CyclicProjectsError=A cycle was detected in the build path of project: {0} +ExportFailedError=Buildfile export failed: {0}. See the error log for more details. +SelectAll=&Select All +DeselectAll=&Deselect All +NoProjectsError=Select one or more projects to export. +StatusMessage=Creating Gradle build files... +FileStatusMessage=Generating build file for {0}... +WindowTitle=Export
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportPage.java new file mode 100644 index 0000000..300d528 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportPage.java @@ -0,0 +1,459 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.wizards.exportgradle; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.core.runtime.SubMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaModel; +import org.eclipse.jdt.core.IJavaModelMarker; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.TableLayout; +import org.eclipse.jface.wizard.WizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.eclipse.ui.model.WorkbenchContentProvider; +import org.eclipse.ui.model.WorkbenchLabelProvider; + +import com.android.ide.eclipse.adt.AdtPlugin; +import com.google.common.base.Joiner; +import com.ibm.icu.text.MessageFormat; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * Displays a wizard page that lets the user choose the projects for which to create Gradle build + * files. + * <p> + * Based on {@link org.eclipse.ant.internal.ui.datatransfer.AntBuildfileExportPage} + */ +public class GradleExportPage extends WizardPage { + private static final String NEWLINE = System.getProperty("line.separator"); //$NON-NLS-1$ + + private static final Comparator<IJavaProject> PROJECT_COMPARATOR = + new Comparator<IJavaProject>() { + @Override + public int compare(IJavaProject o1, IJavaProject o2) { + return o1.getProject().getName().compareTo(o2.getProject().getName()); + } + }; + + private CheckboxTableViewer mTableViewer; + private List<IJavaProject> mSelectedJavaProjects = new ArrayList<IJavaProject>(); + + public GradleExportPage() { + super("GradleExportPage"); //$NON-NLS-1$ + setPageComplete(false); + setTitle(ExportMessages.PageTitle); + setDescription(ExportMessages.PageDescription); + } + + @Override + public void createControl(Composite parent) { + initializeDialogUnits(parent); + + Composite workArea = new Composite(parent, SWT.NONE); + setControl(workArea); + + workArea.setLayout(new GridLayout()); + workArea.setLayoutData(new GridData(GridData.FILL_BOTH + | GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL)); + + Label title = new Label(workArea, SWT.NONE); + title.setText(ExportMessages.SelectProjects); + + Composite listComposite = new Composite(workArea, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.numColumns = 2; + layout.marginWidth = 0; + layout.makeColumnsEqualWidth = false; + listComposite.setLayout(layout); + + listComposite.setLayoutData(new GridData(GridData.GRAB_HORIZONTAL + | GridData.GRAB_VERTICAL | GridData.FILL_BOTH)); + + Table table = new Table(listComposite, + SWT.CHECK | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); + mTableViewer = new CheckboxTableViewer(table); + table.setLayout(new TableLayout()); + GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); + data.heightHint = 300; + table.setLayoutData(data); + mTableViewer.setContentProvider(new WorkbenchContentProvider() { + @Override + public Object[] getElements(Object element) { + if (element instanceof IJavaProject[]) { + return (IJavaProject[]) element; + } + return null; + } + }); + mTableViewer.setLabelProvider(new WorkbenchLabelProvider()); + mTableViewer.addCheckStateListener(new ICheckStateListener() { + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + if (event.getChecked()) { + mSelectedJavaProjects.add((IJavaProject) event.getElement()); + } else { + mSelectedJavaProjects.remove(event.getElement()); + } + updateEnablement(); + } + }); + + initializeProjects(); + createSelectionButtons(listComposite); + setControl(workArea); + updateEnablement(); + Dialog.applyDialogFont(parent); + } + + /** + * Creates select all/deselect all buttons. + */ + private void createSelectionButtons(Composite composite) { + Composite buttonsComposite = new Composite(composite, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.marginWidth = 0; + layout.marginHeight = 0; + buttonsComposite.setLayout(layout); + + buttonsComposite.setLayoutData(new GridData( + GridData.VERTICAL_ALIGN_BEGINNING)); + + Button selectAll = new Button(buttonsComposite, SWT.PUSH); + selectAll.setText(ExportMessages.SelectAll); + selectAll.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + for (int i = 0; i < mTableViewer.getTable().getItemCount(); i++) { + mSelectedJavaProjects.add((IJavaProject) mTableViewer.getElementAt(i)); + } + mTableViewer.setAllChecked(true); + updateEnablement(); + } + }); + setButtonLayoutData(selectAll); + + Button deselectAll = new Button(buttonsComposite, SWT.PUSH); + deselectAll.setText(ExportMessages.DeselectAll); + deselectAll.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mSelectedJavaProjects.clear(); + mTableViewer.setAllChecked(false); + updateEnablement(); + } + }); + setButtonLayoutData(deselectAll); + } + + /** + * Populates the list with all the eligible projects in the workspace. + */ + private void initializeProjects() { + IWorkspaceRoot rootWorkspace = ResourcesPlugin.getWorkspace().getRoot(); + IJavaModel javaModel = JavaCore.create(rootWorkspace); + IJavaProject[] javaProjects; + try { + javaProjects = javaModel.getJavaProjects(); + } catch (JavaModelException e) { + javaProjects = new IJavaProject[0]; + } + mTableViewer.setInput(javaProjects); + // Check any necessary projects + if (mSelectedJavaProjects != null) { + mTableViewer.setCheckedElements(mSelectedJavaProjects.toArray( + new IJavaProject[mSelectedJavaProjects.size()])); + } + } + + /** + * Enables/disables the finish button on the wizard and displays error messages as needed. + */ + private void updateEnablement() { + boolean complete = true; + if (mSelectedJavaProjects.size() == 0) { + setErrorMessage(ExportMessages.NoProjectsError); + complete = false; + } + List<String> cyclicProjects; + try { + cyclicProjects = getCyclicProjects(getProjects(false)); + if (cyclicProjects.size() > 0) { + setErrorMessage(MessageFormat.format(ExportMessages.CyclicProjectsError, + Joiner.on(", ").join(cyclicProjects))); //$NON-NLS-1$ + complete = false; + } + } catch (CoreException e) {} + if (complete) { + setErrorMessage(null); + } + setPageComplete(complete); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + if (visible) { + mTableViewer.getTable().setFocus(); + } + } + + /** + * Converts Eclipse Java projects to Gradle build files. Displays error dialogs. + */ + public boolean generateBuildfiles() { + setErrorMessage(null); + final List<String> projectNames = new ArrayList<String>(); + final Set<IJavaProject> projects; + try { + projects = getProjects(true); + if (projects.size() == 0) { + return false; + } + } catch (JavaModelException e) { + AdtPlugin.log(e, null); + setErrorMessage(MessageFormat.format( + ExportMessages.ExportFailedError, e.toString())); + return false; + } + IRunnableWithProgress runnable = new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor pm) throws InterruptedException { + SubMonitor localmonitor = SubMonitor.convert(pm, ExportMessages.StatusMessage, + projects.size()); + Exception problem = null; + try { + projectNames.addAll(BuildFileCreator.createBuildFiles(projects, getShell(), + localmonitor.newChild(projects.size()))); + } catch (JavaModelException e) { + problem = e; + } catch (IOException e) { + problem = e; + } catch (CoreException e) { + problem = e; + } + + if (problem != null) { + AdtPlugin.log(problem, null); + setErrorMessage(MessageFormat.format(ExportMessages.ExportFailedError, + problem.toString())); + } + } + }; + + try { + getContainer().run(false, false, runnable); + } catch (InvocationTargetException e) { + AdtPlugin.log(e, null); + return false; + } catch (InterruptedException e) { + AdtPlugin.log(e, null); + return false; + } + if (getErrorMessage() != null) { + return false; + } + return true; + } + + /** + * Get projects to write buildfiles for. Opens confirmation dialog. + * + * @param displayConfirmation if set to true a dialog prompts for + * confirmation before overwriting files + * @return set of project names + */ + private Set<IJavaProject> getProjects(boolean displayConfirmation) throws JavaModelException { + // collect all projects to create buildfiles for + Set<IJavaProject> projects = new TreeSet<IJavaProject>(PROJECT_COMPARATOR); + for (IJavaProject javaProject : mSelectedJavaProjects) { + projects.addAll(getClasspathProjectsRecursive(javaProject)); + projects.add(javaProject); + } + + // confirm overwrite + List<String> confirmOverwrite = getConfirmOverwriteSet(projects); + if (displayConfirmation && confirmOverwrite.size() > 0) { + String message = ExportMessages.ConfirmOverwrite + NEWLINE + + Joiner.on(NEWLINE).join(confirmOverwrite); + if (!MessageDialog.openQuestion(getShell(), + ExportMessages.ConfirmOverwriteTitle, message)) { + return new TreeSet<IJavaProject>(PROJECT_COMPARATOR); + } + } + return projects; + } + + /** + * Returns given projects that have cyclic dependencies. + * + * @param javaProjects list of IJavaProject objects + * @return set of project names + */ + private List<String> getCyclicProjects(Set<IJavaProject> projects) throws CoreException { + + List<String> cyclicProjects = new ArrayList<String>(); + for (IJavaProject javaProject : projects) { + if (hasCyclicDependency(javaProject)) { + cyclicProjects.add(javaProject.getProject().getName()); + } + } + return cyclicProjects; + } + + /** + * Check if given project has a cyclic dependency. + * <p> + * See {@link org.eclipse.jdt.core.tests.model.ClasspathTests.numberOfCycleMarkers} + */ + public static boolean hasCyclicDependency(IJavaProject javaProject) + throws CoreException { + IMarker[] markers = javaProject.getProject().findMarkers( + IJavaModelMarker.BUILDPATH_PROBLEM_MARKER, false, + IResource.DEPTH_ONE); + for (int i = 0; i < markers.length; i++) { + IMarker marker = markers[i]; + String cycleAttr = (String) marker + .getAttribute(IJavaModelMarker.CYCLE_DETECTED); + if (cycleAttr != null && cycleAttr.equals("true")) { //$NON-NLS-1$ + return true; + } + } + return false; + } + + /** + * Get list of projects which have already a buildfile. + * + * @param javaProjects list of IJavaProject objects + * @return set of project names + */ + private List<String> getConfirmOverwriteSet(Set<IJavaProject> javaProjects) + { + List<String> result = new ArrayList<String>(javaProjects.size()); + for (IJavaProject project : javaProjects) { + String projectRoot = project.getResource().getLocation().toString(); + if (new File(projectRoot, "build.gradle").exists()) { + result.add(project.getProject().getName()); + } + } + return result; + } + + /** + * Get Java project from resource. + */ + private static IJavaProject getJavaProjectByName(String name) { + try { + IProject project = ResourcesPlugin.getWorkspace().getRoot() + .getProject(name); + if (project.exists()) { + return JavaCore.create(project); + } + } catch (IllegalArgumentException iae) { + } + return null; + } + + /** + * Get Java project for given root. + */ + private static IJavaProject getJavaProject(String root) { + IPath path = new Path(root); + if (path.segmentCount() == 1) { + return getJavaProjectByName(root); + } + IResource resource = ResourcesPlugin.getWorkspace().getRoot() + .findMember(path); + if (resource != null && resource.getType() == IResource.PROJECT) { + if (resource.exists()) { + return (IJavaProject) JavaCore.create(resource); + } + } + return null; + } + + /** + * Get for given project all directly and indirectly dependent projects. + * + * @return set of IJavaProject objects + */ + private static List<IJavaProject> getClasspathProjectsRecursive(IJavaProject project) + throws JavaModelException { + LinkedList<IJavaProject> result = new LinkedList<IJavaProject>(); + getClasspathProjectsRecursive(project, result); + return result; + } + + private static void getClasspathProjectsRecursive(IJavaProject project, + LinkedList<IJavaProject> result) throws JavaModelException { + List<IJavaProject> projects = new ArrayList<IJavaProject>(); + IClasspathEntry entries[] = project.getRawClasspath(); + for (IClasspathEntry classpathEntry : entries) { + if (classpathEntry.getContentKind() == IPackageFragmentRoot.K_SOURCE + && classpathEntry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { + // found required project on build path + String subProjectRoot = classpathEntry.getPath().toString(); + IJavaProject subProject = getJavaProject(subProjectRoot); + // is project available in workspace + if (subProject != null) { + projects.add(subProject); + } + } + } + for (IJavaProject javaProject : projects) { + if (!result.contains(javaProject)) { + result.addFirst(javaProject); + getClasspathProjectsRecursive(javaProject, result); // recursion + } + } + } +}
\ No newline at end of file diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java new file mode 100644 index 0000000..5ff4c53 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/GradleExportWizard.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.wizards.exportgradle; + +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.wizard.Wizard; +import org.eclipse.ui.IExportWizard; +import org.eclipse.ui.IWorkbench; + +public class GradleExportWizard extends Wizard implements IExportWizard { + private GradleExportPage mMainPage; + + /** + * Creates buildfile. + */ + @Override + public boolean performFinish() { + return mMainPage.generateBuildfiles(); + } + + @Override + public void addPages() { + mMainPage = new GradleExportPage(); + addPage(mMainPage); + } + + @Override + public void init(IWorkbench workbench, IStructuredSelection selection) { + setWindowTitle(ExportMessages.WindowTitle); + setNeedsProgressMonitor(true); + } +} diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java index 8c8fa29..d168c75 100644 --- a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/newproject/NewProjectCreator.java @@ -24,6 +24,7 @@ import static org.eclipse.core.resources.IResource.DEPTH_ZERO; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; import com.android.ide.common.res2.ValueXmlHelper; import com.android.ide.common.xml.ManifestData; import com.android.ide.common.xml.XmlFormatStyle; @@ -299,7 +300,34 @@ public class NewProjectCreator { WorkspaceModifyOperation op = new WorkspaceModifyOperation() { @Override protected void execute(IProgressMonitor monitor) throws InvocationTargetException { - createProjectAsync(monitor, mainData, testData, null); + createProjectAsync(monitor, mainData, testData, null, true); + } + }; + + // Run the operation in a different thread + runAsyncOperation(op); + return true; + } + + /** + * Creates the a plain Java project without typical android directories or an Android Nature. + * This is intended for use by unit tests and not as a general-purpose Java project creator. + * @return True if the project could be created. + */ + @VisibleForTesting + public boolean createJavaProjects() { + if (mValues.importProjects != null && !mValues.importProjects.isEmpty()) { + return importProjects(); + } + + final ProjectInfo mainData = collectMainPageInfo(); + final ProjectInfo testData = collectTestPageInfo(); + + // Create a monitored operation to create the actual project + WorkspaceModifyOperation op = new WorkspaceModifyOperation() { + @Override + protected void execute(IProgressMonitor monitor) throws InvocationTargetException { + createProjectAsync(monitor, mainData, testData, null, false); } }; @@ -369,7 +397,7 @@ public class NewProjectCreator { WorkspaceModifyOperation op = new WorkspaceModifyOperation() { @Override protected void execute(IProgressMonitor monitor) throws InvocationTargetException { - createProjectAsync(monitor, null, null, projectData); + createProjectAsync(monitor, null, null, projectData, true); } }; @@ -553,6 +581,8 @@ public class NewProjectCreator { * * @param monitor An existing monitor. * @param mainData Data for main project. Can be null. + * @param isAndroidProject true if the project is to be set up as a full Android project; false + * for a plain Java project. * @throws InvocationTargetException to wrap any unmanaged exception and * return it to the calling thread. The method can fail if it fails * to create or modify the project or if it is canceled by the user. @@ -560,7 +590,8 @@ public class NewProjectCreator { private void createProjectAsync(IProgressMonitor monitor, ProjectInfo mainData, ProjectInfo testData, - List<ProjectInfo> importData) + List<ProjectInfo> importData, + boolean isAndroidProject) throws InvocationTargetException { monitor.beginTask("Create Android Project", 100); try { @@ -573,7 +604,8 @@ public class NewProjectCreator { mainData.getDescription(), mainData.getParameters(), mainData.getDictionary(), - null); + null, + isAndroidProject); if (mainProject != null) { final IJavaProject javaProject = JavaCore.create(mainProject); @@ -594,7 +626,8 @@ public class NewProjectCreator { testData.getDescription(), parameters, testData.getDictionary(), - null); + null, + isAndroidProject); if (testProject != null) { final IJavaProject javaProject = JavaCore.create(testProject); Display.getDefault().syncExec(new WorksetAdder(javaProject, @@ -630,7 +663,8 @@ public class NewProjectCreator { data.getDescription(), data.getParameters(), data.getDictionary(), - projectPopulator); + projectPopulator, + isAndroidProject); if (project != null) { final IJavaProject javaProject = JavaCore.create(project); Display.getDefault().syncExec(new WorksetAdder(javaProject, @@ -670,6 +704,8 @@ public class NewProjectCreator { * @param description A description of the project. * @param parameters Template parameters. * @param dictionary String definition. + * @param isAndroidProject true if the project is to be set up as a full Android project; false + * for a plain Java project. * @return The project newly created * @throws StreamException */ @@ -679,12 +715,13 @@ public class NewProjectCreator { @NonNull IProjectDescription description, @NonNull Map<String, Object> parameters, @Nullable Map<String, String> dictionary, - @Nullable ProjectPopulator projectPopulator) + @Nullable ProjectPopulator projectPopulator, + boolean isAndroidProject) throws CoreException, IOException, StreamException { // get the project target IAndroidTarget target = (IAndroidTarget) parameters.get(PARAM_SDK_TARGET); - boolean legacy = target.getVersion().getApiLevel() < 4; + boolean legacy = isAndroidProject && target.getVersion().getApiLevel() < 4; // Create project and open it project.create(description, new SubProgressMonitor(monitor, 10)); @@ -693,14 +730,21 @@ public class NewProjectCreator { project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(monitor, 10)); // Add the Java and android nature to the project - AndroidNature.setupProjectNatures(project, monitor); + AndroidNature.setupProjectNatures(project, monitor, isAndroidProject); // Create folders in the project if they don't already exist addDefaultDirectories(project, AdtConstants.WS_ROOT, DEFAULT_DIRECTORIES, monitor); - String[] sourceFolders = new String[] { + String[] sourceFolders; + if (isAndroidProject) { + sourceFolders = new String[] { (String) parameters.get(PARAM_SRC_FOLDER), GEN_SRC_DIRECTORY }; + } else { + sourceFolders = new String[] { + (String) parameters.get(PARAM_SRC_FOLDER) + }; + } addDefaultDirectories(project, AdtConstants.WS_ROOT, sourceFolders, monitor); // Create the resource folders in the project if they don't already exist. @@ -784,7 +828,9 @@ public class NewProjectCreator { } } - Sdk.getCurrent().initProject(project, target); + if (isAndroidProject) { + Sdk.getCurrent().initProject(project, target); + } // Fix the project to make sure all properties are as expected. // Necessary for existing projects and good for new ones to. @@ -866,7 +912,7 @@ public class NewProjectCreator { public void run(IProgressMonitor submonitor) throws CoreException { try { creator.createEclipseProject(submonitor, project, description, parameters, - dictionary, projectPopulator); + dictionary, projectPopulator, true); } catch (IOException e) { throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "Unexpected error while creating project", e)); diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java index 8dd83d7..9c48ccd 100644 --- a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/editors/layout/refactoring/AdtProjectTest.java @@ -317,7 +317,7 @@ public abstract class AdtProjectTest extends SdkLoadingTestCase { assertNotNull(target); } - private static IProject validateProjectExists(String name) { + protected static IProject validateProjectExists(String name) { IProject iproject = getProject(name); assertTrue(String.format("%s project not created", name), iproject.exists()); assertTrue(String.format("%s project not opened", name), iproject.isOpen()); diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportGradleTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportGradleTest.java new file mode 100644 index 0000000..2cbde5a --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/internal/wizards/exportgradle/ExportGradleTest.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php + * + * 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.ide.eclipse.adt.internal.wizards.exportgradle; + +import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_LIBRARY; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.QualifiedName; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jface.operation.IRunnableContext; +import org.eclipse.jface.operation.IRunnableWithProgress; + +import com.android.SdkConstants; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.AdtProjectTest; +import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; +import com.android.ide.eclipse.adt.internal.sdk.ProjectState; +import com.android.ide.eclipse.adt.internal.sdk.Sdk; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectCreator; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState; +import com.android.ide.eclipse.adt.internal.wizards.newproject.NewProjectWizardState.Mode; +import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; +import com.google.common.base.Charsets; +import com.google.common.collect.Sets; +import com.google.common.io.Files; + +import java.io.File; +import java.lang.reflect.InvocationTargetException; + +public class ExportGradleTest extends AdtProjectTest { + private QualifiedName ERROR_KEY = new QualifiedName(AdtPlugin.PLUGIN_ID, "JobErrorKey"); + private Throwable mLastThrown; + + @Override + public void setUp() throws Exception { + super.setUp(); + mLastThrown = null; + } + + @Override + protected boolean testCaseNeedsUniqueProject() { + return true; + } + + public void testSimpleAndroidApp() throws Throwable { + IProject project = getProject("simple-app"); + final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + Job job = new Job("Validate project") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + BuildFileCreator.createBuildFiles(Sets.newHashSet(javaProject), null, null); + File buildfile = new File(javaProject.getResource().getLocation().toString(), "build.gradle"); + assertTrue(buildfile.exists()); + String contents = Files.toString(buildfile, Charsets.UTF_8); + String expectedContents = + "buildscript {\n" + + " repositories {\n" + + " " + BuildFileCreator.MAVEN_REPOSITORY + "\n" + + " }\n" + + " dependencies {\n" + + " " + BuildFileCreator.PLUGIN_CLASSPATH + "\n" + + " }\n" + + "}\n" + + "apply plugin: 'android'\n" + + "\n" + + "dependencies {\n" + + "}\n" + + "\n" + + "android {\n" + + " compileSdkVersion 16\n" + + " buildToolsVersion \"16\"\n" + + "\n" + + " defaultConfig {\n" + + " minSdkVersion 1\n" + + " targetSdkVersion 1\n" + + " }\n" + + " sourceSets {\n" + + " main {\n" + + " manifest.srcFile 'AndroidManifest.xml'\n" + + " java.srcDirs = ['src']\n" + + " resources.srcDirs = ['src']\n" + + " aidl.srcDirs = ['src']\n" + + " renderscript.srcDirs = ['src']\n" + + " res.srcDirs = ['res']\n" + + " assets.srcDirs = ['assets']\n" + + " }\n" + + " instrumentTest.setRoot('tests')\n" + + " }\n" + + "}"; + + assertEqualsWhitespaceInsensitive(expectedContents, contents); + } catch (Throwable t) { + mLastThrown = t; + } + return null; + } + }; + job.schedule(1000); + job.join(); + Object property = job.getProperty(ERROR_KEY); + assertNull(property); + if (mLastThrown != null) { + throw mLastThrown; + } + } + + public void testSimpleAndroidLib() throws Throwable { + final IProject project = getProject("simple-library"); + ProjectState projectState = Sdk.getProjectState(project.getProject()); + ProjectPropertiesWorkingCopy propertiesWorkingCopy = projectState.getProperties().makeWorkingCopy(); + propertiesWorkingCopy.setProperty(PROPERTY_LIBRARY, "true"); + propertiesWorkingCopy.save(); + IResource projectProp = project.findMember(SdkConstants.FN_PROJECT_PROPERTIES); + if (projectProp != null) { + projectProp.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor()); + } + Job job = new Job("Validate project") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + + BuildFileCreator.createBuildFiles(Sets.newHashSet(javaProject), null, null); + File buildfile = new File(javaProject.getResource().getLocation().toString(), "build.gradle"); + assertTrue(buildfile.exists()); + String contents = Files.toString(buildfile, Charsets.UTF_8); + String expectedContents = + "buildscript {\n" + + " repositories {\n" + + " " + BuildFileCreator.MAVEN_REPOSITORY + "\n" + + " }\n" + + " dependencies {\n" + + " " + BuildFileCreator.PLUGIN_CLASSPATH + "\n" + + " }\n" + + "}\n" + + "apply plugin: 'android-library'\n" + + "\n" + + "dependencies {\n" + + "}\n" + + "\n" + + "android {\n" + + " compileSdkVersion 16\n" + + " buildToolsVersion \"16\"\n" + + "\n" + + " defaultConfig {\n" + + " minSdkVersion 1\n" + + " targetSdkVersion 1\n" + + " }\n" + + " sourceSets {\n" + + " main {\n" + + " manifest.srcFile 'AndroidManifest.xml'\n" + + " java.srcDirs = ['src']\n" + + " resources.srcDirs = ['src']\n" + + " aidl.srcDirs = ['src']\n" + + " renderscript.srcDirs = ['src']\n" + + " res.srcDirs = ['res']\n" + + " assets.srcDirs = ['assets']\n" + + " }\n" + + " instrumentTest.setRoot('tests')\n" + + " }\n" + + "}"; + + assertEqualsWhitespaceInsensitive(expectedContents, contents); + } catch (Throwable t) { + mLastThrown = t; + } + return null; + } + }; + job.schedule(1000); + job.join(); + Object property = job.getProperty(ERROR_KEY); + assertNull(property); + if (mLastThrown != null) { + throw mLastThrown; + } + } + + public void testPlainJavaProject() throws Throwable { + IProject project = getJavaProject("simple-java"); + final IJavaProject javaProject = BaseProjectHelper.getJavaProject(project); + BuildFileCreator.createBuildFiles(Sets.newHashSet(javaProject), null, null); + Job job = new Job("Validate project") { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + File buildfile = new File(javaProject.getResource().getLocation().toString(), "build.gradle"); + assertTrue(buildfile.exists()); + String contents = Files.toString(buildfile, Charsets.UTF_8); + String expectedContents = + "apply plugin: 'java'\n" + + "sourceSets {\n" + + " main.java.srcDirs = ['src']\n" + + "}"; + + assertEqualsWhitespaceInsensitive(expectedContents, contents); + } catch (Throwable t) { + mLastThrown = t; + } + return null; + } + }; + job.schedule(1000); + job.join(); + Object property = job.getProperty(ERROR_KEY); + assertNull(property); + if (mLastThrown != null) { + throw mLastThrown; + } + } + + protected IProject getProject(String projectName) { + IProject project = createProject(projectName); + assertNotNull(project); + if (!testCaseNeedsUniqueProject() && !testNeedsUniqueProject()) { + addCleanupDir(AdtUtils.getAbsolutePath(project).toFile()); + } + addCleanupDir(project.getFullPath().toFile()); + return project; + } + + protected IProject getJavaProject(String projectName) { + IProject project = createJavaProject(projectName); + assertNotNull(project); + if (!testCaseNeedsUniqueProject() && !testNeedsUniqueProject()) { + addCleanupDir(AdtUtils.getAbsolutePath(project).toFile()); + } + addCleanupDir(project.getFullPath().toFile()); + return project; + } + + protected IProject createJavaProject(String name) { + IRunnableContext context = new IRunnableContext() { + @Override + public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) + throws InvocationTargetException, InterruptedException { + runnable.run(new NullProgressMonitor()); + } + }; + NewProjectWizardState state = new NewProjectWizardState(Mode.ANY); + state.projectName = name; + state.packageName = TEST_PROJECT_PACKAGE; + state.activityName = name; + state.applicationName = name; + state.createActivity = false; + state.useDefaultLocation = true; + if (getMinSdk() != -1) { + state.minSdk = Integer.toString(getMinSdk()); + } + + NewProjectCreator creator = new NewProjectCreator(state, context); + creator.createJavaProjects(); + return validateProjectExists(name); + } + + + + /** + * Compares two strings, disregarding whitespace. This makes the test less brittle with respect + * to insignificant changes. + */ + protected void assertEqualsWhitespaceInsensitive(String a, String b) { + a = stripWhitespace(a); + b = stripWhitespace(b); + assertEquals("Expected:\n" + a + "\nbut was:\n" + b + "\n\n", a, b); + } + + protected String stripWhitespace(String s) { + return s.replaceAll("\\s",""); + } +}
\ No newline at end of file |