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") 130 ProjectCreateException(String message) { 131 super(message); 132 } 133 134 ProjectCreateException(Throwable t, String format, Object... args) { 135 super(format != null ? String.format(format, args) : format, t); 136 } 137 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 */ 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 */ 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_PROJECT_PROGUARD_FILE, 373 new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), 374 null /*keywords*/); 375 } catch (Exception e) { 376 mLog.error(e, null); 377 } 378 } 379 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") 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 if (props == null) { 539 // This should not happen yet SDK bug 20535 says it can, not sure how. 540 break; 541 } 542 String ref = props.getProperty(propName); 543 if (ref == null) { 544 break; 545 } else { 546 index++; 547 } 548 } 549 550 String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); 551 propsWC.setProperty(propName, libraryPath); 552 saveProjectProps = true; 553 } 554 555 // save the default props if needed. 556 if (saveProjectProps) { 557 try { 558 assert propsWC != null; 559 propsWC.save(); 560 if (writeProjectProp) { 561 println("Updated and renamed %1$s to %2$s", 562 PropertyType.LEGACY_DEFAULT.getFilename(), 563 PropertyType.PROJECT.getFilename()); 564 } else { 565 println("Updated %1$s", PropertyType.PROJECT.getFilename()); 566 } 567 } catch (Exception e) { 568 mLog.error(e, "Failed to write %1$s file in '%2$s'", 569 PropertyType.PROJECT.getFilename(), 570 folderPath); 571 return false; 572 } 573 574 if (writeProjectProp) { 575 // need to delete the default prop file. 576 ProjectProperties.delete(projectFolder, PropertyType.LEGACY_DEFAULT); 577 } 578 } 579 580 // Refresh/create "sdk" in local.properties 581 // because the file may already exists and contain other values (like apk config), 582 // we first try to load it. 583 props = ProjectProperties.load(projectFolder, PropertyType.LOCAL); 584 if (props == null) { 585 propsWC = ProjectProperties.create(projectFolder, PropertyType.LOCAL); 586 } else { 587 propsWC = props.makeWorkingCopy(); 588 } 589 590 // set or replace the sdk location. 591 propsWC.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); 592 try { 593 propsWC.save(); 594 println("Updated %1$s", PropertyType.LOCAL.getFilename()); 595 } catch (Exception e) { 596 mLog.error(e, "Failed to write %1$s file in '%2$s'", 597 PropertyType.LOCAL.getFilename(), 598 folderPath); 599 return false; 600 } 601 602 // legacy: check if build.properties must be renamed to ant.properties. 603 props = ProjectProperties.load(projectFolder, PropertyType.ANT); 604 if (props == null) { 605 props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_BUILD); 606 if (props != null) { 607 try { 608 // get a working copy with the new property type 609 propsWC = props.makeWorkingCopy(PropertyType.ANT); 610 propsWC.save(); 611 612 // delete the old file 613 ProjectProperties.delete(projectFolder, PropertyType.LEGACY_BUILD); 614 615 println("Renamed %1$s to %2$s", 616 PropertyType.LEGACY_BUILD.getFilename(), 617 PropertyType.ANT.getFilename()); 618 } catch (Exception e) { 619 mLog.error(e, "Failed to write %1$s file in '%2$s'", 620 PropertyType.ANT.getFilename(), 621 folderPath); 622 return false; 623 } 624 } 625 } 626 627 // Build.xml: create if not present or no <androidinit/> in it 628 File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML); 629 boolean needsBuildXml = projectName != null || !buildXml.exists(); 630 631 // if it seems there's no need for a new build.xml, look for inside the file 632 // to try to detect old ones that may need updating. 633 if (!needsBuildXml) { 634 // we are looking for version-tag: followed by either an integer or "custom". 635 if (checkFileContainsRegexp(buildXml, "version-tag:\\s*custom") != null) { //$NON-NLS-1$ 636 println("%1$s: Found version-tag: custom. File will not be updated.", 637 SdkConstants.FN_BUILD_XML); 638 } else { 639 Matcher m = checkFileContainsRegexp(buildXml, "version-tag:\\s*(\\d+)"); //$NON-NLS-1$ 640 if (m == null) { 641 println("----------\n" + 642 "%1$s: Failed to find version-tag string. File must be updated.\n" + 643 "In order to not erase potential customizations, the file will not be automatically regenerated.\n" + 644 "If no changes have been made to the file, delete it manually and run the command again.\n" + 645 "If you have made customizations to the build process, the file must be manually updated.\n" + 646 "It is recommended to:\n" + 647 "\t* Copy current file to a safe location.\n" + 648 "\t* Delete original file.\n" + 649 "\t* Run command again to generate a new file.\n" + 650 "\t* Port customizations to the new file, by looking at the new rules file\n" + 651 "\t located at <SDK>/tools/ant/build.xml\n" + 652 "\t* Update file to contain\n" + 653 "\t version-tag: custom\n" + 654 "\t to prevent file from being rewritten automatically by the SDK tools.\n" + 655 "----------\n", 656 SdkConstants.FN_BUILD_XML); 657 } else { 658 String versionStr = m.group(1); 659 if (versionStr != null) { 660 // can't fail due to regexp above. 661 int version = Integer.parseInt(versionStr); 662 if (version < MIN_BUILD_VERSION_TAG) { 663 println("%1$s: Found version-tag: %2$d. Expected version-tag: %3$d: file must be updated.", 664 SdkConstants.FN_BUILD_XML, version, MIN_BUILD_VERSION_TAG); 665 needsBuildXml = true; 666 } 667 } 668 } 669 } 670 } 671 672 if (needsBuildXml) { 673 // create the map for place-holders of values to replace in the templates 674 final HashMap<String, String> keywords = new HashMap<String, String>(); 675 676 // put the current version-tag value 677 keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); 678 679 // if there was no project name on the command line, figure one out. 680 if (projectName == null) { 681 // otherwise, take it from the existing build.xml if it exists already. 682 if (buildXml.exists()) { 683 try { 684 XPathFactory factory = XPathFactory.newInstance(); 685 XPath xpath = factory.newXPath(); 686 687 projectName = xpath.evaluate(XPATH_PROJECT_NAME, 688 new InputSource(new FileInputStream(buildXml))); 689 } catch (XPathExpressionException e) { 690 // this is ok since we're going to recreate the file. 691 mLog.error(e, "Unable to find existing project name from %1$s", 692 SdkConstants.FN_BUILD_XML); 693 } catch (FileNotFoundException e) { 694 // can't happen since we check above. 695 } 696 } 697 698 // if the project is still null, then we find another way. 699 if (projectName == null) { 700 extractPackageFromManifest(androidManifest, keywords); 701 if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) { 702 String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME); 703 // keep only the last segment if applicable 704 int pos = activity.lastIndexOf('.'); 705 if (pos != -1) { 706 activity = activity.substring(pos + 1); 707 } 708 709 // Use the activity as project name 710 projectName = activity; 711 712 println("No project name specified, using Activity name '%1$s'.\n" + 713 "If you wish to change it, edit the first line of %2$s.", 714 activity, SdkConstants.FN_BUILD_XML); 715 } else { 716 // We need a project name. Just pick up the basename of the project 717 // directory. 718 File projectCanonicalFolder = projectFolder; 719 try { 720 projectCanonicalFolder = projectCanonicalFolder.getCanonicalFile(); 721 } catch (IOException e) { 722 // ignore, keep going 723 } 724 725 // Use the folder name as project name 726 projectName = projectCanonicalFolder.getName(); 727 728 println("No project name specified, using project folder name '%1$s'.\n" + 729 "If you wish to change it, edit the first line of %2$s.", 730 projectName, SdkConstants.FN_BUILD_XML); 731 } 732 } 733 } 734 735 // put the project name in the map for replacement during the template installation. 736 keywords.put(PH_PROJECT_NAME, projectName); 737 738 if (mLevel == OutputLevel.VERBOSE) { 739 println("Regenerating %1$s with project name %2$s", 740 SdkConstants.FN_BUILD_XML, 741 keywords.get(PH_PROJECT_NAME)); 742 } 743 744 try { 745 installTemplate("build.template", buildXml, keywords); 746 } catch (ProjectCreateException e) { 747 mLog.error(e, null); 748 return false; 749 } 750 } 751 752 if (hasProguard == false) { 753 try { 754 installTemplate(SdkConstants.FN_PROJECT_PROGUARD_FILE, 755 // Write ProGuard config files with the extension .pro which 756 // is what is used in the ProGuard documentation and samples 757 new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), 758 null /*placeholderMap*/); 759 } catch (ProjectCreateException e) { 760 mLog.error(e, null); 761 return false; 762 } 763 } 764 765 return true; 766 } 767 768 /** 769 * Updates a test project with a new path to the main (tested) project. 770 * @param folderPath the path of the test project. 771 * @param pathToMainProject the path to the main project, relative to the test project. 772 */ 773 @SuppressWarnings("deprecation") 774 public void updateTestProject(final String folderPath, final String pathToMainProject, 775 final SdkManager sdkManager) { 776 // since this is an update, check the folder does point to a project 777 if (checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { 778 return; 779 } 780 781 // check the path to the main project is valid. 782 File mainProject = new File(pathToMainProject); 783 String resolvedPath; 784 if (mainProject.isAbsolute() == false) { 785 mainProject = new File(folderPath, pathToMainProject); 786 try { 787 resolvedPath = mainProject.getCanonicalPath(); 788 } catch (IOException e) { 789 mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject); 790 return; 791 } 792 } else { 793 resolvedPath = mainProject.getAbsolutePath(); 794 } 795 796 println("Resolved location of main project to: %1$s", resolvedPath); 797 798 // check the main project exists 799 if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { 800 mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); 801 return; 802 } 803 804 // now get the target from the main project 805 ProjectProperties projectProp = ProjectProperties.load(resolvedPath, PropertyType.PROJECT); 806 if (projectProp == null) { 807 // legacy support for older file name. 808 projectProp = ProjectProperties.load(resolvedPath, PropertyType.LEGACY_DEFAULT); 809 if (projectProp == null) { 810 mLog.error(null, "No %1$s at: %2$s", PropertyType.PROJECT.getFilename(), 811 resolvedPath); 812 return; 813 } 814 } 815 816 String targetHash = projectProp.getProperty(ProjectProperties.PROPERTY_TARGET); 817 if (targetHash == null) { 818 mLog.error(null, "%1$s in the main project has no target property.", 819 PropertyType.PROJECT.getFilename()); 820 return; 821 } 822 823 IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash); 824 if (target == null) { 825 mLog.error(null, "Main project target %1$s is not a valid target.", targetHash); 826 return; 827 } 828 829 // update test-project does not support the --name parameter, therefore the project 830 // name should generally not be passed to updateProject(). 831 // However if build.xml does not exist then updateProject() will recreate it. In this 832 // case we will need the project name. 833 // To do this, we look for the parent project name and add "test" to it. 834 // If the main project does not have a project name (yet), then the default behavior 835 // will be used (look for activity and then folder name) 836 String projectName = null; 837 XPathFactory factory = XPathFactory.newInstance(); 838 XPath xpath = factory.newXPath(); 839 840 File testBuildXml = new File(folderPath, SdkConstants.FN_BUILD_XML); 841 if (testBuildXml.isFile() == false) { 842 File mainBuildXml = new File(resolvedPath, SdkConstants.FN_BUILD_XML); 843 if (mainBuildXml.isFile()) { 844 try { 845 // get the name of the main project and add Test to it. 846 String mainProjectName = xpath.evaluate(XPATH_PROJECT_NAME, 847 new InputSource(new FileInputStream(mainBuildXml))); 848 projectName = mainProjectName + "Test"; 849 } catch (XPathExpressionException e) { 850 // it's ok, updateProject() will figure out a name automatically. 851 // We do log the error though as the build.xml file may be broken. 852 mLog.warning("Failed to parse %1$s.\n" + 853 "File may not be valid. Consider running 'android update project' on the main project.", 854 mainBuildXml.getPath()); 855 } catch (FileNotFoundException e) { 856 // should not happen since we check first. 857 } 858 } 859 } 860 861 // now update the project as if it's a normal project 862 if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) { 863 // error message has already been displayed. 864 return; 865 } 866 867 // add the test project specific properties. 868 // At this point, we know build.prop has been renamed ant.prop 869 ProjectProperties antProps = ProjectProperties.load(folderPath, PropertyType.ANT); 870 ProjectPropertiesWorkingCopy antWorkingCopy; 871 if (antProps == null) { 872 antWorkingCopy = ProjectProperties.create(folderPath, PropertyType.ANT); 873 } else { 874 antWorkingCopy = antProps.makeWorkingCopy(); 875 } 876 877 // set or replace the path to the main project 878 antWorkingCopy.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject); 879 try { 880 antWorkingCopy.save(); 881 println("Updated %1$s", PropertyType.ANT.getFilename()); 882 } catch (Exception e) { 883 mLog.error(e, "Failed to write %1$s file in '%2$s'", 884 PropertyType.ANT.getFilename(), 885 folderPath); 886 return; 887 } 888 } 889 890 /** 891 * Checks whether the give <var>folderPath</var> is a valid project folder, and returns 892 * a {@link FileWrapper} to the required file. 893 * <p/>This checks that the folder exists and contains an AndroidManifest.xml file in it. 894 * <p/>Any error are output using {@link #mLog}. 895 * @param folderPath the folder to check 896 * @param requiredFilename the file name of the file that's required. 897 * @return a {@link FileWrapper} to the AndroidManifest.xml file, or null otherwise. 898 */ 899 private FileWrapper checkProjectFolder(String folderPath, String requiredFilename) { 900 // project folder must exist and be a directory, since this is an update 901 FolderWrapper projectFolder = new FolderWrapper(folderPath); 902 if (!projectFolder.isDirectory()) { 903 mLog.error(null, "Project folder '%1$s' is not a valid directory.", 904 projectFolder); 905 return null; 906 } 907 908 // Check AndroidManifest.xml is present 909 FileWrapper requireFile = new FileWrapper(projectFolder, requiredFilename); 910 if (!requireFile.isFile()) { 911 mLog.error(null, 912 "%1$s is not a valid project (%2$s not found).", 913 folderPath, requiredFilename); 914 return null; 915 } 916 917 return requireFile; 918 } 919 920 /** 921 * Looks for a given regex in a file and returns the matcher if any line of the input file 922 * contains the requested regexp. 923 * 924 * @param file the file to search. 925 * @param regexp the regexp to search for. 926 * 927 * @return a Matcher or null if the regexp is not found. 928 */ 929 private Matcher checkFileContainsRegexp(File file, String regexp) { 930 Pattern p = Pattern.compile(regexp); 931 932 try { 933 BufferedReader in = new BufferedReader(new FileReader(file)); 934 String line; 935 936 while ((line = in.readLine()) != null) { 937 Matcher m = p.matcher(line); 938 if (m.find()) { 939 return m; 940 } 941 } 942 943 in.close(); 944 } catch (Exception e) { 945 // ignore 946 } 947 948 return null; 949 } 950 951 /** 952 * Extracts a "full" package & activity name from an AndroidManifest.xml. 953 * <p/> 954 * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}. 955 * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}. 956 * When no activity is found, this key is not created. 957 * 958 * @param manifestFile The AndroidManifest.xml file 959 * @param outKeywords Place where to put the out parameters: package and activity names. 960 * @return True if the package/activity was parsed and updated in the keyword dictionary. 961 */ 962 private boolean extractPackageFromManifest(File manifestFile, 963 Map<String, String> outKeywords) { 964 try { 965 XPath xpath = AndroidXPathFactory.newXPath(); 966 967 InputSource source = new InputSource(new FileReader(manifestFile)); 968 String packageName = xpath.evaluate("/manifest/@package", source); 969 970 source = new InputSource(new FileReader(manifestFile)); 971 972 // Select the "android:name" attribute of all <activity> nodes but only if they 973 // contain a sub-node <intent-filter><action> with an "android:name" attribute which 974 // is 'android.intent.action.MAIN' and an <intent-filter><category> with an 975 // "android:name" attribute which is 'android.intent.category.LAUNCHER' 976 String expression = String.format("/manifest/application/activity" + 977 "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " + 978 "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" + 979 "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX); 980 981 NodeList activityNames = (NodeList) xpath.evaluate(expression, source, 982 XPathConstants.NODESET); 983 984 // If we get here, both XPath expressions were valid so we're most likely dealing 985 // with an actual AndroidManifest.xml file. The nodes may not have the requested 986 // attributes though, if which case we should warn. 987 988 if (packageName == null || packageName.length() == 0) { 989 mLog.error(null, 990 "Missing <manifest package=\"...\"> in '%1$s'", 991 manifestFile.getName()); 992 return false; 993 } 994 995 // Get the first activity that matched earlier. If there is no activity, 996 // activityName is set to an empty string and the generated "combined" name 997 // will be in the form "package." (with a dot at the end). 998 String activityName = ""; 999 if (activityNames.getLength() > 0) { 1000 activityName = activityNames.item(0).getNodeValue(); 1001 } 1002 1003 if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) { 1004 println("WARNING: There is more than one activity defined in '%1$s'.\n" + 1005 "Only the first one will be used. If this is not appropriate, you need\n" + 1006 "to specify one of these values manually instead:", 1007 manifestFile.getName()); 1008 1009 for (int i = 0; i < activityNames.getLength(); i++) { 1010 String name = activityNames.item(i).getNodeValue(); 1011 name = combinePackageActivityNames(packageName, name); 1012 println("- %1$s", name); 1013 } 1014 } 1015 1016 if (activityName.length() == 0) { 1017 mLog.warning("Missing <activity %1$s:name=\"...\"> in '%2$s'.\n" + 1018 "No activity will be generated.", 1019 AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName()); 1020 } else { 1021 outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName); 1022 } 1023 1024 outKeywords.put(PH_PACKAGE, packageName); 1025 return true; 1026 1027 } catch (IOException e) { 1028 mLog.error(e, "Failed to read %1$s", manifestFile.getName()); 1029 } catch (XPathExpressionException e) { 1030 Throwable t = e.getCause(); 1031 mLog.error(t == null ? e : t, 1032 "Failed to parse %1$s", 1033 manifestFile.getName()); 1034 } 1035 1036 return false; 1037 } 1038 1039 private String combinePackageActivityNames(String packageName, String activityName) { 1040 // Activity Name can have 3 forms: 1041 // - ".Name" means this is a class name in the given package name. 1042 // The full FQCN is thus packageName + ".Name" 1043 // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name" 1044 // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is. 1045 // To be valid, the package name should have at least two components. This is checked 1046 // later during the creation of the build.xml file, so we just need to detect there's 1047 // a dot but not at pos==0. 1048 1049 int pos = activityName.indexOf('.'); 1050 if (pos == 0) { 1051 return packageName + activityName; 1052 } else if (pos > 0) { 1053 return activityName; 1054 } else { 1055 return packageName + "." + activityName; 1056 } 1057 } 1058 1059 /** 1060 * Installs a new file that is based on a template file provided by a given target. 1061 * Each match of each key from the place-holder map in the template will be replaced with its 1062 * corresponding value in the created file. 1063 * 1064 * @param templateName the name of to the template file 1065 * @param destFile the path to the destination file, relative to the project 1066 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 1067 * @param target the Target of the project that will be providing the template. 1068 * @throws ProjectCreateException 1069 */ 1070 private void installTargetTemplate(String templateName, File destFile, 1071 Map<String, String> placeholderMap, IAndroidTarget target) 1072 throws ProjectCreateException { 1073 // query the target for its template directory 1074 String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); 1075 final String sourcePath = templateFolder + File.separator + templateName; 1076 1077 installFullPathTemplate(sourcePath, destFile, placeholderMap); 1078 } 1079 1080 /** 1081 * Installs a new file that is based on a template file provided by the tools folder. 1082 * Each match of each key from the place-holder map in the template will be replaced with its 1083 * corresponding value in the created file. 1084 * 1085 * @param templateName the name of to the template file 1086 * @param destFile the path to the destination file, relative to the project 1087 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 1088 * @throws ProjectCreateException 1089 */ 1090 private void installTemplate(String templateName, File destFile, 1091 Map<String, String> placeholderMap) 1092 throws ProjectCreateException { 1093 // query the target for its template directory 1094 String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; 1095 final String sourcePath = templateFolder + File.separator + templateName; 1096 1097 installFullPathTemplate(sourcePath, destFile, placeholderMap); 1098 } 1099 1100 /** 1101 * Installs a new file that is based on a template. 1102 * Each match of each key from the place-holder map in the template will be replaced with its 1103 * corresponding value in the created file. 1104 * 1105 * @param sourcePath the full path to the source template file 1106 * @param destFile the destination file 1107 * @param placeholderMap a map of (place-holder, value) to create the file from the template. 1108 * @throws ProjectCreateException 1109 */ 1110 private void installFullPathTemplate(String sourcePath, File destFile, 1111 Map<String, String> placeholderMap) throws ProjectCreateException { 1112 1113 boolean existed = destFile.exists(); 1114 1115 try { 1116 BufferedWriter out = new BufferedWriter(new FileWriter(destFile)); 1117 BufferedReader in = new BufferedReader(new FileReader(sourcePath)); 1118 String line; 1119 1120 while ((line = in.readLine()) != null) { 1121 if (placeholderMap != null) { 1122 for (Map.Entry<String, String> entry : placeholderMap.entrySet()) { 1123 line = line.replace(entry.getKey(), entry.getValue()); 1124 } 1125 } 1126 1127 out.write(line); 1128 out.newLine(); 1129 } 1130 1131 out.close(); 1132 in.close(); 1133 } catch (Exception e) { 1134 throw new ProjectCreateException(e, "Could not access %1$s: %2$s", 1135 destFile, e.getMessage()); 1136 } 1137 1138 println("%1$s file %2$s", 1139 existed ? "Updated" : "Added", 1140 destFile); 1141 } 1142 1143 /** 1144 * Installs the project icons. 1145 * @param resourceFolder the resource folder 1146 * @param target the target of the project. 1147 * @return true if any icon was installed. 1148 */ 1149 private boolean installIcons(File resourceFolder, IAndroidTarget target) 1150 throws ProjectCreateException { 1151 // query the target for its template directory 1152 String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); 1153 1154 boolean installedIcon = false; 1155 1156 installedIcon |= installIcon(templateFolder, "ic_launcher_xhdpi.png", resourceFolder, 1157 "drawable-xhdpi"); 1158 installedIcon |= installIcon(templateFolder, "ic_launcher_hdpi.png", resourceFolder, 1159 "drawable-hdpi"); 1160 installedIcon |= installIcon(templateFolder, "ic_launcher_mdpi.png", resourceFolder, 1161 "drawable-mdpi"); 1162 installedIcon |= installIcon(templateFolder, "ic_launcher_ldpi.png", resourceFolder, 1163 "drawable-ldpi"); 1164 1165 return installedIcon; 1166 } 1167 1168 /** 1169 * Installs an Icon in the project. 1170 * @return true if the icon was installed. 1171 */ 1172 private boolean installIcon(String templateFolder, String iconName, File resourceFolder, 1173 String folderName) throws ProjectCreateException { 1174 File icon = new File(templateFolder, iconName); 1175 if (icon.exists()) { 1176 File drawable = createDirs(resourceFolder, folderName); 1177 installBinaryFile(icon, new File(drawable, "ic_launcher.png")); 1178 return true; 1179 } 1180 1181 return false; 1182 } 1183 1184 /** 1185 * Installs a binary file 1186 * @param source the source file to copy 1187 * @param destination the destination file to write 1188 * @throws ProjectCreateException 1189 */ 1190 private void installBinaryFile(File source, File destination) throws ProjectCreateException { 1191 byte[] buffer = new byte[8192]; 1192 1193 FileInputStream fis = null; 1194 FileOutputStream fos = null; 1195 try { 1196 fis = new FileInputStream(source); 1197 fos = new FileOutputStream(destination); 1198 1199 int read; 1200 while ((read = fis.read(buffer)) != -1) { 1201 fos.write(buffer, 0, read); 1202 } 1203 1204 } catch (FileNotFoundException e) { 1205 // shouldn't happen since we check before. 1206 } catch (IOException e) { 1207 throw new ProjectCreateException(e, "Failed to read binary file: %1$s", 1208 source.getAbsolutePath()); 1209 } finally { 1210 if (fis != null) { 1211 try { 1212 fis.close(); 1213 } catch (IOException e) { 1214 // ignore 1215 } 1216 } 1217 if (fos != null) { 1218 try { 1219 fos.close(); 1220 } catch (IOException e) { 1221 // ignore 1222 } 1223 } 1224 } 1225 1226 } 1227 1228 /** 1229 * Prints a message unless silence is enabled. 1230 * <p/> 1231 * This is just a convenience wrapper around {@link ISdkLog#printf(String, Object...)} from 1232 * {@link #mLog} after testing if ouput level is {@link OutputLevel#VERBOSE}. 1233 * 1234 * @param format Format for String.format 1235 * @param args Arguments for String.format 1236 */ 1237 private void println(String format, Object... args) { 1238 if (mLevel != OutputLevel.SILENT) { 1239 if (!format.endsWith("\n")) { 1240 format += "\n"; 1241 } 1242 mLog.printf(format, args); 1243 } 1244 } 1245 1246 /** 1247 * Creates a new folder, along with any parent folders that do not exists. 1248 * 1249 * @param parent the parent folder 1250 * @param name the name of the directory to create. 1251 * @throws ProjectCreateException 1252 */ 1253 private File createDirs(File parent, String name) throws ProjectCreateException { 1254 final File newFolder = new File(parent, name); 1255 boolean existedBefore = true; 1256 1257 if (!newFolder.exists()) { 1258 if (!newFolder.mkdirs()) { 1259 throw new ProjectCreateException("Could not create directory: %1$s", newFolder); 1260 } 1261 existedBefore = false; 1262 } 1263 1264 if (newFolder.isDirectory()) { 1265 if (!newFolder.canWrite()) { 1266 throw new ProjectCreateException("Path is not writable: %1$s", newFolder); 1267 } 1268 } else { 1269 throw new ProjectCreateException("Path is not a directory: %1$s", newFolder); 1270 } 1271 1272 if (!existedBefore) { 1273 try { 1274 println("Created directory %1$s", newFolder.getCanonicalPath()); 1275 } catch (IOException e) { 1276 throw new ProjectCreateException( 1277 "Could not determine canonical path of created directory", e); 1278 } 1279 } 1280 1281 return newFolder; 1282 } 1283 1284 /** 1285 * Strips the string of beginning and trailing characters (multiple 1286 * characters will be stripped, example stripString("..test...", '.') 1287 * results in "test"; 1288 * 1289 * @param s the string to strip 1290 * @param strip the character to strip from beginning and end 1291 * @return the stripped string or the empty string if everything is stripped. 1292 */ 1293 private static String stripString(String s, char strip) { 1294 final int sLen = s.length(); 1295 int newStart = 0, newEnd = sLen - 1; 1296 1297 while (newStart < sLen && s.charAt(newStart) == strip) { 1298 newStart++; 1299 } 1300 while (newEnd >= 0 && s.charAt(newEnd) == strip) { 1301 newEnd--; 1302 } 1303 1304 /* 1305 * newEnd contains a char we want, and substring takes end as being 1306 * exclusive 1307 */ 1308 newEnd++; 1309 1310 if (newStart >= sLen || newEnd < 0) { 1311 return ""; 1312 } 1313 1314 return s.substring(newStart, newEnd); 1315 } 1316 } 1317