View Javadoc

1   /*
2    * Copyright 2011 SOFTEC sa. All rights reserved.
3    *
4    * This source code is licensed under the Creative Commons
5    * Attribution-NonCommercial-NoDerivs 3.0 Luxembourg
6    * License.
7    *
8    * To view a copy of this license, visit
9    * http://creativecommons.org/licenses/by-nc-nd/3.0/lu/
10   * or send a letter to Creative Commons, 171 Second Street,
11   * Suite 300, San Francisco, California, 94105, USA.
12   */
13  
14  package org.codehaus.mojo.javascript;
15  
16  import org.apache.maven.artifact.Artifact;
17  import org.apache.maven.artifact.factory.ArtifactFactory;
18  import org.apache.maven.artifact.repository.ArtifactRepository;
19  import org.apache.maven.artifact.resolver.ArtifactNotFoundException;
20  import org.apache.maven.artifact.resolver.ArtifactResolutionException;
21  import org.apache.maven.artifact.resolver.ArtifactResolver;
22  import org.apache.maven.plugin.AbstractMojo;
23  import org.apache.maven.plugin.MojoExecutionException;
24  import org.apache.maven.project.MavenProject;
25  import org.codehaus.mojo.javascript.titanium.TitaniumUtils;
26  import org.codehaus.plexus.util.IOUtil;
27  
28  import java.io.File;
29  import java.io.FileInputStream;
30  import java.io.FileOutputStream;
31  import java.io.IOException;
32  import java.text.MessageFormat;
33  import java.util.List;
34  import java.util.zip.ZipEntry;
35  import java.util.zip.ZipInputStream;
36  
37  /**
38   * Titanium package abstract base class;
39   */
40  public abstract class AbstractTitaniumPackageMojo extends AbstractMojo {
41      /**
42       * <p>Parameter designed to pick <code>-DandroidBuilder</code>
43       * in case there is no pom with an <code>&lt;titaniumSettings&gt;</code> configuration tag.</p>
44       * <p>Coresponds to {@link TitaniumSettings#androidBuilder}</p>
45       * @parameter expression="${androidBuilder}"
46       * @readonly
47       */
48      private File androidBuilderPath;
49  
50      /**
51       * <p>Parameter designed to pick <code>-DiosBuilder</code>
52       * in case there is no pom with an <code>&lt;titaniumSettings&gt;</code> configuration tag.</p>
53       * <p>Corresponds to {@link TitaniumSettings#iosBuilder}</p>
54       * @parameter expression="${iosBuilder}"
55       * @readonly
56       */
57      private File iosBuilderPath;
58  
59      /**
60       * <p>Parameter designed to pick <code>-DandroidSDK</code>
61       * in case there is no pom with <code>&lt;titaniumSettings&gt;</code> configuration tag.</p>
62       * <p>Corresponds to {@link TitaniumSettings#androidSDK}</p>
63       * @parameter expression="${androidSDK}"
64       * @readonly
65       */
66      private File androidSDKPath;
67  
68      /**
69       * <p>Parameter designed to pick <code>-Dsign.keystore</code>
70       * in case there is no pom with <code>&lt;titaniumSettings&gt;</code> configuration tag.</p>
71       * <p>Corresponds to {@link TitaniumSettings#keystore}</p>
72       * @parameter expression="${sign.keystore}"
73       * @readonly
74       */
75  
76      private String titaniumSettingsKeystore;
77  
78      /**
79       * <p>Parameter designed to pick <code>-Dsign.password</code>
80       * in case there is no pom with <code>&lt;titaniumSettings&gt;</code> configuration tag.</p>
81       * <p>Corresponds to {@link TitaniumSettings#keystorePassword}</p>
82       * @parameter expression="${sign.password}"
83       * @readonly
84       */
85      private String titaniumSettingsKeystorePassword;
86  
87      /**
88       * <p>Parameter designed to pick <code>-Dsign.alias</code>
89       * in case there is no pom with <code>&lt;titaniumSettings&gt;</code> configuration tag.</p>
90       * <p>Corresponds to {@link TitaniumSettings#keystoreAlias}</p>
91       * @parameter expression="${sign.alias}"
92       * @readonly
93       */
94      private String titaniumSettingsKeystoreAlias;
95  
96      /**
97       * <p>Parameter designed to pick <code>-DvirtualDevice.androidAPI</code>
98       * in case there is no pom with <code>&lt;virtualDevice&gt;</code> configuration tag.</p>
99       * <p>Corresponds to {@link VirtualDevice#androidAPI}</p>
100      * @parameter expression="${virtualDevice.androidAPI}"
101      * @readonly
102      */
103     private String virtualDeviceAndroidAPI;
104 
105     /**
106      * <p>Parameter designed to pick <code>-DvirtualDevice.iosVersion</code>
107      * in case there is no pom with <code>&lt;virtualDevice&gt;</code> configuration tag.</p>
108      * <p>Corresponds to {@link VirtualDevice#iosVersion}</p>
109      * @parameter expression="${virtualDevice.iosVersion}"
110      * @readonly
111      */
112     private String virtualDeviceIosVersion;
113 
114     /**
115      * <p>Parameter designed to pick <code>-DvirtualDevice.skin</code>
116      * in case there is no pom with <code>&lt;virtualDevice&gt;</code> configuration tag.</p>
117      * <p>Corresponds to {@link VirtualDevice#skin}</p>
118      * @parameter expression="${virtualDevice.skin}"
119      * @readonly
120      */
121     private String virtualDeviceSkin;
122 
123     /**
124      * <p>Parameter designed to pick <code>-DvirualDevice.family</code>
125      * in case there is no pom with <code>&lt;virtualDevice&gt;</code> configuration tag.</p>
126      * <p>Corresponds to {@link VirtualDevice#family}</p>
127      * @parameter expression="${virtualDevice.family}"
128      * @readonly
129      */
130     private String virtualDeviceFamily;
131 
132     /**
133      * <p>Parameter designed to pick <code>-DvirtualDevice.wait</code>
134      * in case there is no pom with <code>&lt;virtualDevice&gt;</code> configuration tag.</p>
135      * <p>Corresponds to {@link VirtualDevice#wait}</p>
136      * @parameter expression="${virtualDevice.wait}"
137      * @readonly
138      */
139     private Long virtualDeviceWait;
140 
141     /**
142      * <p>The platform for which the code should be packaged.</p>
143      * <p>Supported platforms are:</p>
144      * <dl>
145      *     <dt>android</dt>
146      *     <dd>Package for the android platform.</dd>
147      *     <dt>iphone</dt>
148      *     <dd>Package for the iPhone platform.</dd>
149      *     <dt>ipad</dt>
150      *     <dd>Package for the iPad platform.</dd>
151      *     <dt>universal</dt>
152      *     <dd>Package for iPhone and iPad.</dd>
153      * </dl>
154      *
155      * @parameter expression="${platform}"
156      * @required
157      */
158     protected String platform;
159 
160     /**
161      * <p>The version of the platform for which the code should be compiled.</p>
162      * <p>This is the version of the library to use to compile the application.
163      * It's possible to specify another android API for the android virtual device.
164      * See {@link VirtualDevice#androidAPI}.</p>
165      *
166      * @parameter expression="${androidAPI}"
167      */
168     protected String androidAPI;
169 
170     /**
171      * The version of the platform for which the code should be compiled.
172      *
173      * @parameter expression="${iosVersion}"
174      */
175     protected String iosVersion;
176 
177     /**
178      * The titanium SDK version to use.
179      *
180      * @parameter expression="${titaniumVersion}"
181      * @required
182      */
183     protected String titaniumVersion;
184 
185     /**
186      * <p>The titanium settings.</p>
187      * <p>Contains various information needed to execute a titanium build.</p>
188      * <p>Here's the list of the titaniumSettings parameters:</p>
189      * <dl>
190      *     <dt>{@link TitaniumSettings#androidBuilder}</dt>
191      *     <dd>The titanium android builder.py file location.
192      *     Optional.
193      *     If not specified it tries to retrieve the builder based on {@link #titaniumVersion}</dd>
194      *     <dt>{@link TitaniumSettings#iosBuilder}</dt>
195      *     <dd>The titanium iOS builder.py location.
196      *     Optional.
197      *     If not specified, it tries to retrieve the builder based on {@link #titaniumVersion}</dd>
198      *     <dt>{@link TitaniumSettings#androidSDK}</dt>
199      *     <dd>The android SDK location.
200      *     This parameter is optional, by default the android SDK is retrieved based on the environment
201      *     variable ANDROID_HOME.</dd>
202      *     <dt>{@link TitaniumSettings#keystore}</dt>
203      *     <dd>The android keystore to use to sign the application.
204      *     Optional. If not specified the Titanium keystore is used.</dd>
205      *     <dt>{@link TitaniumSettings#keystorePassword}</dt>
206      *     <dd>The android keystore password.
207      *     Optional. If not specified the default titanium keystore password is used.</dd>
208      *     <dt>{@link TitaniumSettings#keystoreAlias}</dt>
209      *     <dd>The android keystore key alias.
210      *     Optional. The alias of the key to use to sign the application.
211      *     If not specified the default titanium alias is used.</dd>
212      *     <dt>{@link TitaniumSettings#iosDevelopmentProvisioningProfile}</dt>
213      *     <dd>The iOS development provisioning profile.
214      *     This profile is use when {@link #executeMode} is <code>virtual</code>
215      *     or <code>device</code>.</dd>
216      *     <dt>{@link TitaniumSettings#iosDistributionProvisioningProfile}</dt>
217      *     <dd>The iOS distribution provisioning profile.
218      *     This profile is used when {@link #executeMode} is <code>none</code>.</dd>
219      *     <dt>{@link TitaniumSettings#iosDevelopmentCertificate}</dt>
220      *     <dd>The iOS development certificate.
221      *     This certificate is used when {@link #executeMode} is <code>virtual</code>
222      *     or <code>device</code>.</dd>
223      *     <dt>{@link TitaniumSettings#iosDistributionCertificate}</dt>
224      *     <dd>The iOS distribution certificate.
225      *     This certificate is used when {@link #executeMode} is <code>none</code>.</dd>
226      * </dl>
227      * @parameter
228      */
229     protected TitaniumSettings titaniumSettings;
230 
231     /**
232      * <p>Virtual device configuration.</p>
233      * <p>When {@link #executeMode} is virtual,
234      * the parameters in virtualDevice are used to configure the
235      * android emulator or iphone simulator.</p>
236      * <p>VirtualDevice has the following parameters:</p>
237      * <dl>
238      *     <dt>{@link VirtualDevice#androidAPI}</dt>
239      *     <dd>The version on which the virtual device should run.
240      *     If not specified, the latest android API version will be used.
241      *     Regardless of the global androidAPI value.</dd>
242      *     <dt>{@link VirtualDevice#iosVersion}</dt>
243      *     <dd>The ios version of the virtual device.
244      *     If not specified the latest available version will be used.
245      *     Regardless of the global parameter value.</dd>
246      *     <dt>{@link VirtualDevice#skin}</dt>
247      *     <dd>The skin of the android emulator.
248      *     Defaults to HVGA for version less than 10 and to WXGA for version greater than 10</dd>
249      *     <dt>{@link VirtualDevice#family}</dt>
250      *     <dd>The iOS device family.
251      *     Valid values are <code>iphone</code> or <code>ipad</code>.</dd>
252      *     <dt>{@link VirtualDevice#wait}</dt>
253      *     <dd>How much miliseconds to wait after launching emulator before
254      *     installing the android application.</dd>
255      * </dl>
256      * @see VirtualDevice
257      * @parameter
258      */
259     protected VirtualDevice virtualDevice;
260 
261     /**
262      * <p>The package execution mode.</p>
263      * <p>Allow the execution of the package on an emulator/device.</p>
264      * <p>Values are:</p>
265      * <dl>
266      *   <dt>none</dt>
267      *   <dd>Do not execute. (Default value)</dd>
268      *   <dt>virtual</dt>
269      *   <dd>Execute on an emulator whose settings are specified in {@link #virtualDevice}.</dd>
270      *   <dt>device</dt>
271      *   <dd>Execute on a connected device.</dd>
272      * </dl>
273      *
274      * @parameter default-value="none" expression="${executeMode}"
275      */
276     protected String executeMode;
277 
278     /**
279      * The output directory of the packaged titanium files.
280      *
281      * @parameter default-value="${project.build.outputDirectory}"
282      */
283     protected File outputDirectory;
284 
285     /**
286      * The ios Development certificate.
287      * @parameter expression="${iosDevelopmentCertificate}"
288      * @readonly
289      */
290     protected String iosDevelopmentCertificate;
291 
292     /**
293      * The ios Distribution certificate.
294      * @parameter expression="${iosDistributionCertificate}"
295      * @readonly
296      */
297     protected String iosDistributionCertificate;
298 
299 
300     /**
301      * The maven project.
302      *
303      * @parameter expression="${project}"
304      * @required
305      * @readonly
306      */
307     protected MavenProject project;
308 
309     /**
310      * @component
311      */
312     protected ArtifactFactory artifactFactory;
313 
314     /**
315      * @component
316      */
317     protected ArtifactResolver artifactResolver;
318 
319     /**
320      * @parameter default-value="${localRepository}"
321      * @readonly
322      */
323     protected ArtifactRepository localRepository;
324 
325     /**
326      * @parameter default-value="${project.remoteArtifactRepositories}"
327      * @readonly
328      */
329     protected List remoteRepositories;
330 
331     /**
332      * The iOS development provisioning profile.
333      * @parameter expression="${iosDevelopmentProvisioningProfile}"
334      * @readonly
335      */
336     protected String iosDevelopmentProvisioningProfile;
337 
338     /**
339      * The ios distribution provisioning profile.
340      * @parameter expression="${iosDistributionProvisioningProfile}"
341      * @readonly
342      */
343     protected String iosDistributionProvisioningProfile;
344 
345 
346     /**
347      * <p>Retrieve the ios platform version.</p>
348      * <p>If the ios platform version is not specified, retrieve one
349      * from the XCode installation.</p>
350      * @return The iOS platform version.
351      */
352     protected String getIosVersion() {
353         if (iosVersion == null) {
354             iosVersion = TitaniumUtils.getLatestIosPlatformVersion();
355         }
356         return iosVersion;
357     }
358 
359     /**
360      * <p>Retrieve the android API to use when building the application.</p>
361      * <p>If the android API is not specified, the latest one is retrieved based on the
362      * Android SDK location.</p>
363      * @return The android API level.
364      * @throws org.apache.maven.plugin.MojoExecutionException
365      */
366     protected String getAndroidAPI() throws MojoExecutionException {
367         if (androidAPI == null) {
368             androidAPI = TitaniumUtils.getLatestAndroidPlatformVersion(getTitaniumSettings().getAndroidSdk());
369         }
370         return androidAPI;
371     }
372 
373     /**
374      * Retrieve the titanium project folder for the specified platform.
375      * @return A File representing the titanium project folder.
376      */
377     protected File getTiProjectDirectory() {
378         return new File(outputDirectory, platform);
379     }
380 
381     /**
382      * <p>Retrieve the TitaniumSettings object.</p>
383      * <p>If {@link #titaniumSettings} is null, a new TitaniumSettings
384      * is constructed based on the specified expressions.</p>
385      * @return An initialized TitaniumSettings object.
386      * @see #androidSDKPath
387      * @see #iosBuilderPath
388      * @see #androidBuilderPath
389      * @see #titaniumSettingsKeystore
390      * @see #titaniumSettingsKeystorePassword
391      * @see #titaniumSettingsKeystoreAlias
392      */
393     protected TitaniumSettings getTitaniumSettings() {
394         if (titaniumSettings == null) {
395             titaniumSettings = new TitaniumSettings();
396         }
397         if (androidSDKPath != null) {
398             titaniumSettings.setAndroidSdk(androidSDKPath);
399         }
400         if (iosBuilderPath != null) {
401             titaniumSettings.setIosBuilder(iosBuilderPath);
402         }
403         if (androidBuilderPath != null) {
404             titaniumSettings.setAndroidBuilder(androidBuilderPath);
405         }
406         if (titaniumSettingsKeystore != null) {
407             titaniumSettings.setKeystore(titaniumSettingsKeystore);
408         }
409         if (titaniumSettingsKeystoreAlias != null) {
410             titaniumSettings.setKeystoreAlias(titaniumSettingsKeystoreAlias);
411         }
412         if (titaniumSettingsKeystorePassword != null) {
413             titaniumSettings.setKeystorePassword(titaniumSettingsKeystorePassword);
414         }
415         if (iosDevelopmentCertificate != null) {
416             titaniumSettings.setIosDevelopmentCertificate(iosDevelopmentCertificate);
417         }
418         if (iosDistributionCertificate != null) {
419             titaniumSettings.setIosDistributionCertificate(iosDistributionCertificate);
420         }
421         if (iosDevelopmentProvisioningProfile != null) {
422             titaniumSettings.setIosDevelopmentProvisioningProfile(iosDevelopmentProvisioningProfile);
423         }
424         if (iosDistributionProvisioningProfile != null) {
425             titaniumSettings.setIosDistributionProvisioningProfile(iosDistributionProvisioningProfile);
426         }
427         return titaniumSettings;
428     }
429 
430     /**
431      * <p>Retrieve the VirtualDevice object.</p>
432      * <p>If {@link #virtualDevice} is null, a new VirtualDevice
433      * is constructed based on the specified expressions.</p>
434      * @return An initialized VirtualDevice object.
435      * @see #virtualDeviceAndroidAPI
436      * @see #virtualDeviceFamily
437      * @see #virtualDeviceIosVersion
438      * @see #virtualDeviceSkin
439      * @see #virtualDeviceWait
440      */
441     protected VirtualDevice getVirtualDevice() {
442         if (virtualDevice == null) {
443             virtualDevice = new VirtualDevice();
444         }
445         if (this.virtualDeviceAndroidAPI != null) {
446             virtualDevice.setAndroidAPI(this.virtualDeviceAndroidAPI);
447         }
448         if (this.virtualDeviceFamily != null) {
449             virtualDevice.setFamily(this.virtualDeviceFamily);
450         }
451         if (this.virtualDeviceIosVersion != null) {
452             virtualDevice.setIosVersion(this.virtualDeviceIosVersion);
453         }
454         if (this.virtualDeviceSkin != null) {
455             virtualDevice.setSkin(this.virtualDeviceSkin);
456         }
457         if (this.virtualDeviceWait != null) {
458             virtualDevice.setWait(this.virtualDeviceWait);
459         }
460         return virtualDevice;
461     }
462 
463     /**
464      * <p>Check the titanium pom settings.</p>
465      * <p>Mainly check if the {@link #androidAPI} or the {@link #iosVersion} parameters are valid.</p>
466      * @return true if the pom settings are not valid.
467      * @throws MojoExecutionException If an error occurs.
468      */
469     protected boolean checkPomSettings() throws MojoExecutionException {
470         if (platform.equals("android")) {
471             if (!TitaniumUtils.isAndroidVersionValid(getTitaniumSettings().getAndroidSdk(), getAndroidAPI())) {
472                 try {
473                     List<Integer> availApis = TitaniumUtils.getAvailableAndroidPlatformVersions(getTitaniumSettings().getAndroidSdk());
474                     StringBuilder availPlatforms = new StringBuilder();
475                     for (int i=0; i<availApis.size(); i++) {
476                         if (i>0) {
477                             availPlatforms.append(", ");
478                         }
479                         availPlatforms.append(availApis.get(i).toString());
480                     }
481                     String errorMsg = MessageFormat.format("The ios version {0} is not valid. Valid versions are ${1}.",
482                             getAndroidAPI(), availPlatforms.toString());
483                     getLog().error(errorMsg);
484                 } catch (Throwable t) {
485                     getLog().error("The specified android API is not valid");
486                 }
487                 return false;
488             }
489         } else if (platform.equals("iphone") || platform.equals("ipad") || platform.equals("universal")) {
490             if (!TitaniumUtils.isIphoneVersionValid(getIosVersion())) {
491                 try {
492                     List<String> platforms = TitaniumUtils.listAvailableIosPlatformVersions();
493                     StringBuilder availPlatforms = new StringBuilder();
494                     if (platforms != null) {
495                         for(int i=0; i<platforms.size(); i++) {
496                             if (i>0) {
497                                 availPlatforms.append(", ");
498                             }
499                             availPlatforms.append(platforms.get(i));
500                         }
501                     }
502                     String errorMsg = MessageFormat.format("The ios version {0} is not valid. Valid versions are ${1}.",
503                             getIosVersion(), availPlatforms.toString());
504                     getLog().error(errorMsg);
505                 } catch (Throwable t) {}
506                 return false;
507             }
508         }
509 
510         return true;
511     }
512 
513     protected void downloadTitaniumFromRepositories(String tiVersion, File targetDir) throws ArtifactResolutionException, ArtifactNotFoundException, IOException {
514         final String tiGroupId = "org.appcelerator.titanium";
515         final String tiArtifactId = "titanium-mobile";
516 
517         String classifier = TitaniumUtils.getOsClassifier();
518         Artifact artifact = artifactFactory.createArtifactWithClassifier(tiGroupId, tiArtifactId, tiVersion, "zip", classifier);
519 
520         artifactResolver.resolve(artifact, remoteRepositories, localRepository);
521 
522         File file = artifact.getFile();
523 
524         extractZipFile(file, targetDir);
525     }
526 
527     protected File resolveAndroidBuilder() throws MojoExecutionException {
528         File destTiFolder = new File(outputDirectory, "titanium_mobile");
529 
530         File builderFile = getTitaniumSettings().getAndroidBuilder(titaniumVersion);
531         if (builderFile == null) {
532             try {
533                 builderFile = new File(destTiFolder, "mobilesdk" + File.separator
534                     + TitaniumUtils.getOsClassifier() + File.separatorChar
535                     + titaniumVersion + File.separatorChar
536                     + "android" + File.separatorChar + "builder.py");
537 
538                 if (!builderFile.exists()) {
539                     getLog().info("Downloading Titanium SDK from repository...");
540                     downloadTitaniumFromRepositories(titaniumVersion, destTiFolder);
541                 }
542                 if ( !builderFile.exists()) {
543                     getLog().error("Unable to retrieve android builder from extracted artifact");
544                     builderFile = null;
545                     throw new MojoExecutionException("Unable to retrieve android builder from extracted artifact");
546                 } else {
547                     getLog().info("Using artifact android builder: " + builderFile.getAbsolutePath());
548                 }
549             } catch (ArtifactResolutionException e) {
550                 throw new MojoExecutionException("Unable to resolve Titanium SDK artifact", e);
551             } catch (ArtifactNotFoundException e) {
552                 throw new MojoExecutionException("Unable to find the titanium SDK artifact to download for version " + titaniumVersion, e);
553             } catch (IOException e) {
554                 throw new MojoExecutionException("An error occured while extracting the titanium mobile SDK", e);
555             }
556         }
557 
558         return builderFile;
559     }
560 
561     protected File resolveIOSBuilder() throws MojoExecutionException {
562         File destTiFolder = new File(outputDirectory, "titanium_mobile");
563 
564         File builderFile = getTitaniumSettings().getIosBuilder(titaniumVersion);
565         if (builderFile == null) {
566             builderFile = new File(destTiFolder, "mobilesdk" + File.separator
567                 + TitaniumUtils.getOsClassifier() + File.separator
568                 + titaniumVersion + File.separator
569                 + "iphone" + File.separator + "builder.py");
570 
571             if (!builderFile.exists()) {
572                 getLog().info("Downloading Titanium SDK from repository...");
573                 try {
574                     downloadTitaniumFromRepositories(titaniumVersion, destTiFolder);
575                 } catch (ArtifactResolutionException e) {
576                     throw new MojoExecutionException("Unable to resolve Titanium SDK artifact", e);
577                 } catch (ArtifactNotFoundException e) {
578                     throw new MojoExecutionException("Unable to find the titanium SDK artifact to download for version " + titaniumVersion, e);
579                 } catch (IOException e) {
580                     throw new MojoExecutionException("An error occured while extracting the titanium mobile SDK", e);
581                 }
582             }
583             if ( !builderFile.exists()) {
584                 getLog().error("Unable to retrieve ios builder from extracted artifact");
585                 builderFile = null;
586                 throw new MojoExecutionException("Unable to retrieve ios builder from extracted artifact");
587             } else {
588                 getLog().info("Using artifact ios builder: " + builderFile.getAbsolutePath());
589             }
590 
591         }
592 
593         return builderFile;
594     }
595 
596     protected void extractZipFile(File zipFile, File destFolder) throws IOException {
597         final int BUFFER_SIZE = 1024;
598         byte[] buffer = new byte[BUFFER_SIZE];
599 
600         ZipInputStream zis = null;
601         ZipEntry zipEntry;
602 
603         try {
604             zis = new ZipInputStream(new FileInputStream(zipFile));
605 
606             while ((zipEntry = zis.getNextEntry()) != null) {
607                 String entryName = zipEntry.getName();
608                 File newFile = new File(destFolder, entryName);
609                 if (newFile.getParentFile() != null) {
610                     newFile.getParentFile().mkdirs();
611                 }
612 
613                 FileOutputStream fos = null;
614                 try {
615                     fos = new FileOutputStream(newFile);
616                     int n;
617 
618                     while ((n = zis.read(buffer, 0, BUFFER_SIZE)) > -1) {
619                         fos.write(buffer, 0, n);
620                     }
621                 } finally {
622                     IOUtil.close(fos);
623                 }
624                 zis.closeEntry();
625 
626                 newFile.setReadable(true);
627                 newFile.setWritable(true);
628                 if (getFileExtension(newFile).equals("py")) {
629                     newFile.setExecutable(true);
630                 }
631             }
632         } finally {
633             IOUtil.close(zis);
634         }
635     }
636 
637     private String getFileExtension(final File file) {
638         int mid = file.getName().lastIndexOf(".");
639         if (mid != -1) {
640             return file.getName().substring(mid+1, file.getName().length());
641         } else {
642             return "";
643         }
644     }
645 }