/* * Copyright (C) 2007 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.sdklib.internal.project; import com.android.SdkConstants; import com.android.io.FileWrapper; import com.android.io.FolderWrapper; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.SdkManager; import com.android.sdklib.internal.project.ProjectProperties.PropertyType; import com.android.utils.ILogger; import com.android.xml.AndroidManifest; import com.android.xml.AndroidXPathFactory; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; /** * Creates the basic files needed to get an Android project up and running. */ public class ProjectCreator { /** Version of the build.xml. Stored in version-tag */ private static final int MIN_BUILD_VERSION_TAG = 1; /** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */ private static final String PH_PACKAGE_PATH = "PACKAGE_PATH"; /** Package name substitution string used in template files, i.e. "PACKAGE" */ private static final String PH_PACKAGE = "PACKAGE"; /** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME". * @deprecated This is only used for older templates. For new ones see * {@link #PH_ACTIVITY_ENTRY_NAME}, and {@link #PH_ACTIVITY_CLASS_NAME}. */ @Deprecated private static final String PH_ACTIVITY_NAME = "ACTIVITY_NAME"; /** Activity name substitution string used in manifest templates, i.e. "ACTIVITY_ENTRY_NAME".*/ private static final String PH_ACTIVITY_ENTRY_NAME = "ACTIVITY_ENTRY_NAME"; /** Activity name substitution string used in class templates, i.e. "ACTIVITY_CLASS_NAME".*/ private static final String PH_ACTIVITY_CLASS_NAME = "ACTIVITY_CLASS_NAME"; /** Activity FQ-name substitution string used in class templates, i.e. "ACTIVITY_FQ_NAME".*/ private static final String PH_ACTIVITY_FQ_NAME = "ACTIVITY_FQ_NAME"; /** Original Activity class name substitution string used in class templates, i.e. * "ACTIVITY_TESTED_CLASS_NAME".*/ private static final String PH_ACTIVITY_TESTED_CLASS_NAME = "ACTIVITY_TESTED_CLASS_NAME"; /** Project name substitution string used in template files, i.e. "PROJECT_NAME". */ public static final String PH_PROJECT_NAME = "PROJECT_NAME"; /** Application icon substitution string used in the manifest template */ private static final String PH_ICON = "ICON"; /** Version tag name substitution string used in template files, i.e. "VERSION_TAG". */ private static final String PH_VERSION_TAG = "VERSION_TAG"; /** Target name substitution string used in template files, i.e. "TARGET". */ private static final String PH_TARGET = "TARGET"; /** Gradle plugin substitution string used in the build.gradle template */ private static final String PH_PLUGIN = "PLUGIN"; /** Gradle artifact version substitution string used in the build.gradle template */ private static final String PH_ARTIFACT_VERSION = "ARTIFACT_VERSION"; /** Build tool revision substitution string used in the build.gradle template */ private static final String PH_BUILD_TOOL_REV = "BUILD_TOOL_REV"; /** The xpath to find a project name in an Ant build file. */ private static final String XPATH_PROJECT_NAME = "/project/@name"; /** Pattern for characters accepted in a project name. Since this will be used as a * directory name, we're being a bit conservative on purpose: dot and space cannot be used. */ public static final Pattern RE_PROJECT_NAME = Pattern.compile("[a-zA-Z0-9_]+"); /** List of valid characters for a project name. Used for display purposes. */ public static final String CHARS_PROJECT_NAME = "a-z A-Z 0-9 _"; /** Pattern for characters accepted in a package name. A package is list of Java identifier * separated by a dot. We need to have at least one dot (e.g. a two-level package name). * A Java identifier cannot start by a digit. */ public static final Pattern RE_PACKAGE_NAME = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)+"); /** List of valid characters for a project name. Used for display purposes. */ public static final String CHARS_PACKAGE_NAME = "a-z A-Z 0-9 _"; /** Pattern for characters accepted in an activity name, which is a Java identifier. */ public static final Pattern RE_ACTIVITY_NAME = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); /** List of valid characters for a project name. Used for display purposes. */ public static final String CHARS_ACTIVITY_NAME = "a-z A-Z 0-9 _"; /** Gradle plugin to use with standard projects */ private static final String PLUGIN_PROJECT = "android"; /** Gradle plugin to use with library projects */ private static final String PLUGIN_LIB_PROJECT = "android-library"; public enum OutputLevel { /** Silent mode. Project creation will only display errors. */ SILENT, /** Normal mode. Project creation will display what's being done, display * error but not warnings. */ NORMAL, /** Verbose mode. Project creation will display what's being done, errors and warnings. */ VERBOSE } /** * Exception thrown when a project creation fails, typically because a template * file cannot be written. */ private static class ProjectCreateException extends Exception { /** default UID. This will not be serialized anyway. */ private static final long serialVersionUID = 1L; @SuppressWarnings("unused") ProjectCreateException(String message) { super(message); } ProjectCreateException(Throwable t, String format, Object... args) { super(format != null ? String.format(format, args) : format, t); } ProjectCreateException(String format, Object... args) { super(String.format(format, args)); } } /** The {@link OutputLevel} verbosity. */ private final OutputLevel mLevel; /** Logger for errors and output. Cannot be null. */ private final ILogger mLog; /** The OS path of the SDK folder. */ private final String mSdkFolder; /** The {@link SdkManager} instance. */ private final SdkManager mSdkManager; /** * Helper class to create android projects. * * @param sdkManager The {@link SdkManager} instance. * @param sdkFolder The OS path of the SDK folder. * @param level The {@link OutputLevel} verbosity. * @param log Logger for errors and output. Cannot be null. */ public ProjectCreator(SdkManager sdkManager, String sdkFolder, OutputLevel level, ILogger log) { mSdkManager = sdkManager; mSdkFolder = sdkFolder; mLevel = level; mLog = log; } /** * Creates a new (ant) project. * <p/> * The caller should have already checked and sanitized the parameters. * * @param folderPath the folder of the project to create. * @param projectName the name of the project. The name must match the * {@link #RE_PROJECT_NAME} regex. * @param packageName the package of the project. The name must match the * {@link #RE_PACKAGE_NAME} regex. * @param activityEntry the activity of the project as it will appear in the manifest. Can be * null if no activity should be created. The name must match the * {@link #RE_ACTIVITY_NAME} regex. * @param target the project target. * @param library whether the project is a library. * @param pathToMainProject if non-null the project will be setup to test a main project * located at the given path. */ public void createProject(String folderPath, String projectName, String packageName, String activityEntry, IAndroidTarget target, boolean library, String pathToMainProject) { // create project folder if it does not exist File projectFolder = checkNewProjectLocation(folderPath); if (projectFolder == null) { return; } try { boolean isTestProject = pathToMainProject != null; // first create the project properties. // location of the SDK goes in localProperty ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath, PropertyType.LOCAL); localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); localProperties.save(); // target goes in project properties ProjectPropertiesWorkingCopy projectProperties = ProjectProperties.create(folderPath, PropertyType.PROJECT); projectProperties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); if (library) { projectProperties.setProperty(ProjectProperties.PROPERTY_LIBRARY, "true"); } projectProperties.save(); // create a ant.properties file with just the application package ProjectPropertiesWorkingCopy antProperties = ProjectProperties.create(folderPath, PropertyType.ANT); if (isTestProject) { antProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject); } antProperties.save(); // create the map for place-holders of values to replace in the templates final HashMap<String, String> keywords = new HashMap<String, String>(); // create the required folders. // compute src folder path final String packagePath = stripString(packageName.replace(".", File.separator), File.separatorChar); // put this path in the place-holder map for project files that needs to list // files manually. keywords.put(PH_PACKAGE_PATH, packagePath); keywords.put(PH_PACKAGE, packageName); keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); // compute some activity related information String fqActivityName = null, activityPath = null, activityClassName = null; String originalActivityEntry = activityEntry; String originalActivityClassName = null; if (activityEntry != null) { if (isTestProject) { // append Test so that it doesn't collide with the main project activity. activityEntry += "Test"; // get the classname from the original activity entry. int pos = originalActivityEntry.lastIndexOf('.'); if (pos != -1) { originalActivityClassName = originalActivityEntry.substring(pos + 1); } else { originalActivityClassName = originalActivityEntry; } } // get the fully qualified name of the activity fqActivityName = AndroidManifest.combinePackageAndClassName(packageName, activityEntry); // get the activity path (replace the . to /) activityPath = stripString(fqActivityName.replace(".", File.separator), File.separatorChar); // remove the last segment, so that we only have the path to the activity, but // not the activity filename itself. activityPath = activityPath.substring(0, activityPath.lastIndexOf(File.separatorChar)); // finally, get the class name for the activity activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1); } // at this point we have the following for the activity: // activityEntry: this is the manifest entry. For instance .MyActivity // fqActivityName: full-qualified class name: com.foo.MyActivity // activityClassName: only the classname: MyActivity // originalActivityClassName: the classname of the activity being tested (if applicable) // Add whatever activity info is needed in the place-holder map. // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests). if (target.getVersion().getApiLevel() < 4) { // legacy if (originalActivityEntry != null) { keywords.put(PH_ACTIVITY_NAME, originalActivityEntry); } } else { // newer templates make a difference between the manifest entries, classnames, // as well as the main and test classes. if (activityEntry != null) { keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry); keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName); keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName); if (originalActivityClassName != null) { keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName); } } } // Take the project name from the command line if there's one if (projectName != null) { keywords.put(PH_PROJECT_NAME, projectName); } else { if (activityClassName != null) { // Use the activity class name as project name keywords.put(PH_PROJECT_NAME, activityClassName); } else { // We need a project name. Just pick up the basename of the project // directory. projectName = projectFolder.getName(); keywords.put(PH_PROJECT_NAME, projectName); } } // create the source folder for the activity if (activityClassName != null) { String srcActivityFolderPath = SdkConstants.FD_SOURCES + File.separator + activityPath; File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); String javaTemplate = isTestProject ? "java_tests_file.template" : "java_file.template"; String activityFileName = activityClassName + ".java"; installTargetTemplate(javaTemplate, new File(sourceFolder, activityFileName), keywords, target); } else { // we should at least create 'src' createDirs(projectFolder, SdkConstants.FD_SOURCES); } // create other useful folders File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES); createDirs(projectFolder, SdkConstants.FD_OUTPUT); createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS); if (isTestProject == false) { /* Make res files only for non test projects */ File valueFolder = createDirs(resourceFolder, SdkConstants.FD_RES_VALUES); installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"), keywords, target); File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_RES_LAYOUT); installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"), keywords, target); // create the icons if (installIcons(resourceFolder, target)) { keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\""); } else { keywords.put(PH_ICON, ""); } } /* Make AndroidManifest.xml and build.xml files */ String manifestTemplate = "AndroidManifest.template"; if (isTestProject) { manifestTemplate = "AndroidManifest.tests.template"; } installTargetTemplate(manifestTemplate, new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML), keywords, target); installTemplate("build.template", new File(projectFolder, SdkConstants.FN_BUILD_XML), keywords); // install the proguard config file. installTemplate(SdkConstants.FN_PROJECT_PROGUARD_FILE, new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), null /*keywords*/); } catch (Exception e) { mLog.error(e, null); } } /** * Creates a new (gradle) project. * <p/> * The caller should have already checked and sanitized the parameters. * * @param folderPath the folder of the project to create. * @param projectName the name of the project. The name must match the * {@link #RE_PROJECT_NAME} regex. * @param packageName the package of the project. The name must match the * {@link #RE_PACKAGE_NAME} regex. * @param activityEntry the activity of the project as it will appear in the manifest. Can be * null if no activity should be created. The name must match the * {@link #RE_ACTIVITY_NAME} regex. * @param target the project target. * @param library whether the project is a library. * @param artifactVersion the version of the gradle artifact in maven. */ public void createGradleProject(String folderPath, String projectName, String packageName, String activityEntry, IAndroidTarget target, boolean library, String artifactVersion) { // create project folder if it does not exist File projectFolder = checkNewProjectLocation(folderPath); if (projectFolder == null) { return; } try { // first create the project properties. // location of the SDK goes in localProperty ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath, PropertyType.LOCAL); localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); localProperties.save(); // create the map for place-holders of values to replace in the templates final HashMap<String, String> keywords = new HashMap<String, String>(); final HashMap<String, String> testKeywords = new HashMap<String, String>(); // create the required folders. // compute src folder path final String packagePath = stripString(packageName.replace(".", File.separator), File.separatorChar); // put this path in the place-holder map for project files that needs to list // files manually. keywords.put(PH_PACKAGE_PATH, packagePath); keywords.put(PH_PACKAGE, packageName); testKeywords.put(PH_PACKAGE_PATH, packagePath); testKeywords.put(PH_PACKAGE, packageName); // compute some activity related information String activityPath = null, activityClassName = null; String testActivityPath = null, testActivityClassName = null; if (activityEntry != null) { // get the fully qualified name of the activity String fqActivityName = AndroidManifest.combinePackageAndClassName(packageName, activityEntry); // get the activity path (replace the . to /) activityPath = stripString(fqActivityName.replace(".", File.separator), File.separatorChar); // remove the last segment, so that we only have the path to the activity, but // not the activity filename itself. activityPath = activityPath.substring(0, activityPath.lastIndexOf(File.separatorChar)); // finally, get the class name for the activity activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1); // at this point we have the following for the activity: // activityEntry: this is the manifest entry. For instance .MyActivity // fqActivityName: full-qualified class name: com.foo.MyActivity // activityClassName: only the classname: MyActivity // append Test so that it doesn't collide with the main project activity. String testActivityEntry = activityEntry + "Test"; // get the fully qualified name of the test String testFqActivityName = AndroidManifest.combinePackageAndClassName(packageName, testActivityEntry); // get the test path (replace the . to /) testActivityPath = stripString(testFqActivityName.replace(".", File.separator), File.separatorChar); // remove the last segment, so that we only have the path to the test, but // not the test filename itself. testActivityPath = testActivityPath.substring(0, testActivityPath.lastIndexOf(File.separatorChar)); // finally, get the class name for the test testActivityClassName = testFqActivityName.substring(testFqActivityName.lastIndexOf('.') + 1); // at this point we have the following for the test: // testActivityEntry: this is the manifest entry. For instance .MyActivityTest // testFqActivityName: full-qualified class name: com.foo.MyActivityTest // testActivityClassName: only the classname: MyActivityTest // Add whatever activity info is needed in the place-holder map. // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests). if (target.getVersion().getApiLevel() < 4) { // legacy keywords.put(PH_ACTIVITY_NAME, activityEntry); testKeywords.put(PH_ACTIVITY_NAME, activityEntry); } else { // newer templates make a difference between the manifest entries, classnames, // as well as the main and test classes. keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry); keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName); keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName); testKeywords.put(PH_ACTIVITY_ENTRY_NAME, testActivityEntry); testKeywords.put(PH_ACTIVITY_CLASS_NAME, testActivityClassName); testKeywords.put(PH_ACTIVITY_FQ_NAME, testFqActivityName); testKeywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, activityClassName); } } // Take the project name from the command line if there's one if (projectName != null) { keywords.put(PH_PROJECT_NAME, projectName); testKeywords.put(PH_PROJECT_NAME, projectName); } else { // Use the activity class name as project name, else just // pick up the basename of the project directory. keywords.put(PH_PROJECT_NAME, (activityClassName != null) ? activityClassName : projectFolder.getName()); testKeywords.put(PH_PROJECT_NAME, (testActivityClassName != null) ? testActivityClassName : projectFolder.getName()); } String srcMainPath = SdkConstants.FD_SOURCES + File.separator + SdkConstants.FD_MAIN; String srcTestPath = SdkConstants.FD_SOURCES + File.separator + SdkConstants.FD_TEST; // create the source folders for the activity String srcMainCodePath = srcMainPath + File.separator + SdkConstants.FD_JAVA; createDirs(projectFolder, srcMainCodePath); if (activityClassName != null) { String srcActivityFolderPath = srcMainCodePath + File.separator + activityPath; File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); String activityFileName = activityClassName + ".java"; installTargetTemplate("java_file.template", new File(sourceFolder, activityFileName), keywords, target); } // create the source folders for the test String srcTestCodePath = srcTestPath + File.separator + SdkConstants.FD_JAVA; createDirs(projectFolder, srcTestCodePath); if (testActivityClassName != null) { String srcActivityFolderPath = srcTestCodePath + File.separator + testActivityPath; File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); String activityFileName = testActivityClassName + ".java"; installTargetTemplate("java_tests_file.template", new File(sourceFolder, activityFileName), testKeywords, target); } // create the res xml files String srcMainResPath = srcMainPath + File.separator + SdkConstants.FD_RES; File resourceFolder = createDirs(projectFolder, srcMainResPath); File valueFolder = createDirs(resourceFolder, SdkConstants.FD_RES_VALUES); installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"), keywords, target); File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_RES_LAYOUT); installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"), keywords, target); // create the icons if (installIcons(resourceFolder, target)) { keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\""); } else { keywords.put(PH_ICON, ""); } // Create the AndroidManifest.xml and build.gradle files installTargetTemplate("AndroidManifest.template", new File(projectFolder, srcMainPath + File.separator + SdkConstants.FN_ANDROID_MANIFEST_XML), keywords, target); String buildToolRev = mSdkManager.getLatestBuildTool().getRevision().toString(); keywords.put(PH_BUILD_TOOL_REV, buildToolRev); keywords.put(PH_ARTIFACT_VERSION, artifactVersion); keywords.put(PH_TARGET, target.hashString()); keywords.put(PH_PLUGIN, (library) ? PLUGIN_LIB_PROJECT : PLUGIN_PROJECT); installTemplate("build_gradle.template", new File(projectFolder, SdkConstants.FN_BUILD_GRADLE), keywords); // Create the gradle wrapper files createDirs(projectFolder, SdkConstants.FD_GRADLE_WRAPPER); installGradleWrapperFile(SdkConstants.FD_GRADLE_WRAPPER + File.separator + SdkConstants.FN_GRADLE_WRAPPER_JAR, projectFolder); installGradleWrapperFile(SdkConstants.FD_GRADLE_WRAPPER + File.separator + SdkConstants.FN_GRADLE_WRAPPER_PROPERTIES, projectFolder); installGradleWrapperFile(SdkConstants.FN_GRADLE_WRAPPER_WIN, projectFolder); installGradleWrapperFile(SdkConstants.FN_GRADLE_WRAPPER_UNIX, projectFolder); new File(projectFolder, SdkConstants.FN_GRADLE_WRAPPER_UNIX).setExecutable(true, false); } catch (Exception e) { mLog.error(e, null); } } private File checkNewProjectLocation(String folderPath) { File projectFolder = new File(folderPath); if (!projectFolder.exists()) { boolean created = false; Throwable t = null; try { created = projectFolder.mkdirs(); } catch (Exception e) { t = e; } if (created) { println("Created project directory: %1$s", projectFolder); } else { mLog.error(t, "Could not create directory: %1$s", projectFolder); return null; } } else { Exception e = null; String error = null; try { String[] content = projectFolder.list(); if (content == null) { error = "Project folder '%1$s' is not a directory."; } else if (content.length != 0) { error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead."; } } catch (Exception e1) { e = e1; } if (e != null || error != null) { mLog.error(e, error, projectFolder, SdkConstants.androidCmdName()); } } return projectFolder; } /** * Updates an existing project. * <p/> * Workflow: * <ul> * <li> Check AndroidManifest.xml is present (required) * <li> Check if there's a legacy properties file and convert it * <li> Check there's a project.properties with a target *or* --target was specified * <li> Update default.prop if --target was specified * <li> Refresh/create "sdk" in local.properties * <li> Build.xml: create if not present or if version-tag is found or not. version-tag:custom * prevent any overwrite. version-tag:[integer] will override. missing version-tag will query * the dev. * </ul> * * @param folderPath the folder of the project to update. This folder must exist. * @param target the project target. Can be null. * @param projectName The project name from --name. Can be null. * @param libraryPath the path to a library to add to the references. Can be null. * @return true if the project was successfully updated. */ @SuppressWarnings("deprecation") public boolean updateProject(String folderPath, IAndroidTarget target, String projectName, String libraryPath) { // since this is an update, check the folder does point to a project FileWrapper androidManifest = checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML); if (androidManifest == null) { return false; } // get the parent folder. FolderWrapper projectFolder = (FolderWrapper) androidManifest.getParentFolder(); boolean hasProguard = false; // Check there's a project.properties with a target *or* --target was specified IAndroidTarget originalTarget = null; boolean writeProjectProp = false; ProjectProperties props = ProjectProperties.load(projectFolder, PropertyType.PROJECT); if (props == null) { // no project.properties, try to load default.properties props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_DEFAULT); writeProjectProp = true; } if (props != null) { String targetHash = props.getProperty(ProjectProperties.PROPERTY_TARGET); originalTarget = mSdkManager.getTargetFromHashString(targetHash); // if the project is already setup with proguard, we won't copy the proguard config. hasProguard = props.getProperty(ProjectProperties.PROPERTY_PROGUARD_CONFIG) != null; } if (originalTarget == null && target == null) { mLog.error(null, "The project either has no target set or the target is invalid.\n" + "Please provide a --target to the '%1$s update' command.", SdkConstants.androidCmdName()); return false; } boolean saveProjectProps = false; ProjectPropertiesWorkingCopy propsWC = null; // Update default.prop if --target was specified if (target != null || writeProjectProp) { // we already attempted to load the file earlier, if that failed, create it. if (props == null) { propsWC = ProjectProperties.create(projectFolder, PropertyType.PROJECT); } else { propsWC = props.makeWorkingCopy(PropertyType.PROJECT); } // set or replace the target if (target != null) { propsWC.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); } saveProjectProps = true; } if (libraryPath != null) { // At this point, the default properties already exists, either because they were // already there or because they were created with a new target if (propsWC == null) { assert props != null; propsWC = props.makeWorkingCopy(); } // check the reference is valid File libProject = new File(libraryPath); String resolvedPath; if (libProject.isAbsolute() == false) { libProject = new File(projectFolder, libraryPath); try { resolvedPath = libProject.getCanonicalPath(); } catch (IOException e) { mLog.error(e, "Unable to resolve path to library project: %1$s", libraryPath); return false; } } else { resolvedPath = libProject.getAbsolutePath(); } println("Resolved location of library project to: %1$s", resolvedPath); // check the lib project exists if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); return false; } // look for other references to figure out the index int index = 1; while (true) { String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); assert props != null; if (props == null) { // This should not happen yet SDK bug 20535 says it can, not sure how. break; } String ref = props.getProperty(propName); if (ref == null) { break; } else { index++; } } String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); propsWC.setProperty(propName, libraryPath); saveProjectProps = true; } // save the default props if needed. if (saveProjectProps) { try { assert propsWC != null; propsWC.save(); if (writeProjectProp) { println("Updated and renamed %1$s to %2$s", PropertyType.LEGACY_DEFAULT.getFilename(), PropertyType.PROJECT.getFilename()); } else { println("Updated %1$s", PropertyType.PROJECT.getFilename()); } } catch (Exception e) { mLog.error(e, "Failed to write %1$s file in '%2$s'", PropertyType.PROJECT.getFilename(), folderPath); return false; } if (writeProjectProp) { // need to delete the default prop file. ProjectProperties.delete(projectFolder, PropertyType.LEGACY_DEFAULT); } } // Refresh/create "sdk" in local.properties // because the file may already exists and contain other values (like apk config), // we first try to load it. props = ProjectProperties.load(projectFolder, PropertyType.LOCAL); if (props == null) { propsWC = ProjectProperties.create(projectFolder, PropertyType.LOCAL); } else { propsWC = props.makeWorkingCopy(); } // set or replace the sdk location. propsWC.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); try { propsWC.save(); println("Updated %1$s", PropertyType.LOCAL.getFilename()); } catch (Exception e) { mLog.error(e, "Failed to write %1$s file in '%2$s'", PropertyType.LOCAL.getFilename(), folderPath); return false; } // legacy: check if build.properties must be renamed to ant.properties. props = ProjectProperties.load(projectFolder, PropertyType.ANT); if (props == null) { props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_BUILD); if (props != null) { try { // get a working copy with the new property type propsWC = props.makeWorkingCopy(PropertyType.ANT); propsWC.save(); // delete the old file ProjectProperties.delete(projectFolder, PropertyType.LEGACY_BUILD); println("Renamed %1$s to %2$s", PropertyType.LEGACY_BUILD.getFilename(), PropertyType.ANT.getFilename()); } catch (Exception e) { mLog.error(e, "Failed to write %1$s file in '%2$s'", PropertyType.ANT.getFilename(), folderPath); return false; } } } // Build.xml: create if not present or no <androidinit/> in it File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML); boolean needsBuildXml = projectName != null || !buildXml.exists(); // if it seems there's no need for a new build.xml, look for inside the file // to try to detect old ones that may need updating. if (!needsBuildXml) { // we are looking for version-tag: followed by either an integer or "custom". if (checkFileContainsRegexp(buildXml, "version-tag:\\s*custom") != null) { //$NON-NLS-1$ println("%1$s: Found version-tag: custom. File will not be updated.", SdkConstants.FN_BUILD_XML); } else { Matcher m = checkFileContainsRegexp(buildXml, "version-tag:\\s*(\\d+)"); //$NON-NLS-1$ if (m == null) { println("----------\n" + "%1$s: Failed to find version-tag string. File must be updated.\n" + "In order to not erase potential customizations, the file will not be automatically regenerated.\n" + "If no changes have been made to the file, delete it manually and run the command again.\n" + "If you have made customizations to the build process, the file must be manually updated.\n" + "It is recommended to:\n" + "\t* Copy current file to a safe location.\n" + "\t* Delete original file.\n" + "\t* Run command again to generate a new file.\n" + "\t* Port customizations to the new file, by looking at the new rules file\n" + "\t located at <SDK>/tools/ant/build.xml\n" + "\t* Update file to contain\n" + "\t version-tag: custom\n" + "\t to prevent file from being rewritten automatically by the SDK tools.\n" + "----------\n", SdkConstants.FN_BUILD_XML); } else { String versionStr = m.group(1); if (versionStr != null) { // can't fail due to regexp above. int version = Integer.parseInt(versionStr); if (version < MIN_BUILD_VERSION_TAG) { println("%1$s: Found version-tag: %2$d. Expected version-tag: %3$d: file must be updated.", SdkConstants.FN_BUILD_XML, version, MIN_BUILD_VERSION_TAG); needsBuildXml = true; } } } } } if (needsBuildXml) { // create the map for place-holders of values to replace in the templates final HashMap<String, String> keywords = new HashMap<String, String>(); // put the current version-tag value keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); // if there was no project name on the command line, figure one out. if (projectName == null) { // otherwise, take it from the existing build.xml if it exists already. if (buildXml.exists()) { try { XPathFactory factory = XPathFactory.newInstance(); XPath xpath = factory.newXPath(); projectName = xpath.evaluate(XPATH_PROJECT_NAME, new InputSource(new FileInputStream(buildXml))); } catch (XPathExpressionException e) { // this is ok since we're going to recreate the file. mLog.error(e, "Unable to find existing project name from %1$s", SdkConstants.FN_BUILD_XML); } catch (FileNotFoundException e) { // can't happen since we check above. } } // if the project is still null, then we find another way. if (projectName == null) { extractPackageFromManifest(androidManifest, keywords); if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) { String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME); // keep only the last segment if applicable int pos = activity.lastIndexOf('.'); if (pos != -1) { activity = activity.substring(pos + 1); } // Use the activity as project name projectName = activity; println("No project name specified, using Activity name '%1$s'.\n" + "If you wish to change it, edit the first line of %2$s.", activity, SdkConstants.FN_BUILD_XML); } else { // We need a project name. Just pick up the basename of the project // directory. File projectCanonicalFolder = projectFolder; try { projectCanonicalFolder = projectCanonicalFolder.getCanonicalFile(); } catch (IOException e) { // ignore, keep going } // Use the folder name as project name projectName = projectCanonicalFolder.getName(); println("No project name specified, using project folder name '%1$s'.\n" + "If you wish to change it, edit the first line of %2$s.", projectName, SdkConstants.FN_BUILD_XML); } } } // put the project name in the map for replacement during the template installation. keywords.put(PH_PROJECT_NAME, projectName); if (mLevel == OutputLevel.VERBOSE) { println("Regenerating %1$s with project name %2$s", SdkConstants.FN_BUILD_XML, keywords.get(PH_PROJECT_NAME)); } try { installTemplate("build.template", buildXml, keywords); } catch (ProjectCreateException e) { mLog.error(e, null); return false; } } if (hasProguard == false) { try { installTemplate(SdkConstants.FN_PROJECT_PROGUARD_FILE, // Write ProGuard config files with the extension .pro which // is what is used in the ProGuard documentation and samples new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), null /*placeholderMap*/); } catch (ProjectCreateException e) { mLog.error(e, null); return false; } } return true; } /** * Updates a test project with a new path to the main (tested) project. * @param folderPath the path of the test project. * @param pathToMainProject the path to the main project, relative to the test project. */ @SuppressWarnings("deprecation") public void updateTestProject(final String folderPath, final String pathToMainProject, final SdkManager sdkManager) { // since this is an update, check the folder does point to a project if (checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { return; } // check the path to the main project is valid. File mainProject = new File(pathToMainProject); String resolvedPath; if (mainProject.isAbsolute() == false) { mainProject = new File(folderPath, pathToMainProject); try { resolvedPath = mainProject.getCanonicalPath(); } catch (IOException e) { mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject); return; } } else { resolvedPath = mainProject.getAbsolutePath(); } println("Resolved location of main project to: %1$s", resolvedPath); // check the main project exists if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); return; } // now get the target from the main project ProjectProperties projectProp = ProjectProperties.load(resolvedPath, PropertyType.PROJECT); if (projectProp == null) { // legacy support for older file name. projectProp = ProjectProperties.load(resolvedPath, PropertyType.LEGACY_DEFAULT); if (projectProp == null) { mLog.error(null, "No %1$s at: %2$s", PropertyType.PROJECT.getFilename(), resolvedPath); return; } } String targetHash = projectProp.getProperty(ProjectProperties.PROPERTY_TARGET); if (targetHash == null) { mLog.error(null, "%1$s in the main project has no target property.", PropertyType.PROJECT.getFilename()); return; } IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash); if (target == null) { mLog.error(null, "Main project target %1$s is not a valid target.", targetHash); return; } // update test-project does not support the --name parameter, therefore the project // name should generally not be passed to updateProject(). // However if build.xml does not exist then updateProject() will recreate it. In this // case we will need the project name. // To do this, we look for the parent project name and add "test" to it. // If the main project does not have a project name (yet), then the default behavior // will be used (look for activity and then folder name) String projectName = null; XPathFactory factory = XPathFactory.newInstance(); XPath xpath = factory.newXPath(); File testBuildXml = new File(folderPath, SdkConstants.FN_BUILD_XML); if (testBuildXml.isFile() == false) { File mainBuildXml = new File(resolvedPath, SdkConstants.FN_BUILD_XML); if (mainBuildXml.isFile()) { try { // get the name of the main project and add Test to it. String mainProjectName = xpath.evaluate(XPATH_PROJECT_NAME, new InputSource(new FileInputStream(mainBuildXml))); projectName = mainProjectName + "Test"; } catch (XPathExpressionException e) { // it's ok, updateProject() will figure out a name automatically. // We do log the error though as the build.xml file may be broken. mLog.warning("Failed to parse %1$s.\n" + "File may not be valid. Consider running 'android update project' on the main project.", mainBuildXml.getPath()); } catch (FileNotFoundException e) { // should not happen since we check first. } } } // now update the project as if it's a normal project if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) { // error message has already been displayed. return; } // add the test project specific properties. // At this point, we know build.prop has been renamed ant.prop ProjectProperties antProps = ProjectProperties.load(folderPath, PropertyType.ANT); ProjectPropertiesWorkingCopy antWorkingCopy; if (antProps == null) { antWorkingCopy = ProjectProperties.create(folderPath, PropertyType.ANT); } else { antWorkingCopy = antProps.makeWorkingCopy(); } // set or replace the path to the main project antWorkingCopy.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject); try { antWorkingCopy.save(); println("Updated %1$s", PropertyType.ANT.getFilename()); } catch (Exception e) { mLog.error(e, "Failed to write %1$s file in '%2$s'", PropertyType.ANT.getFilename(), folderPath); return; } } /** * Checks whether the give <var>folderPath</var> is a valid project folder, and returns * a {@link FileWrapper} to the required file. * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it. * <p/>Any error are output using {@link #mLog}. * @param folderPath the folder to check * @param requiredFilename the file name of the file that's required. * @return a {@link FileWrapper} to the AndroidManifest.xml file, or null otherwise. */ private FileWrapper checkProjectFolder(String folderPath, String requiredFilename) { // project folder must exist and be a directory, since this is an update FolderWrapper projectFolder = new FolderWrapper(folderPath); if (!projectFolder.isDirectory()) { mLog.error(null, "Project folder '%1$s' is not a valid directory.", projectFolder); return null; } // Check AndroidManifest.xml is present FileWrapper requireFile = new FileWrapper(projectFolder, requiredFilename); if (!requireFile.isFile()) { mLog.error(null, "%1$s is not a valid project (%2$s not found).", folderPath, requiredFilename); return null; } return requireFile; } /** * Looks for a given regex in a file and returns the matcher if any line of the input file * contains the requested regexp. * * @param file the file to search. * @param regexp the regexp to search for. * * @return a Matcher or null if the regexp is not found. */ private Matcher checkFileContainsRegexp(File file, String regexp) { Pattern p = Pattern.compile(regexp); BufferedReader in = null; try { in = new BufferedReader(new FileReader(file)); String line; while ((line = in.readLine()) != null) { Matcher m = p.matcher(line); if (m.find()) { return m; } } in.close(); } catch (Exception e) { // ignore } finally { if (in != null) { try { in.close(); } catch (IOException e) { // ignore } } } return null; } /** * Extracts a "full" package & activity name from an AndroidManifest.xml. * <p/> * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}. * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}. * When no activity is found, this key is not created. * * @param manifestFile The AndroidManifest.xml file * @param outKeywords Place where to put the out parameters: package and activity names. * @return True if the package/activity was parsed and updated in the keyword dictionary. */ private boolean extractPackageFromManifest(File manifestFile, Map<String, String> outKeywords) { try { XPath xpath = AndroidXPathFactory.newXPath(); InputSource source = new InputSource(new FileReader(manifestFile)); String packageName = xpath.evaluate("/manifest/@package", source); source = new InputSource(new FileReader(manifestFile)); // Select the "android:name" attribute of all <activity> nodes but only if they // contain a sub-node <intent-filter><action> with an "android:name" attribute which // is 'android.intent.action.MAIN' and an <intent-filter><category> with an // "android:name" attribute which is 'android.intent.category.LAUNCHER' String expression = String.format("/manifest/application/activity" + "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " + "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" + "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX); NodeList activityNames = (NodeList) xpath.evaluate(expression, source, XPathConstants.NODESET); // If we get here, both XPath expressions were valid so we're most likely dealing // with an actual AndroidManifest.xml file. The nodes may not have the requested // attributes though, if which case we should warn. if (packageName == null || packageName.isEmpty()) { mLog.error(null, "Missing <manifest package=\"...\"> in '%1$s'", manifestFile.getName()); return false; } // Get the first activity that matched earlier. If there is no activity, // activityName is set to an empty string and the generated "combined" name // will be in the form "package." (with a dot at the end). String activityName = ""; if (activityNames.getLength() > 0) { activityName = activityNames.item(0).getNodeValue(); } if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) { println("WARNING: There is more than one activity defined in '%1$s'.\n" + "Only the first one will be used. If this is not appropriate, you need\n" + "to specify one of these values manually instead:", manifestFile.getName()); for (int i = 0; i < activityNames.getLength(); i++) { String name = activityNames.item(i).getNodeValue(); name = combinePackageActivityNames(packageName, name); println("- %1$s", name); } } if (activityName.isEmpty()) { mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" + "No activity will be generated.", AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName()); } else { outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName); } outKeywords.put(PH_PACKAGE, packageName); return true; } catch (IOException e) { mLog.error(e, "Failed to read %1$s", manifestFile.getName()); } catch (XPathExpressionException e) { Throwable t = e.getCause(); mLog.error(t == null ? e : t, "Failed to parse %1$s", manifestFile.getName()); } return false; } private String combinePackageActivityNames(String packageName, String activityName) { // Activity Name can have 3 forms: // - ".Name" means this is a class name in the given package name. // The full FQCN is thus packageName + ".Name" // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name" // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is. // To be valid, the package name should have at least two components. This is checked // later during the creation of the build.xml file, so we just need to detect there's // a dot but not at pos==0. int pos = activityName.indexOf('.'); if (pos == 0) { return packageName + activityName; } else if (pos > 0) { return activityName; } else { return packageName + "." + activityName; } } /** * Installs a new file that is based on a template file provided by a given target. * Each match of each key from the place-holder map in the template will be replaced with its * corresponding value in the created file. * * @param templateName the name of to the template file * @param destFile the path to the destination file, relative to the project * @param placeholderMap a map of (place-holder, value) to create the file from the template. * @param target the Target of the project that will be providing the template. * @throws ProjectCreateException */ private void installTargetTemplate(String templateName, File destFile, Map<String, String> placeholderMap, IAndroidTarget target) throws ProjectCreateException { // query the target for its template directory String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); final String sourcePath = templateFolder + File.separator + templateName; installFullPathTemplate(sourcePath, destFile, placeholderMap); } /** * Installs a new file from the gradle wrapper template. * * @param templateName the name of the template file * @param projectFolder the path to the project folder * @throws ProjectCreateException */ public void installGradleWrapperFile(String templateName, File projectFolder) throws ProjectCreateException { String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_TEMPLATES_GRADLE_WRAPPER_FOLDER; installBinaryFile(new File(templateFolder, templateName), new File(projectFolder, templateName)); } /** * Installs a new file that is based on a template file provided by the tools folder. * Each match of each key from the place-holder map in the template will be replaced with its * corresponding value in the created file. * * @param templateName the name of to the template file * @param destFile the path to the destination file, relative to the project * @param placeholderMap a map of (place-holder, value) to create the file from the template. * @throws ProjectCreateException */ public void installTemplate(String templateName, File destFile, Map<String, String> placeholderMap) throws ProjectCreateException { // query the target for its template directory String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; final String sourcePath = templateFolder + File.separator + templateName; installFullPathTemplate(sourcePath, destFile, placeholderMap); } /** * Installs a new file that is based on a template. * Each match of each key from the place-holder map in the template will be replaced with its * corresponding value in the created file. * * @param sourcePath the full path to the source template file * @param destFile the destination file * @param placeholderMap a map of (place-holder, value) to create the file from the template. * @throws ProjectCreateException */ private void installFullPathTemplate(String sourcePath, File destFile, Map<String, String> placeholderMap) throws ProjectCreateException { boolean existed = destFile.exists(); try { BufferedWriter out = new BufferedWriter(new FileWriter(destFile)); BufferedReader in = new BufferedReader(new FileReader(sourcePath)); String line; while ((line = in.readLine()) != null) { if (placeholderMap != null) { for (Map.Entry<String, String> entry : placeholderMap.entrySet()) { line = line.replace(entry.getKey(), entry.getValue()); } } out.write(line); out.newLine(); } out.close(); in.close(); } catch (Exception e) { throw new ProjectCreateException(e, "Could not access %1$s: %2$s", destFile, e.getMessage()); } println("%1$s file %2$s", existed ? "Updated" : "Added", destFile); } /** * Installs the project icons. * @param resourceFolder the resource folder * @param target the target of the project. * @return true if any icon was installed. */ private boolean installIcons(File resourceFolder, IAndroidTarget target) throws ProjectCreateException { // query the target for its template directory String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); boolean installedIcon = false; installedIcon |= installIcon(templateFolder, "ic_launcher_xhdpi.png", resourceFolder, "drawable-xhdpi"); installedIcon |= installIcon(templateFolder, "ic_launcher_hdpi.png", resourceFolder, "drawable-hdpi"); installedIcon |= installIcon(templateFolder, "ic_launcher_mdpi.png", resourceFolder, "drawable-mdpi"); installedIcon |= installIcon(templateFolder, "ic_launcher_ldpi.png", resourceFolder, "drawable-ldpi"); return installedIcon; } /** * Installs an Icon in the project. * @return true if the icon was installed. */ private boolean installIcon(String templateFolder, String iconName, File resourceFolder, String folderName) throws ProjectCreateException { File icon = new File(templateFolder, iconName); if (icon.exists()) { File drawable = createDirs(resourceFolder, folderName); installBinaryFile(icon, new File(drawable, "ic_launcher.png")); return true; } return false; } /** * Installs a binary file * @param source the source file to copy * @param destination the destination file to write * @throws ProjectCreateException */ private void installBinaryFile(File source, File destination) throws ProjectCreateException { byte[] buffer = new byte[8192]; FileInputStream fis = null; FileOutputStream fos = null; try { fis = new FileInputStream(source); fos = new FileOutputStream(destination); int read; while ((read = fis.read(buffer)) != -1) { fos.write(buffer, 0, read); } } catch (FileNotFoundException e) { // shouldn't happen since we check before. } catch (IOException e) { throw new ProjectCreateException(e, "Failed to read binary file: %1$s", source.getAbsolutePath()); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { // ignore } } if (fos != null) { try { fos.close(); } catch (IOException e) { // ignore } } } } /** * Prints a message unless silence is enabled. * <p/> * This is just a convenience wrapper around {@link ILogger#info(String, Object...)} from * {@link #mLog} after testing if output level is {@link OutputLevel#VERBOSE}. * * @param format Format for String.format * @param args Arguments for String.format */ private void println(String format, Object... args) { if (mLevel != OutputLevel.SILENT) { if (!format.endsWith("\n")) { format += "\n"; } mLog.info(format, args); } } /** * Creates a new folder, along with any parent folders that do not exists. * * @param parent the parent folder * @param name the name of the directory to create. * @throws ProjectCreateException */ private File createDirs(File parent, String name) throws ProjectCreateException { final File newFolder = new File(parent, name); boolean existedBefore = true; if (!newFolder.exists()) { if (!newFolder.mkdirs()) { throw new ProjectCreateException("Could not create directory: %1$s", newFolder); } existedBefore = false; } if (newFolder.isDirectory()) { if (!newFolder.canWrite()) { throw new ProjectCreateException("Path is not writable: %1$s", newFolder); } } else { throw new ProjectCreateException("Path is not a directory: %1$s", newFolder); } if (!existedBefore) { try { println("Created directory %1$s", newFolder.getCanonicalPath()); } catch (IOException e) { throw new ProjectCreateException( "Could not determine canonical path of created directory", e); } } return newFolder; } /** * Strips the string of beginning and trailing characters (multiple * characters will be stripped, example stripString("..test...", '.') * results in "test"; * * @param s the string to strip * @param strip the character to strip from beginning and end * @return the stripped string or the empty string if everything is stripped. */ private static String stripString(String s, char strip) { final int sLen = s.length(); int newStart = 0, newEnd = sLen - 1; while (newStart < sLen && s.charAt(newStart) == strip) { newStart++; } while (newEnd >= 0 && s.charAt(newEnd) == strip) { newEnd--; } /* * newEnd contains a char we want, and substring takes end as being * exclusive */ newEnd++; if (newStart >= sLen || newEnd < 0) { return ""; } return s.substring(newStart, newEnd); } }