1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.sdklib.internal.project; 18 19 import com.android.AndroidConstants; 20 import com.android.io.FileWrapper; 21 import com.android.io.FolderWrapper; 22 import com.android.sdklib.IAndroidTarget; 23 import com.android.sdklib.ISdkLog; 24 import com.android.sdklib.SdkConstants; 25 import com.android.sdklib.SdkManager; 26 import com.android.sdklib.internal.project.ProjectProperties.PropertyType; 27 import com.android.sdklib.xml.AndroidManifest; 28 import com.android.sdklib.xml.AndroidXPathFactory; 29 30 import org.w3c.dom.NodeList; 31 import org.xml.sax.InputSource; 32 33 import java.io.BufferedReader; 34 import java.io.BufferedWriter; 35 import java.io.File; 36 import java.io.FileInputStream; 37 import java.io.FileNotFoundException; 38 import java.io.FileOutputStream; 39 import java.io.FileReader; 40 import java.io.FileWriter; 41 import java.io.IOException; 42 import java.util.HashMap; 43 import java.util.Map; 44 import java.util.regex.Matcher; 45 import java.util.regex.Pattern; 46 47 import javax.xml.xpath.XPath; 48 import javax.xml.xpath.XPathConstants; 49 import javax.xml.xpath.XPathExpressionException; 50 import javax.xml.xpath.XPathFactory; 51 52 /** 53 * Creates the basic files needed to get an Android project up and running. 54 * 55 * @hide 56 */ 57 public class ProjectCreator { 58 59 /** Version of the build.xml. Stored in version-tag */ 60 private final static int MIN_BUILD_VERSION_TAG = 1; 61 62 /** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */ 63 private final static String PH_JAVA_FOLDER = "PACKAGE_PATH"; 64 /** Package name substitution string used in template files, i.e. "PACKAGE" */ 65 private final static String PH_PACKAGE = "PACKAGE"; 66 /** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME". 67 * @deprecated This is only used for older templates. For new ones see 68 * {@link #PH_ACTIVITY_ENTRY_NAME}, and {@link #PH_ACTIVITY_CLASS_NAME}. */ 69 @Deprecated 70 private final static String PH_ACTIVITY_NAME = "ACTIVITY_NAME"; 71 /** Activity name substitution string used in manifest templates, i.e. "ACTIVITY_ENTRY_NAME".*/ 72 private final static String PH_ACTIVITY_ENTRY_NAME = "ACTIVITY_ENTRY_NAME"; 73 /** Activity name substitution string used in class templates, i.e. "ACTIVITY_CLASS_NAME".*/ 74 private final static String PH_ACTIVITY_CLASS_NAME = "ACTIVITY_CLASS_NAME"; 75 /** Activity FQ-name substitution string used in class templates, i.e. "ACTIVITY_FQ_NAME".*/ 76 private final static String PH_ACTIVITY_FQ_NAME = "ACTIVITY_FQ_NAME"; 77 /** Original Activity class name substitution string used in class templates, i.e. 78 * "ACTIVITY_TESTED_CLASS_NAME".*/ 79 private final static String PH_ACTIVITY_TESTED_CLASS_NAME = "ACTIVITY_TESTED_CLASS_NAME"; 80 /** Project name substitution string used in template files, i.e. "PROJECT_NAME". */ 81 private final static String PH_PROJECT_NAME = "PROJECT_NAME"; 82 /** Application icon substitution string used in the manifest template */ 83 private final static String PH_ICON = "ICON"; 84 /** Version tag name substitution string used in template files, i.e. "VERSION_TAG". */ 85 private final static String PH_VERSION_TAG = "VERSION_TAG"; 86 87 /** The xpath to find a project name in an Ant build file. */ 88 private static final String XPATH_PROJECT_NAME = "/project/@name"; 89 90 /** Pattern for characters accepted in a project name. Since this will be used as a 91 * directory name, we're being a bit conservative on purpose: dot and space cannot be used. */ 92 public static final Pattern RE_PROJECT_NAME = Pattern.compile("[a-zA-Z0-9_]+"); 93 /** List of valid characters for a project name. Used for display purposes. */ 94 public final static String CHARS_PROJECT_NAME = "a-z A-Z 0-9 _"; 95 96 /** Pattern for characters accepted in a package name. A package is list of Java identifier 97 * separated by a dot. We need to have at least one dot (e.g. a two-level package name). 98 * A Java identifier cannot start by a digit. */ 99 public static final Pattern RE_PACKAGE_NAME = 100 Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)+"); 101 /** List of valid characters for a project name. Used for display purposes. */ 102 public final static String CHARS_PACKAGE_NAME = "a-z A-Z 0-9 _"; 103 104 /** Pattern for characters accepted in an activity name, which is a Java identifier. */ 105 public static final Pattern RE_ACTIVITY_NAME = 106 Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); 107 /** List of valid characters for a project name. Used for display purposes. */ 108 public final static String CHARS_ACTIVITY_NAME = "a-z A-Z 0-9 _"; 109 110 111 public enum OutputLevel { 112 /** Silent mode. Project creation will only display errors. */ 113 SILENT, 114 /** Normal mode. Project creation will display what's being done, display 115 * error but not warnings. */ 116 NORMAL, 117 /** Verbose mode. Project creation will display what's being done, errors and warnings. */ 118 VERBOSE; 119 } 120 121 /** 122 * Exception thrown when a project creation fails, typically because a template 123 * file cannot be written. 124 */ 125 private static class ProjectCreateException extends Exception { 126 /** default UID. This will not be serialized anyway. */ 127 private static final long serialVersionUID = 1L; 128 129 @SuppressWarnings("unused") ProjectCreateException(String message)130 ProjectCreateException(String message) { 131 super(message); 132 } 133 ProjectCreateException(Throwable t, String format, Object... args)134 ProjectCreateException(Throwable t, String format, Object... args) { 135 super(format != null ? String.format(format, args) : format, t); 136 } 137 ProjectCreateException(String format, Object... args)138 ProjectCreateException(String format, Object... args) { 139 super(String.format(format, args)); 140 } 141 } 142 143 /** The {@link OutputLevel} verbosity. */ 144 private final OutputLevel mLevel; 145 /** Logger for errors and output. Cannot be null. */ 146 private final ISdkLog mLog; 147 /** The OS path of the SDK folder. */ 148 private final String mSdkFolder; 149 /** The {@link SdkManager} instance. */ 150 private final SdkManager mSdkManager; 151 152 /** 153 * Helper class to create android projects. 154 * 155 * @param sdkManager The {@link SdkManager} instance. 156 * @param sdkFolder The OS path of the SDK folder. 157 * @param level The {@link OutputLevel} verbosity. 158 * @param log Logger for errors and output. Cannot be null. 159 */ ProjectCreator(SdkManager sdkManager, String sdkFolder, OutputLevel level, ISdkLog log)160 public ProjectCreator(SdkManager sdkManager, String sdkFolder, OutputLevel level, ISdkLog log) { 161 mSdkManager = sdkManager; 162 mSdkFolder = sdkFolder; 163 mLevel = level; 164 mLog = log; 165 } 166 167 /** 168 * Creates a new project. 169 * <p/> 170 * The caller should have already checked and sanitized the parameters. 171 * 172 * @param folderPath the folder of the project to create. 173 * @param projectName the name of the project. The name must match the 174 * {@link #RE_PROJECT_NAME} regex. 175 * @param packageName the package of the project. The name must match the 176 * {@link #RE_PACKAGE_NAME} regex. 177 * @param activityEntry the activity of the project as it will appear in the manifest. Can be 178 * null if no activity should be created. The name must match the 179 * {@link #RE_ACTIVITY_NAME} regex. 180 * @param target the project target. 181 * @param library whether the project is a library. 182 * @param pathToMainProject if non-null the project will be setup to test a main project 183 * located at the given path. 184 */ createProject(String folderPath, String projectName, String packageName, String activityEntry, IAndroidTarget target, boolean library, String pathToMainProject)185 public void createProject(String folderPath, String projectName, 186 String packageName, String activityEntry, IAndroidTarget target, boolean library, 187 String pathToMainProject) { 188 189 // create project folder if it does not exist 190 File projectFolder = checkNewProjectLocation(folderPath); 191 if (projectFolder == null) { 192 return; 193 } 194 195 try { 196 boolean isTestProject = pathToMainProject != null; 197 198 // first create the project properties. 199 200 // location of the SDK goes in localProperty 201 ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath, 202 PropertyType.LOCAL); 203 localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); 204 localProperties.save(); 205 206 // target goes in default properties 207 ProjectPropertiesWorkingCopy defaultProperties = ProjectProperties.create(folderPath, 208 PropertyType.PROJECT); 209 defaultProperties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); 210 if (library) { 211 defaultProperties.setProperty(ProjectProperties.PROPERTY_LIBRARY, "true"); 212 } 213 defaultProperties.save(); 214 215 // create a build.properties file with just the application package 216 ProjectPropertiesWorkingCopy buildProperties = ProjectProperties.create(folderPath, 217 PropertyType.ANT); 218 219 if (isTestProject) { 220 buildProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, 221 pathToMainProject); 222 } 223 224 buildProperties.save(); 225 226 // create the map for place-holders of values to replace in the templates 227 final HashMap<String, String> keywords = new HashMap<String, String>(); 228 229 // create the required folders. 230 // compute src folder path 231 final String packagePath = 232 stripString(packageName.replace(".", File.separator), 233 File.separatorChar); 234 235 // put this path in the place-holder map for project files that needs to list 236 // files manually. 237 keywords.put(PH_JAVA_FOLDER, packagePath); 238 keywords.put(PH_PACKAGE, packageName); 239 keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); 240 241 242 // compute some activity related information 243 String fqActivityName = null, activityPath = null, activityClassName = null; 244 String originalActivityEntry = activityEntry; 245 String originalActivityClassName = null; 246 if (activityEntry != null) { 247 if (isTestProject) { 248 // append Test so that it doesn't collide with the main project activity. 249 activityEntry += "Test"; 250 251 // get the classname from the original activity entry. 252 int pos = originalActivityEntry.lastIndexOf('.'); 253 if (pos != -1) { 254 originalActivityClassName = originalActivityEntry.substring(pos + 1); 255 } else { 256 originalActivityClassName = originalActivityEntry; 257 } 258 } 259 260 // get the fully qualified name of the activity 261 fqActivityName = AndroidManifest.combinePackageAndClassName(packageName, 262 activityEntry); 263 264 // get the activity path (replace the . to /) 265 activityPath = stripString(fqActivityName.replace(".", File.separator), 266 File.separatorChar); 267 268 // remove the last segment, so that we only have the path to the activity, but 269 // not the activity filename itself. 270 activityPath = activityPath.substring(0, 271 activityPath.lastIndexOf(File.separatorChar)); 272 273 // finally, get the class name for the activity 274 activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1); 275 } 276 277 // at this point we have the following for the activity: 278 // activityEntry: this is the manifest entry. For instance .MyActivity 279 // fqActivityName: full-qualified class name: com.foo.MyActivity 280 // activityClassName: only the classname: MyActivity 281 // originalActivityClassName: the classname of the activity being tested (if applicable) 282 283 // Add whatever activity info is needed in the place-holder map. 284 // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests). 285 if (target.getVersion().getApiLevel() < 4) { // legacy 286 if (originalActivityEntry != null) { 287 keywords.put(PH_ACTIVITY_NAME, originalActivityEntry); 288 } 289 } else { 290 // newer templates make a difference between the manifest entries, classnames, 291 // as well as the main and test classes. 292 if (activityEntry != null) { 293 keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry); 294 keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName); 295 keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName); 296 if (originalActivityClassName != null) { 297 keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName); 298 } 299 } 300 } 301 302 // Take the project name from the command line if there's one 303 if (projectName != null) { 304 keywords.put(PH_PROJECT_NAME, projectName); 305 } else { 306 if (activityClassName != null) { 307 // Use the activity class name as project name 308 keywords.put(PH_PROJECT_NAME, activityClassName); 309 } else { 310 // We need a project name. Just pick up the basename of the project 311 // directory. 312 projectName = projectFolder.getName(); 313 keywords.put(PH_PROJECT_NAME, projectName); 314 } 315 } 316 317 // create the source folder for the activity 318 if (activityClassName != null) { 319 String srcActivityFolderPath = 320 SdkConstants.FD_SOURCES + File.separator + activityPath; 321 File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); 322 323 String javaTemplate = isTestProject ? "java_tests_file.template" 324 : "java_file.template"; 325 String activityFileName = activityClassName + ".java"; 326 327 installTargetTemplate(javaTemplate, new File(sourceFolder, activityFileName), 328 keywords, target); 329 } else { 330 // we should at least create 'src' 331 createDirs(projectFolder, SdkConstants.FD_SOURCES); 332 } 333 334 // create other useful folders 335 File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES); 336 createDirs(projectFolder, SdkConstants.FD_OUTPUT); 337 createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS); 338 339 if (isTestProject == false) { 340 /* Make res files only for non test projects */ 341 File valueFolder = createDirs(resourceFolder, AndroidConstants.FD_RES_VALUES); 342 installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"), 343 keywords, target); 344 345 File layoutFolder = createDirs(resourceFolder, AndroidConstants.FD_RES_LAYOUT); 346 installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"), 347 keywords, target); 348 349 // create the icons 350 if (installIcons(resourceFolder, target)) { 351 keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\""); 352 } else { 353 keywords.put(PH_ICON, ""); 354 } 355 } 356 357 /* Make AndroidManifest.xml and build.xml files */ 358 String manifestTemplate = "AndroidManifest.template"; 359 if (isTestProject) { 360 manifestTemplate = "AndroidManifest.tests.template"; 361 } 362 363 installTargetTemplate(manifestTemplate, 364 new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML), 365 keywords, target); 366 367 installTemplate("build.template", 368 new File(projectFolder, SdkConstants.FN_BUILD_XML), 369 keywords); 370 371 // install the proguard config file. 372 installTemplate(SdkConstants.FN_PROGUARD_CFG, 373 new File(projectFolder, SdkConstants.FN_PROGUARD_CFG), 374 null /*keywords*/); 375 } catch (Exception e) { 376 mLog.error(e, null); 377 } 378 } 379 checkNewProjectLocation(String folderPath)380 private File checkNewProjectLocation(String folderPath) { 381 File projectFolder = new File(folderPath); 382 if (!projectFolder.exists()) { 383 384 boolean created = false; 385 Throwable t = null; 386 try { 387 created = projectFolder.mkdirs(); 388 } catch (Exception e) { 389 t = e; 390 } 391 392 if (created) { 393 println("Created project directory: %1$s", projectFolder); 394 } else { 395 mLog.error(t, "Could not create directory: %1$s", projectFolder); 396 return null; 397 } 398 } else { 399 Exception e = null; 400 String error = null; 401 try { 402 String[] content = projectFolder.list(); 403 if (content == null) { 404 error = "Project folder '%1$s' is not a directory."; 405 } else if (content.length != 0) { 406 error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead."; 407 } 408 } catch (Exception e1) { 409 e = e1; 410 } 411 412 if (e != null || error != null) { 413 mLog.error(e, error, projectFolder, SdkConstants.androidCmdName()); 414 } 415 } 416 return projectFolder; 417 } 418 419 /** 420 * Updates an existing project. 421 * <p/> 422 * Workflow: 423 * <ul> 424 * <li> Check AndroidManifest.xml is present (required) 425 * <li> Check if there's a legacy properties file and convert it 426 * <li> Check there's a project.properties with a target *or* --target was specified 427 * <li> Update default.prop if --target was specified 428 * <li> Refresh/create "sdk" in local.properties 429 * <li> Build.xml: create if not present or if version-tag is found or not. version-tag:custom 430 * prevent any overwrite. version-tag:[integer] will override. missing version-tag will query 431 * the dev. 432 * </ul> 433 * 434 * @param folderPath the folder of the project to update. This folder must exist. 435 * @param target the project target. Can be null. 436 * @param projectName The project name from --name. Can be null. 437 * @param libraryPath the path to a library to add to the references. Can be null. 438 * @return true if the project was successfully updated. 439 */ 440 @SuppressWarnings("deprecation") updateProject(String folderPath, IAndroidTarget target, String projectName, String libraryPath)441 public boolean updateProject(String folderPath, IAndroidTarget target, String projectName, 442 String libraryPath) { 443 // since this is an update, check the folder does point to a project 444 FileWrapper androidManifest = checkProjectFolder(folderPath, 445 SdkConstants.FN_ANDROID_MANIFEST_XML); 446 if (androidManifest == null) { 447 return false; 448 } 449 450 // get the parent folder. 451 FolderWrapper projectFolder = (FolderWrapper) androidManifest.getParentFolder(); 452 453 boolean hasProguard = false; 454 455 // Check there's a project.properties with a target *or* --target was specified 456 IAndroidTarget originalTarget = null; 457 boolean writeProjectProp = false; 458 ProjectProperties props = ProjectProperties.load(projectFolder, PropertyType.PROJECT); 459 460 if (props == null) { 461 // no project.properties, try to load default.properties 462 props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_DEFAULT); 463 writeProjectProp = true; 464 } 465 466 if (props != null) { 467 String targetHash = props.getProperty(ProjectProperties.PROPERTY_TARGET); 468 originalTarget = mSdkManager.getTargetFromHashString(targetHash); 469 470 // if the project is already setup with proguard, we won't copy the proguard config. 471 hasProguard = props.getProperty(ProjectProperties.PROPERTY_PROGUARD_CONFIG) != null; 472 } 473 474 if (originalTarget == null && target == null) { 475 mLog.error(null, 476 "The project either has no target set or the target is invalid.\n" + 477 "Please provide a --target to the '%1$s update' command.", 478 SdkConstants.androidCmdName()); 479 return false; 480 } 481 482 boolean saveProjectProps = false; 483 484 ProjectPropertiesWorkingCopy propsWC = null; 485 486 // Update default.prop if --target was specified 487 if (target != null || writeProjectProp) { 488 // we already attempted to load the file earlier, if that failed, create it. 489 if (props == null) { 490 propsWC = ProjectProperties.create(projectFolder, PropertyType.PROJECT); 491 } else { 492 propsWC = props.makeWorkingCopy(PropertyType.PROJECT); 493 } 494 495 // set or replace the target 496 if (target != null) { 497 propsWC.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); 498 } 499 saveProjectProps = true; 500 } 501 502 if (libraryPath != null) { 503 // At this point, the default properties already exists, either because they were 504 // already there or because they were created with a new target 505 if (propsWC == null) { 506 assert props != null; 507 propsWC = props.makeWorkingCopy(); 508 } 509 510 // check the reference is valid 511 File libProject = new File(libraryPath); 512 String resolvedPath; 513 if (libProject.isAbsolute() == false) { 514 libProject = new File(projectFolder, libraryPath); 515 try { 516 resolvedPath = libProject.getCanonicalPath(); 517 } catch (IOException e) { 518 mLog.error(e, "Unable to resolve path to library project: %1$s", libraryPath); 519 return false; 520 } 521 } else { 522 resolvedPath = libProject.getAbsolutePath(); 523 } 524 525 println("Resolved location of library project to: %1$s", resolvedPath); 526 527 // check the lib project exists 528 if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { 529 mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); 530 return false; 531 } 532 533 // look for other references to figure out the index 534 int index = 1; 535 while (true) { 536 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); 537 assert props != null; 538 String ref = props.getProperty(propName); 539 if (ref == null) { 540 break; 541 } else { 542 index++; 543 } 544 } 545 546 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); 547 propsWC.setProperty(propName, libraryPath); 548 saveProjectProps = true; 549 } 550 551 // save the default props if needed. 552 if (saveProjectProps) { 553 try { 554 assert propsWC != null; 555 propsWC.save(); 556 if (writeProjectProp) { 557 println("Updated and renamed %1$s to %2$s", 558 PropertyType.LEGACY_DEFAULT.getFilename(), 559 PropertyType.PROJECT.getFilename()); 560 } else { 561 println("Updated %1$s", PropertyType.PROJECT.getFilename()); 562 } 563 } catch (Exception e) { 564 mLog.error(e, "Failed to write %1$s file in '%2$s'", 565 PropertyType.PROJECT.getFilename(), 566 folderPath); 567 return false; 568 } 569 570 if (writeProjectProp) { 571 // need to delete the default prop file. 572 ProjectProperties.delete(projectFolder, PropertyType.LEGACY_DEFAULT); 573 } 574 } 575 576 // Refresh/create "sdk" in local.properties 577 // because the file may already exists and contain other values (like apk config), 578 // we first try to load it. 579 props = ProjectProperties.load(projectFolder, PropertyType.LOCAL); 580 if (props == null) { 581 propsWC = ProjectProperties.create(projectFolder, PropertyType.LOCAL); 582 } else { 583 propsWC = props.makeWorkingCopy(); 584 } 585 586 // set or replace the sdk location. 587 propsWC.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); 588 try { 589 propsWC.save(); 590 println("Updated %1$s", PropertyType.LOCAL.getFilename()); 591 } catch (Exception e) { 592 mLog.error(e, "Failed to write %1$s file in '%2$s'", 593 PropertyType.LOCAL.getFilename(), 594 folderPath); 595 return false; 596 } 597 598 // legacy: check if build.properties must be renamed to ant.properties. 599 props = ProjectProperties.load(projectFolder, PropertyType.ANT); 600 if (props == null) { 601 props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_BUILD); 602 if (props != null) { 603 try { 604 // get a working copy with the new property type 605 propsWC = props.makeWorkingCopy(PropertyType.ANT); 606 propsWC.save(); 607 608 // delete the old file 609 ProjectProperties.delete(projectFolder, PropertyType.LEGACY_BUILD); 610 611 println("Renamed %1$s to %2$s", 612 PropertyType.LEGACY_BUILD.getFilename(), 613 PropertyType.ANT.getFilename()); 614 } catch (Exception e) { 615 mLog.error(e, "Failed to write %1$s file in '%2$s'", 616 PropertyType.ANT.getFilename(), 617 folderPath); 618 return false; 619 } 620 } 621 } 622 623 // Build.xml: create if not present or no <androidinit/> in it 624 File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML); 625 boolean needsBuildXml = projectName != null || !buildXml.exists(); 626 627 // if it seems there's no need for a new build.xml, look for inside the file 628 // to try to detect old ones that may need updating. 629 if (!needsBuildXml) { 630 // we are looking for version-tag: followed by either an integer or "custom". 631 if (checkFileContainsRegexp(buildXml, "version-tag:\\s*custom") != null) { //$NON-NLS-1$ 632 println("%1$s: Found version-tag: custom. File will not be updated.", 633 SdkConstants.FN_BUILD_XML); 634 } else { 635 Matcher m = checkFileContainsRegexp(buildXml, "version-tag:\\s*(\\d+)"); //$NON-NLS-1$ 636 if (m == null) { 637 println("----------\n" + 638 "%1$s: Failed to find version-tag string. File must be updated.\n" + 639 "In order to not erase potential customizations, the file will not be automatically regenerated.\n" + 640 "If no changes have been made to the file, delete it manually and run the command again.\n" + 641 "If you have made customizations to the build process, the file must be manually updated.\n" + 642 "It is recommended to:\n" + 643 "\t* Copy current file to a safe location.\n" + 644 "\t* Delete original file.\n" + 645 "\t* Run command again to generate a new file.\n" + 646 "\t* Port customizations to the new file, by looking at the new rules file\n" + 647 "\t located at <SDK>/tools/ant/build.xml\n" + 648 "\t* Update file to contain\n" + 649 "\t version-tag: custom\n" + 650 "\t to prevent file from being rewritten automatically by the SDK tools.\n" + 651 "----------\n", 652 SdkConstants.FN_BUILD_XML); 653 } else { 654 String versionStr = m.group(1); 655 if (versionStr != null) { 656 // can't fail due to regexp above. 657 int version = Integer.parseInt(versionStr); 658 if (version < MIN_BUILD_VERSION_TAG) { 659 println("%1$s: Found version-tag: %2$d. Expected version-tag: %3$d: file must be updated.", 660 SdkConstants.FN_BUILD_XML, version, MIN_BUILD_VERSION_TAG); 661 needsBuildXml = true; 662 } 663 } 664 } 665 } 666 } 667 668 if (needsBuildXml) { 669 // create the map for place-holders of values to replace in the templates 670 final HashMap<String, String> keywords = new HashMap<String, String>(); 671 672 // put the current version-tag value 673 keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); 674 675 // if there was no project name on the command line, figure one out. 676 if (projectName == null) { 677 // otherwise, take it from the existing build.xml if it exists already. 678 if (buildXml.exists()) { 679 try { 680 XPathFactory factory = XPathFactory.newInstance(); 681 XPath xpath = factory.newXPath(); 682 683 projectName = xpath.evaluate(XPATH_PROJECT_NAME, 684 new InputSource(new FileInputStream(buildXml))); 685 } catch (XPathExpressionException e) { 686 // this is ok since we're going to recreate the file. 687 mLog.error(e, "Unable to find existing project name from %1$s", 688 SdkConstants.FN_BUILD_XML); 689 } catch (FileNotFoundException e) { 690 // can't happen since we check above. 691 } 692 } 693 694 // if the project is still null, then we find another way. 695 if (projectName == null) { 696 extractPackageFromManifest(androidManifest, keywords); 697 if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) { 698 String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME); 699 // keep only the last segment if applicable 700 int pos = activity.lastIndexOf('.'); 701 if (pos != -1) { 702 activity = activity.substring(pos + 1); 703 } 704 705 // Use the activity as project name 706 projectName = activity; 707 708 println("No project name specified, using Activity name '%1$s'.\n" + 709 "If you wish to change it, edit the first line of %2$s.", 710 activity, SdkConstants.FN_BUILD_XML); 711 } else { 712 // We need a project name. Just pick up the basename of the project 713 // directory. 714 File projectCanonicalFolder = projectFolder; 715 try { 716 projectCanonicalFolder = projectCanonicalFolder.getCanonicalFile(); 717 } catch (IOException e) { 718 // ignore, keep going 719 } 720 721 // Use the folder name as project name 722 projectName = projectCanonicalFolder.getName(); 723 724 println("No project name specified, using project folder name '%1$s'.\n" + 725 "If you wish to change it, edit the first line of %2$s.", 726 projectName, SdkConstants.FN_BUILD_XML); 727 } 728 } 729 } 730 731 // put the project name in the map for replacement during the template installation. 732 keywords.put(PH_PROJECT_NAME, projectName); 733 734 if (mLevel == OutputLevel.VERBOSE) { 735 println("Regenerating %1$s with project name %2$s", 736 SdkConstants.FN_BUILD_XML, 737 keywords.get(PH_PROJECT_NAME)); 738 } 739 740 try { 741 installTemplate("build.template", buildXml, keywords); 742 } catch (ProjectCreateException e) { 743 mLog.error(e, null); 744 return false; 745 } 746 } 747 748 if (hasProguard == false) { 749 try { 750 installTemplate(SdkConstants.FN_PROGUARD_CFG, 751 new File(projectFolder, SdkConstants.FN_PROGUARD_CFG), 752 null /*placeholderMap*/); 753 } catch (ProjectCreateException e) { 754 mLog.error(e, null); 755 return false; 756 } 757 } 758 759 return true; 760 } 761 762 /** 763 * Updates a test project with a new path to the main (tested) project. 764 * @param folderPath the path of the test project. 765 * @param pathToMainProject the path to the main project, relative to the test project. 766 */ 767 @SuppressWarnings("deprecation") updateTestProject(final String folderPath, final String pathToMainProject, final SdkManager sdkManager)768 public void updateTestProject(final String folderPath, final String pathToMainProject, 769 final SdkManager sdkManager) { 770 // since this is an update, check the folder does point to a project 771 if (checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { 772 return; 773 } 774 775 // check the path to the main project is valid. 776 File mainProject = new File(pathToMainProject); 777 String resolvedPath; 778 if (mainProject.isAbsolute() == false) { 779 mainProject = new File(folderPath, pathToMainProject); 780 try { 781 resolvedPath = mainProject.getCanonicalPath(); 782 } catch (IOException e) { 783 mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject); 784 return; 785 } 786 } else { 787 resolvedPath = mainProject.getAbsolutePath(); 788 } 789 790 println("Resolved location of main project to: %1$s", resolvedPath); 791 792 // check the main project exists 793 if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { 794 mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); 795 return; 796 } 797 798 // now get the target from the main project 799 ProjectProperties projectProp = ProjectProperties.load(resolvedPath, PropertyType.PROJECT); 800 if (projectProp == null) { 801 // legacy support for older file name. 802 projectProp = ProjectProperties.load(resolvedPath, PropertyType.LEGACY_DEFAULT); 803 if (projectProp == null) { 804 mLog.error(null, "No %1$s at: %2$s", PropertyType.PROJECT.getFilename(), 805 resolvedPath); 806 return; 807 } 808 } 809 810 String targetHash = projectProp.getProperty(ProjectProperties.PROPERTY_TARGET); 811 if (targetHash == null) { 812 mLog.error(null, "%1$s in the main project has no target property.", 813 PropertyType.PROJECT.getFilename()); 814 return; 815 } 816 817 IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash); 818 if (target == null) { 819 mLog.error(null, "Main project target %1$s is not a valid target.", targetHash); 820 return; 821 } 822 823 // update test-project does not support the --name parameter, therefore the project 824 // name should generally not be passed to updateProject(). 825 // However if build.xml does not exist then updateProject() will recreate it. In this 826 // case we will need the project name. 827 // To do this, we look for the parent project name and add "test" to it. 828 // If the main project does not have a project name (yet), then the default behavior 829 // will be used (look for activity and then folder name) 830 String projectName = null; 831 XPathFactory factory = XPathFactory.newInstance(); 832 XPath xpath = factory.newXPath(); 833 834 File testBuildXml = new File(folderPath, SdkConstants.FN_BUILD_XML); 835 if (testBuildXml.isFile() == false) { 836 File mainBuildXml = new File(resolvedPath, SdkConstants.FN_BUILD_XML); 837 if (mainBuildXml.isFile()) { 838 try { 839 // get the name of the main project and add Test to it. 840 String mainProjectName = xpath.evaluate(XPATH_PROJECT_NAME, 841 new InputSource(new FileInputStream(mainBuildXml))); 842 projectName = mainProjectName + "Test"; 843 } catch (XPathExpressionException e) { 844 // it's ok, updateProject() will figure out a name automatically. 845 // We do log the error though as the build.xml file may be broken. 846 mLog.warning("Failed to parse %1$s.\n" + 847 "File may not be valid. Consider running 'android update project' on the main project.", 848 mainBuildXml.getPath()); 849 } catch (FileNotFoundException e) { 850 // should not happen since we check first. 851 } 852 } 853 } 854 855 // now update the project as if it's a normal project 856 if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) { 857 // error message has already been displayed. 858 return; 859 } 860 861 // add the test project specific properties. 862 // At this point, we know build.prop has been renamed ant.prop 863 ProjectProperties antProps = ProjectProperties.load(folderPath, PropertyType.ANT); 864 ProjectPropertiesWorkingCopy antWorkingCopy; 865 if (antProps == null) { 866 antWorkingCopy = ProjectProperties.create(folderPath, PropertyType.ANT); 867 } else { 868 antWorkingCopy = antProps.makeWorkingCopy(); 869 } 870 871 // set or replace the path to the main project 872 antWorkingCopy.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject); 873 try { 874 antWorkingCopy.save(); 875 println("Updated %1$s", PropertyType.ANT.getFilename()); 876 } catch (Exception e) { 877 mLog.error(e, "Failed to write %1$s file in '%2$s'", 878 PropertyType.ANT.getFilename(), 879 folderPath); 880 return; 881 } 882 } 883 884 /** 885 * Checks whether the give <var>folderPath</var> is a valid project folder, and returns 886 * a {@link FileWrapper} to the required file. 887 * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it. 888 * <p/>Any error are output using {@link #mLog}. 889 * @param folderPath the folder to check 890 * @param requiredFilename the file name of the file that's required. 891 * @return a {@link FileWrapper} to the AndroidManifest.xml file, or null otherwise. 892 */ checkProjectFolder(String folderPath, String requiredFilename)893 private FileWrapper checkProjectFolder(String folderPath, String requiredFilename) { 894 // project folder must exist and be a directory, since this is an update 895 FolderWrapper projectFolder = new FolderWrapper(folderPath); 896 if (!projectFolder.isDirectory()) { 897 mLog.error(null, "Project folder '%1$s' is not a valid directory.", 898 projectFolder); 899 return null; 900 } 901 902 // Check AndroidManifest.xml is present 903 FileWrapper requireFile = new FileWrapper(projectFolder, requiredFilename); 904 if (!requireFile.isFile()) { 905 mLog.error(null, 906 "%1$s is not a valid project (%2$s not found).", 907 folderPath, requiredFilename); 908 return null; 909 } 910 911 return requireFile; 912 } 913 914 /** 915 * Looks for a given regex in a file and returns the matcher if any line of the input file 916 * contains the requested regexp. 917 * 918 * @param file the file to search. 919 * @param regexp the regexp to search for. 920 * 921 * @return a Matcher or null if the regexp is not found. 922 */ checkFileContainsRegexp(File file, String regexp)923 private Matcher checkFileContainsRegexp(File file, String regexp) { 924 Pattern p = Pattern.compile(regexp); 925 926 try { 927 BufferedReader in = new BufferedReader(new FileReader(file)); 928 String line; 929 930 while ((line = in.readLine()) != null) { 931 Matcher m = p.matcher(line); 932 if (m.find()) { 933 return m; 934 } 935 } 936 937 in.close(); 938 } catch (Exception e) { 939 // ignore 940 } 941 942 return null; 943 } 944 945 /** 946 * Extracts a "full" package & activity name from an AndroidManifest.xml. 947 * <p/> 948 * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}. 949 * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}. 950 * When no activity is found, this key is not created. 951 * 952 * @param manifestFile The AndroidManifest.xml file 953 * @param outKeywords Place where to put the out parameters: package and activity names. 954 * @return True if the package/activity was parsed and updated in the keyword dictionary. 955 */ extractPackageFromManifest(File manifestFile, Map<String, String> outKeywords)956 private boolean extractPackageFromManifest(File manifestFile, 957 Map<String, String> outKeywords) { 958 try { 959 XPath xpath = AndroidXPathFactory.newXPath(); 960 961 InputSource source = new InputSource(new FileReader(manifestFile)); 962 String packageName = xpath.evaluate("/manifest/@package", source); 963 964 source = new InputSource(new FileReader(manifestFile)); 965 966 // Select the "android:name" attribute of all <activity> nodes but only if they 967 // contain a sub-node <intent-filter><action> with an "android:name" attribute which 968 // is 'android.intent.action.MAIN' and an <intent-filter><category> with an 969 // "android:name" attribute which is 'android.intent.category.LAUNCHER' 970 String expression = String.format("/manifest/application/activity" + 971 "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " + 972 "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" + 973 "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX); 974 975 NodeList activityNames = (NodeList) xpath.evaluate(expression, source, 976 XPathConstants.NODESET); 977 978 // If we get here, both XPath expressions were valid so we're most likely dealing 979 // with an actual AndroidManifest.xml file. The nodes may not have the requested 980 // attributes though, if which case we should warn. 981 982 if (packageName == null || packageName.length() == 0) { 983 mLog.error(null, 984 "Missing <manifest package=\"...\"> in '%1$s'", 985 manifestFile.getName()); 986 return false; 987 } 988 989 // Get the first activity that matched earlier. If there is no activity, 990 // activityName is set to an empty string and the generated "combined" name 991 // will be in the form "package." (with a dot at the end). 992 String activityName = ""; 993 if (activityNames.getLength() > 0) { 994 activityName = activityNames.item(0).getNodeValue(); 995 } 996 997 if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) { 998 println("WARNING: There is more than one activity defined in '%1$s'.\n" + 999 "Only the first one will be used. If this is not appropriate, you need\n" + 1000 "to specify one of these values manually instead:", 1001 manifestFile.getName()); 1002 1003 for (int i = 0; i < activityNames.getLength(); i++) { 1004 String name = activityNames.item(i).getNodeValue(); 1005 name = combinePackageActivityNames(packageName, name); 1006 println("- %1$s", name); 1007 } 1008 } 1009 1010 if (activityName.length() == 0) { 1011 mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" + 1012 "No activity will be generated.", 1013 AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName()); 1014 } else { 1015 outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName); 1016 } 1017 1018 outKeywords.put(PH_PACKAGE, packageName); 1019 return true; 1020 1021 } catch (IOException e) { 1022 mLog.error(e, "Failed to read %1$s", manifestFile.getName()); 1023 } catch (XPathExpressionException e) { 1024 Throwable t = e.getCause(); 1025 mLog.error(t == null ? e : t, 1026 "Failed to parse %1$s", 1027 manifestFile.getName()); 1028 } 1029 1030 return false; 1031 } 1032 combinePackageActivityNames(String packageName, String activityName)1033 private String combinePackageActivityNames(String packageName, String activityName) { 1034 // Activity Name can have 3 forms: 1035 // - ".Name" means this is a class name in the given package name. 1036 // The full FQCN is thus packageName + ".Name" 1037 // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name" 1038 // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is. 1039 // To be valid, the package name should have at least two components. This is checked 1040 // later during the creation of the build.xml file, so we just need to detect there's 1041 // a dot but not at pos==0. 1042 1043 int pos = activityName.indexOf('.'); 1044 if (pos == 0) { 1045 return packageName + activityName; 1046 } else if (pos > 0) { 1047 return activityName; 1048 } else { 1049 return packageName + "." + activityName; 1050 } 1051 } 1052 1053 /** 1054 * Installs a new file that is based on a template file provided by a given target. 1055 * Each match of each key from the place-holder map in the template will be replaced with its 1056 * corresponding value in the created file. 1057 * 1058 * @param templateName the name of to the template file 1059 * @param destFile the path to the destination file, relative to the project 1060 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 1061 * @param target the Target of the project that will be providing the template. 1062 * @throws ProjectCreateException 1063 */ installTargetTemplate(String templateName, File destFile, Map<String, String> placeholderMap, IAndroidTarget target)1064 private void installTargetTemplate(String templateName, File destFile, 1065 Map<String, String> placeholderMap, IAndroidTarget target) 1066 throws ProjectCreateException { 1067 // query the target for its template directory 1068 String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); 1069 final String sourcePath = templateFolder + File.separator + templateName; 1070 1071 installFullPathTemplate(sourcePath, destFile, placeholderMap); 1072 } 1073 1074 /** 1075 * Installs a new file that is based on a template file provided by the tools folder. 1076 * Each match of each key from the place-holder map in the template will be replaced with its 1077 * corresponding value in the created file. 1078 * 1079 * @param templateName the name of to the template file 1080 * @param destFile the path to the destination file, relative to the project 1081 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 1082 * @throws ProjectCreateException 1083 */ installTemplate(String templateName, File destFile, Map<String, String> placeholderMap)1084 private void installTemplate(String templateName, File destFile, 1085 Map<String, String> placeholderMap) 1086 throws ProjectCreateException { 1087 // query the target for its template directory 1088 String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; 1089 final String sourcePath = templateFolder + File.separator + templateName; 1090 1091 installFullPathTemplate(sourcePath, destFile, placeholderMap); 1092 } 1093 1094 /** 1095 * Installs a new file that is based on a template. 1096 * Each match of each key from the place-holder map in the template will be replaced with its 1097 * corresponding value in the created file. 1098 * 1099 * @param sourcePath the full path to the source template file 1100 * @param destFile the destination file 1101 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 1102 * @throws ProjectCreateException 1103 */ installFullPathTemplate(String sourcePath, File destFile, Map<String, String> placeholderMap)1104 private void installFullPathTemplate(String sourcePath, File destFile, 1105 Map<String, String> placeholderMap) throws ProjectCreateException { 1106 1107 boolean existed = destFile.exists(); 1108 1109 try { 1110 BufferedWriter out = new BufferedWriter(new FileWriter(destFile)); 1111 BufferedReader in = new BufferedReader(new FileReader(sourcePath)); 1112 String line; 1113 1114 while ((line = in.readLine()) != null) { 1115 if (placeholderMap != null) { 1116 for (Map.Entry<String, String> entry : placeholderMap.entrySet()) { 1117 line = line.replace(entry.getKey(), entry.getValue()); 1118 } 1119 } 1120 1121 out.write(line); 1122 out.newLine(); 1123 } 1124 1125 out.close(); 1126 in.close(); 1127 } catch (Exception e) { 1128 throw new ProjectCreateException(e, "Could not access %1$s: %2$s", 1129 destFile, e.getMessage()); 1130 } 1131 1132 println("%1$s file %2$s", 1133 existed ? "Updated" : "Added", 1134 destFile); 1135 } 1136 1137 /** 1138 * Installs the project icons. 1139 * @param resourceFolder the resource folder 1140 * @param target the target of the project. 1141 * @return true if any icon was installed. 1142 */ installIcons(File resourceFolder, IAndroidTarget target)1143 private boolean installIcons(File resourceFolder, IAndroidTarget target) 1144 throws ProjectCreateException { 1145 // query the target for its template directory 1146 String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); 1147 1148 boolean installedIcon = false; 1149 1150 installedIcon |= installIcon(templateFolder, "ic_launcher_hdpi.png", resourceFolder, 1151 "drawable-hdpi"); 1152 installedIcon |= installIcon(templateFolder, "ic_launcher_mdpi.png", resourceFolder, 1153 "drawable-mdpi"); 1154 installedIcon |= installIcon(templateFolder, "ic_launcher_ldpi.png", resourceFolder, 1155 "drawable-ldpi"); 1156 1157 return installedIcon; 1158 } 1159 1160 /** 1161 * Installs an Icon in the project. 1162 * @return true if the icon was installed. 1163 */ installIcon(String templateFolder, String iconName, File resourceFolder, String folderName)1164 private boolean installIcon(String templateFolder, String iconName, File resourceFolder, 1165 String folderName) throws ProjectCreateException { 1166 File icon = new File(templateFolder, iconName); 1167 if (icon.exists()) { 1168 File drawable = createDirs(resourceFolder, folderName); 1169 installBinaryFile(icon, new File(drawable, "ic_launcher.png")); 1170 return true; 1171 } 1172 1173 return false; 1174 } 1175 1176 /** 1177 * Installs a binary file 1178 * @param source the source file to copy 1179 * @param destination the destination file to write 1180 * @throws ProjectCreateException 1181 */ installBinaryFile(File source, File destination)1182 private void installBinaryFile(File source, File destination) throws ProjectCreateException { 1183 byte[] buffer = new byte[8192]; 1184 1185 FileInputStream fis = null; 1186 FileOutputStream fos = null; 1187 try { 1188 fis = new FileInputStream(source); 1189 fos = new FileOutputStream(destination); 1190 1191 int read; 1192 while ((read = fis.read(buffer)) != -1) { 1193 fos.write(buffer, 0, read); 1194 } 1195 1196 } catch (FileNotFoundException e) { 1197 // shouldn't happen since we check before. 1198 } catch (IOException e) { 1199 throw new ProjectCreateException(e, "Failed to read binary file: %1$s", 1200 source.getAbsolutePath()); 1201 } finally { 1202 if (fis != null) { 1203 try { 1204 fis.close(); 1205 } catch (IOException e) { 1206 // ignore 1207 } 1208 } 1209 if (fos != null) { 1210 try { 1211 fos.close(); 1212 } catch (IOException e) { 1213 // ignore 1214 } 1215 } 1216 } 1217 1218 } 1219 1220 /** 1221 * Prints a message unless silence is enabled. 1222 * <p/> 1223 * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from 1224 * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}. 1225 * 1226 * @param format Format for String.format 1227 * @param args Arguments for String.format 1228 */ println(String format, Object... args)1229 private void println(String format, Object... args) { 1230 if (mLevel != OutputLevel.SILENT) { 1231 if (!format.endsWith("\n")) { 1232 format += "\n"; 1233 } 1234 mLog.printf(format, args); 1235 } 1236 } 1237 1238 /** 1239 * Creates a new folder, along with any parent folders that do not exists. 1240 * 1241 * @param parent the parent folder 1242 * @param name the name of the directory to create. 1243 * @throws ProjectCreateException 1244 */ createDirs(File parent, String name)1245 private File createDirs(File parent, String name) throws ProjectCreateException { 1246 final File newFolder = new File(parent, name); 1247 boolean existedBefore = true; 1248 1249 if (!newFolder.exists()) { 1250 if (!newFolder.mkdirs()) { 1251 throw new ProjectCreateException("Could not create directory: %1$s", newFolder); 1252 } 1253 existedBefore = false; 1254 } 1255 1256 if (newFolder.isDirectory()) { 1257 if (!newFolder.canWrite()) { 1258 throw new ProjectCreateException("Path is not writable: %1$s", newFolder); 1259 } 1260 } else { 1261 throw new ProjectCreateException("Path is not a directory: %1$s", newFolder); 1262 } 1263 1264 if (!existedBefore) { 1265 try { 1266 println("Created directory %1$s", newFolder.getCanonicalPath()); 1267 } catch (IOException e) { 1268 throw new ProjectCreateException( 1269 "Could not determine canonical path of created directory", e); 1270 } 1271 } 1272 1273 return newFolder; 1274 } 1275 1276 /** 1277 * Strips the string of beginning and trailing characters (multiple 1278 * characters will be stripped, example stripString("..test...", '.') 1279 * results in "test"; 1280 * 1281 * @param s the string to strip 1282 * @param strip the character to strip from beginning and end 1283 * @return the stripped string or the empty string if everything is stripped. 1284 */ stripString(String s, char strip)1285 private static String stripString(String s, char strip) { 1286 final int sLen = s.length(); 1287 int newStart = 0, newEnd = sLen - 1; 1288 1289 while (newStart < sLen && s.charAt(newStart) == strip) { 1290 newStart++; 1291 } 1292 while (newEnd >= 0 && s.charAt(newEnd) == strip) { 1293 newEnd--; 1294 } 1295 1296 /* 1297 * newEnd contains a char we want, and substring takes end as being 1298 * exclusive 1299 */ 1300 newEnd++; 1301 1302 if (newStart >= sLen || newEnd < 0) { 1303 return ""; 1304 } 1305 1306 return s.substring(newStart, newEnd); 1307 } 1308 } 1309