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