diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/Application.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/Application.java index 590633c250..646d03affd 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/Application.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/Application.java @@ -51,8 +51,6 @@ public class Application { private List applicationReleases; -// private ApplicationRelease releaseVersion; - private DeviceType devicetype; public int getId() { @@ -105,14 +103,6 @@ public class Application { this.unrestrictedRoles = unrestrictedRoles; } -// public ApplicationRelease getReleaseVersion() { -// return releaseVersion; -// } -// -// public void setReleaseVersion(ApplicationRelease releaseVersion) { -// this.releaseVersion = releaseVersion; -// } - public String getType() { return type; } diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/ApplicationRelease.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/ApplicationRelease.java index 4f9271f2a6..b46aa0d07d 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/ApplicationRelease.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/ApplicationRelease.java @@ -82,6 +82,8 @@ public class ApplicationRelease { private int stars; + private String url; + public int getNoOfRatedUsers() { return noOfRatedUsers; } @@ -289,4 +291,12 @@ public class ApplicationRelease { public void setIconLoc(String iconLoc) { this.iconLoc = iconLoc; } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } } diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/ApplicationType.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/ApplicationType.java index 0a41501cc0..bfcd77a58f 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/ApplicationType.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.common/src/main/java/org/wso2/carbon/device/application/mgt/common/ApplicationType.java @@ -23,6 +23,6 @@ package org.wso2.carbon.device.application.mgt.common; * Application Types. */ public enum ApplicationType { - ANDROID, iOS, WEB_CLIP + ANDROID, IOS, WEB_CLIP } diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/pom.xml b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/pom.xml index 6dec736627..4fbf25844e 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/pom.xml +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/pom.xml @@ -186,6 +186,16 @@ apk-parser 2.5.2 + + com.googlecode.plist + dd-plist + 1.20 + + + commons-validator + commons-validator + 1.6 + diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/impl/ApplicationStorageManagerImpl.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/impl/ApplicationStorageManagerImpl.java index caa5847071..a4aad2b7bc 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/impl/ApplicationStorageManagerImpl.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/impl/ApplicationStorageManagerImpl.java @@ -19,6 +19,10 @@ package org.wso2.carbon.device.application.mgt.core.impl; +import com.dd.plist.NSDictionary; +import com.dd.plist.NSString; +import com.dd.plist.PropertyListFormatException; +import com.dd.plist.PropertyListParser; import net.dongliu.apk.parser.ApkFile; import net.dongliu.apk.parser.bean.ApkMeta; import org.apache.commons.codec.digest.DigestUtils; @@ -28,20 +32,32 @@ import org.apache.commons.logging.LogFactory; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.application.mgt.common.ApplicationRelease; import org.wso2.carbon.device.application.mgt.common.ApplicationType; -import org.wso2.carbon.device.application.mgt.common.exception.ApplicationManagementException; import org.wso2.carbon.device.application.mgt.common.exception.ApplicationStorageManagementException; import org.wso2.carbon.device.application.mgt.common.exception.ResourceManagementException; import org.wso2.carbon.device.application.mgt.common.services.ApplicationStorageManager; import org.wso2.carbon.device.application.mgt.core.util.ConnectionManagerUtil; import org.wso2.carbon.device.application.mgt.core.util.Constants; import org.wso2.carbon.device.application.mgt.core.util.StorageManagementUtil; +import org.apache.commons.validator.routines.UrlValidator; +import org.xml.sax.SAXException; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.text.ParseException; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import javax.xml.parsers.ParserConfigurationException; + +import static org.wso2.carbon.device.application.mgt.core.util.StorageManagementUtil.deleteDir; import static org.wso2.carbon.device.application.mgt.core.util.StorageManagementUtil.saveFile; /** @@ -51,6 +67,7 @@ public class ApplicationStorageManagerImpl implements ApplicationStorageManager private static final Log log = LogFactory.getLog(ApplicationStorageManagerImpl.class); private String storagePath; private int screenShotMaxCount; + private static final int BUFFER_SIZE = 4096; /** * Create a new ApplicationStorageManager Instance @@ -163,11 +180,30 @@ public class ApplicationStorageManagerImpl implements ApplicationStorageManager public ApplicationRelease uploadReleaseArtifact(ApplicationRelease applicationRelease, String appType, InputStream binaryFile) throws ResourceManagementException { - String artifactDirectoryPath; - String md5OfApp; - md5OfApp = getMD5(binaryFile); - try { + if (ApplicationType.WEB_CLIP.toString().equals(appType)) { + applicationRelease.setVersion(Constants.DEFAULT_VERSION); + UrlValidator urlValidator = new UrlValidator(); + if (applicationRelease.getUrl() == null || !urlValidator.isValid(applicationRelease.getUrl())) { + throw new ApplicationStorageManagementException("Request payload doesn't contains Web Clip URL " + + "with application release object or Web " + + "Clip URL is invalid"); + //todo if we throw this we must send BAD REQUEST to end user + } + applicationRelease.setAppStoredLoc(applicationRelease.getUrl()); + applicationRelease.setAppHashValue(null); + return applicationRelease; + } + + String artifactDirectoryPath; + String md5OfApp; + md5OfApp = getMD5(binaryFile); + + if (md5OfApp == null) { + throw new ApplicationStorageManagementException( + "Error occurred while md5sum value retrieving process: " + + "application UUID " + applicationRelease.getUuid()); + } if (ApplicationType.ANDROID.toString().equals(appType)) { String prefix = "stream2file"; @@ -184,38 +220,43 @@ public class ApplicationStorageManagerImpl implements ApplicationStorageManager if (!isTempDelete) { log.error("Temporary created APK file deletion failed"); } - } else if (ApplicationType.iOS.toString().equals(appType)) { - //todo iOS ipa validate - } else if (ApplicationType.WEB_CLIP.toString().equals(appType)) { - //todo Web Clip validate + } else if (ApplicationType.IOS.toString().equals(appType)) { + String prefix = "stream2file"; + String suffix = ".ipa"; + Boolean isTempDelete; + + File tempFile = File.createTempFile(prefix, suffix); + FileOutputStream out = new FileOutputStream(tempFile); + IOUtils.copy(binaryFile, out); + Map plistInfo = getIPAInfo(tempFile); + applicationRelease.setVersion(plistInfo.get("CFBundleVersion")); + isTempDelete = tempFile.delete(); + if (!isTempDelete) { + log.error("Temporary created ipa file deletion failed"); + } } else { throw new ApplicationStorageManagementException("Application Type doesn't match with supporting " + "application types " + applicationRelease.getUuid()); } - if (md5OfApp != null) { - artifactDirectoryPath = storagePath + md5OfApp; - StorageManagementUtil.createArtifactDirectory(artifactDirectoryPath); - if (log.isDebugEnabled()) { - log.debug("Artifact Directory Path for saving the application release related artifacts related with " - + "application UUID " + applicationRelease.getUuid() + " is " + artifactDirectoryPath); - } - - String artifactPath = artifactDirectoryPath + Constants.RELEASE_ARTIFACT; - saveFile(binaryFile, artifactPath); - applicationRelease.setAppStoredLoc(artifactPath); - applicationRelease.setAppHashValue(md5OfApp); - } else { - throw new ApplicationStorageManagementException("Error occurred while md5sum value retrieving process: " + - "application UUID " + applicationRelease.getUuid()); + artifactDirectoryPath = storagePath + md5OfApp; + StorageManagementUtil.createArtifactDirectory(artifactDirectoryPath); + if (log.isDebugEnabled()) { + log.debug("Artifact Directory Path for saving the application release related artifacts related with " + + "application UUID " + applicationRelease.getUuid() + " is " + artifactDirectoryPath); } + + String artifactPath = artifactDirectoryPath + Constants.RELEASE_ARTIFACT; + saveFile(binaryFile, artifactPath); + applicationRelease.setAppStoredLoc(artifactPath); + applicationRelease.setAppHashValue(md5OfApp); + } catch (IOException e) { throw new ApplicationStorageManagementException( "IO Exception while saving the release artifacts in the server for the application UUID " + applicationRelease.getUuid(), e); } - return applicationRelease; } @@ -267,4 +308,130 @@ public class ApplicationStorageManagerImpl implements ApplicationStorageManager } return md5; } + + private synchronized Map getIPAInfo(File ipaFile) throws ApplicationStorageManagementException { + Map ipaInfo = new HashMap<>(); + + String ipaDirectory = null; + try { + String ipaAbsPath = ipaFile.getAbsolutePath(); + ipaDirectory = new File(ipaAbsPath).getParent(); + + if (new File(ipaDirectory + File.separator + Constants.PAYLOAD).exists()) { + deleteDir(new File(ipaDirectory + File.separator + Constants.PAYLOAD)); + } + + // unzip ipa zip file + unzip(ipaAbsPath, ipaDirectory); + + // fetch app file name, after unzip ipa + String appFileName = ""; + for (File file : Objects.requireNonNull( + new File(ipaDirectory + File.separator + Constants.PAYLOAD).listFiles() + )) { + if (file.toString().endsWith(Constants.APP_EXTENSION)) { + appFileName = new File(file.toString()).getAbsolutePath(); + break; + } + } + + String plistFilePath = appFileName + File.separator + Constants.PLIST_NAME; + + // parse info.plist + File plistFile = new File(plistFilePath); + NSDictionary rootDict; + rootDict = (NSDictionary) PropertyListParser.parse(plistFile); + + // get version + NSString parameter = (NSString) rootDict.objectForKey(Constants.CF_BUNDLE_VERSION); + ipaInfo.put(Constants.CF_BUNDLE_VERSION, parameter.toString()); + + } catch (ParseException e) { + String msg = "Error occurred while parsing the plist data"; + log.error(msg); + throw new ApplicationStorageManagementException(msg, e); + } catch (IOException e) { + String msg = "Error occurred while accessing the ipa file"; + log.error(msg); + throw new ApplicationStorageManagementException(msg, e); + } catch (SAXException | ParserConfigurationException | PropertyListFormatException e) { + log.error(e); + throw new ApplicationStorageManagementException(e.getMessage(), e); + } catch (ApplicationStorageManagementException e) { + String msg = "Error occurred while unzipping the ipa file"; + log.error(msg); + throw new ApplicationStorageManagementException(msg, e); + } finally { + if (ipaDirectory != null) { + // remove unzip folder + deleteDir(new File(ipaDirectory + File.separator + Constants.PAYLOAD)); + } + } + return ipaInfo; + } + + /** + * Extracts a zip file specified by the zipFilePath to a directory specified by + * destDirectory (will be created if does not exists) + * + * @param zipFilePath file path of the zip + * @param destDirectory destination directory path + */ + private void unzip(String zipFilePath, String destDirectory) + throws IOException, ApplicationStorageManagementException { + File destDir = new File(destDirectory); + Boolean isDirCreated; + + if (!destDir.exists()) { + isDirCreated = destDir.mkdir(); + if (!isDirCreated) { + throw new ApplicationStorageManagementException("Directory Creation Is Failed while iOS app vertion " + + "retrieval"); + } + } + + ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath)); + ZipEntry entry = zipIn.getNextEntry(); + + // iterates over entries in the zip file + while (entry != null) { + String filePath = destDirectory + File.separator + entry.getName(); + + if (!entry.isDirectory()) { + // if the entry is a file, extracts it + extractFile(zipIn, filePath); + } else { + // if the entry is a directory, make the directory + File dir = new File(filePath); + isDirCreated = dir.mkdir(); + if (!isDirCreated) { + throw new ApplicationStorageManagementException( + "Directory Creation Is Failed while iOS app vertion " + + "retrieval"); + } + + } + + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + zipIn.close(); + } + + /** + * Extracts a zip entry (file entry) + * + * @param zipIn zip input stream + * @param filePath file path + */ + private void extractFile(ZipInputStream zipIn, String filePath) throws IOException { + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath)); + byte[] bytesIn = new byte[BUFFER_SIZE]; + int read; + + while ((read = zipIn.read(bytesIn)) != -1) { + bos.write(bytesIn, 0, read); + } + bos.close(); + } } diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/util/Constants.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/util/Constants.java index e01914eaf7..a0294c4aad 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/util/Constants.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.core/src/main/java/org/wso2/carbon/device/application/mgt/core/util/Constants.java @@ -31,9 +31,11 @@ public class Constants { public static final String DEFAULT_CONFIG_FILE_LOCATION = CarbonUtils.getCarbonConfigDirPath() + File.separator + Constants.APPLICATION_CONFIG_XML_FILE; - //can remove - public static final String PLATFORMS_DEPLOYMENT_DIR_NAME = "platforms"; - public static final String PLATFORM_DEPLOYMENT_EXT = ".xml"; + public static final String DEFAULT_VERSION = "1.0.0"; + public static final String PAYLOAD = "Payload"; + public static final String PLIST_NAME = "Info.plist"; + public static final String CF_BUNDLE_VERSION = "CFBundleVersion"; + public static final String APP_EXTENSION = ".app"; /** * Database types supported by Application Management. diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/ApplicationManagementAPI.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/ApplicationManagementAPI.java index c22132d513..69fc0e9a64 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/ApplicationManagementAPI.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/ApplicationManagementAPI.java @@ -397,7 +397,7 @@ public interface ApplicationManagementAPI { response = ErrorResponse.class) }) Response updateApplicationArtifact( - @ApiParam(name = "appType", value = "Type of the application i.e Android, iOS etc", required = true) + @ApiParam(name = "appType", value = "Type of the application i.e Android, IOS etc", required = true) @PathParam("appType") String appType, @ApiParam(name = "id", value = "Id of the application", required = true) @PathParam("uuid") int applicationId, diff --git a/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/impl/ApplicationManagementAPIImpl.java b/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/impl/ApplicationManagementAPIImpl.java index 6ecd44cba7..eaa12feed8 100644 --- a/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/impl/ApplicationManagementAPIImpl.java +++ b/components/application-mgt/org.wso2.carbon.device.application.mgt.publisher.api/src/main/java/org/wso2/carbon/device/application/mgt/publisher/api/services/impl/ApplicationManagementAPIImpl.java @@ -138,10 +138,16 @@ public class ApplicationManagementAPIImpl implements ApplicationManagementAPI { return Response.status(Response.Status.BAD_REQUEST).build(); } - if (binaryFile == null) { + if (binaryFile == null && !ApplicationType.WEB_CLIP.toString().equals(application.getType())) { log.error("Binary file is not uploaded for the application release of " + application.getName() + " of application type " + application.getType()); return Response.status(Response.Status.BAD_REQUEST).build(); + }else if(binaryFile == null && ApplicationType.WEB_CLIP.toString().equals(application.getType())){ + applicationRelease = applicationStorageManager.uploadReleaseArtifact(applicationRelease, application.getType(), + null); + }else if (binaryFile != null && !ApplicationType.WEB_CLIP.toString().equals(application.getType())){ + applicationRelease = applicationStorageManager.uploadReleaseArtifact(applicationRelease, application.getType(), + binaryFile.getDataHandler().getInputStream()); } iconFileStream = iconFile.getDataHandler().getInputStream(); @@ -151,8 +157,6 @@ public class ApplicationManagementAPIImpl implements ApplicationManagementAPI { attachments.add(screenshot.getDataHandler().getInputStream()); } - applicationRelease = applicationStorageManager.uploadReleaseArtifact(applicationRelease, application.getType(), - binaryFile.getDataHandler().getInputStream()); if (applicationRelease.getAppStoredLoc() == null || applicationRelease.getAppHashValue() == null) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();