diff --git a/pom.xml b/pom.xml index 27555f5..0eed730 100644 --- a/pom.xml +++ b/pom.xml @@ -49,7 +49,7 @@ UTF-8 11 - 1.1.5 + 1.2.6 1.0.2 2.10.1 4.11.0 @@ -83,16 +83,6 @@ com.zebrunner carina-utils ${carina-utils.version} - - - org.testng - * - - - org.seleniumhq.selenium - * - - com.zebrunner diff --git a/src/main/java/com/zebrunner/carina/appcenter/AppCenterApp.java b/src/main/java/com/zebrunner/carina/appcenter/AppCenterApp.java new file mode 100644 index 0000000..5657f8b --- /dev/null +++ b/src/main/java/com/zebrunner/carina/appcenter/AppCenterApp.java @@ -0,0 +1,40 @@ +package com.zebrunner.carina.appcenter; + +import com.zebrunner.carina.commons.artifact.IAppInfo; + +public final class AppCenterApp implements IAppInfo { + private String directLink; + private String version; + private String build; + + AppCenterApp() { + } + + @Override + public String getDirectLink() { + return this.directLink; + } + + void setDirectLink(String directLink) { + this.directLink = directLink; + } + + @Override + public String getVersion() { + return this.version; + } + + void setVersion(String version) { + this.version = version; + } + + @Override + public String getBuild() { + return this.build; + } + + void setBuild(String build) { + this.build = build; + } +} + diff --git a/src/main/java/com/zebrunner/carina/appcenter/AppCenterManager.java b/src/main/java/com/zebrunner/carina/appcenter/AppCenterManager.java index cbb16fa..909bafc 100644 --- a/src/main/java/com/zebrunner/carina/appcenter/AppCenterManager.java +++ b/src/main/java/com/zebrunner/carina/appcenter/AppCenterManager.java @@ -27,6 +27,11 @@ import com.zebrunner.carina.utils.config.StandardConfigurationOption; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.concurrent.ConcurrentException; +import org.apache.commons.lang3.concurrent.LazyInitializer; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,12 +46,12 @@ import java.nio.channels.ReadableByteChannel; import java.nio.file.Path; import java.util.Comparator; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -56,70 +61,186 @@ */ public class AppCenterManager implements IArtifactManager { private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + private static final Map> APP_INFO_MAP = new ConcurrentHashMap<>(); private final ApiClient apiClient; - - private String ownerName; - private String versionLong; - private String versionShort; + private static final String INVALID_LINK_MESSAGE = "AppCenter url is not correct: %s%n It should be like: %s."; + private static final String VALID_LINK = "appcenter://appName/platformName/buildType/version"; private static final String APP_NAME = "appName"; private static final String PLATFORM_NAME = "platformName"; private static final String BUILD_TYPE = "buildType"; private static final String APP_VERSION = "version"; - // appcenter://appName/platformName/buildType/version + /** + * Pattern for link like {@code appcenter://appName/platformName/buildType/version} + */ private static final Pattern APP_CENTER_ENDPOINT_PATTERN = Pattern.compile( "appcenter:\\/\\/(?<" + APP_NAME + ">[a-zA-Z-0-9][^\\/]*)\\/" + "(?<" + PLATFORM_NAME + ">[a-zA-Z-0-9][^\\/]*)\\/" + "(?<" + BUILD_TYPE + ">[a-zA-Z-0-9][^\\/]*)\\/" + "(?<" + APP_VERSION + ">[a-zA-Z-0-9][^\\/]*)"); - private static AppCenterManager instance = null; private AppCenterManager() { - this.apiClient = new ApiClient().addDefaultHeader("x-api-token", - Configuration.getRequired(AppCenterConfiguration.Parameter.APPCENTER_TOKEN, StandardConfigurationOption.DECRYPT)); + apiClient = new ApiClient().addDefaultHeader("x-api-token", + Configuration.getRequired(AppCenterConfiguration.Parameter.APPCENTER_TOKEN, + StandardConfigurationOption.DECRYPT)); } + /** + * Get new instance of {@link AppCenterManager} + * + * @return {@link AppCenterManager} + */ public static synchronized AppCenterManager getInstance() { - if (instance == null) { - instance = new AppCenterManager(); - } - return instance; + return new AppCenterManager(); } /** + * Get direct (pre-signed) link to the application + * * @param appName takes in the AppCenter Name to look for. * @param platformName takes in the platform we wish to download for. * @param buildType takes in the particular build to download (i.e. Prod.AdHoc, QA.Debug, Prod-Release, QA-Internal etc...) * @param version takes in either "latest" to take the first build that matches the criteria or allows to consume a version to download that * build. * @return download url for build artifact. + * @deprecated use {@link #getAppInfo(String, String, String, String)} instead + */ + @Deprecated(forRemoval = true, since = "1.2.6") + public String getDownloadUrl(String appName, String platformName, String buildType, String version) { + if (StringUtils.isBlank(appName) || + StringUtils.isBlank(platformName) || + StringUtils.isBlank(buildType) || + StringUtils.isBlank(version)) { + throw new IllegalArgumentException("Parameters could not be null or empty or blank."); + } + return getAppInfo(appName, platformName, buildType, version) + .getDirectLink(); + } + + /** + * Get {@link AppCenterApp} with the information about application (direct link, version and so on) + * + * @param appName takes in the AppCenter Name to look for. + * @param platformName takes in the platform we wish to download for. + * @param buildType takes in the particular build to download (i.e. Prod.AdHoc, QA.Debug, Prod-Release, QA-Internal etc...) + * @param version takes in either "latest" to take the first build that matches the criteria or allows to consume a version to download that + * build. + * @return {@link AppCenterApp} */ - public String getDownloadUrl(String appName, String platformName, String buildType, String version) { - return scanAppForBuild(getAppId(appName, platformName), buildType, version); - } + public AppCenterApp getAppInfo(String appName, String platformName, String buildType, String version) { + return getAppInfo(String.format("appcenter://%s/%s/%s/%s", appName, platformName, buildType, version)); + } + + /** + * Get {@link AppCenterApp} with the information about application (direct link, version and so on) + * + * @param originalLink {@code appcenter://appName/platformName/buildType/version} + * @return {@link AppCenterApp} + */ + public AppCenterApp getAppInfo(String originalLink) { + try { + return APP_INFO_MAP.computeIfAbsent(originalLink, + link -> new LazyInitializer<>() { + @Override + protected AppCenterApp initialize() throws ConcurrentException { + Matcher matcher = APP_CENTER_ENDPOINT_PATTERN.matcher(Objects.requireNonNull(link)); + if (!matcher.find()) { + throw new IllegalArgumentException(String.format(INVALID_LINK_MESSAGE, link, VALID_LINK)); + } + String appName = matcher.group(APP_NAME); + String platformName = matcher.group(PLATFORM_NAME); + String buildType = matcher.group(BUILD_TYPE); + String version = matcher.group(APP_VERSION); + + Map appMap = new AccountApi(apiClient) + .appsList(null) + .stream() + .filter(a -> StringUtils.equalsIgnoreCase(platformName, a.getOs().toString()) && + StringUtils.containsIgnoreCase(a.getName(), appName)) + .map(a -> { + String app = a.getName(); + return new ImmutablePair<>(app, a); + }) + .sorted((a, b) -> Comparator.reverseOrder() + .compare(getLatestBuildDate(a.getLeft(), a.getRight().getUpdatedAt(), a.getRight().getOwner().getName()), + getLatestBuildDate(b.getLeft(), b.getRight().getUpdatedAt(), a.getRight().getOwner().getName()))) + .collect(Collectors.toMap(ImmutablePair::getLeft, ImmutablePair::getRight, (e1, e2) -> e1, LinkedHashMap::new)); + + if (appMap.isEmpty()) { + throw new NotFoundException( + String.format("Application '%s' Not Found in AppCenter, Platform (%s)", + appName, + platformName)); + } + + for (Map.Entry entry : appMap.entrySet()) { + String name = entry.getKey(); + App app = entry.getValue(); + + List retrieveList = new DistributeApi(apiClient) + .releasesList(app.getOwner().getName(), name, true, + "tester", null, + null); + + LOGGER.debug("Available Builds JSON: {}", retrieveList); + if (!retrieveList.isEmpty()) { + int buildLimiter = 0; + for (ReleasesAvailableToTester build : retrieveList) { + buildLimiter += 1; + if (buildLimiter >= 50) { + break; + } + + Integer latestBuildNumber = build.getId(); + AppCenterApp appCenterApp = new AppCenterApp(); + appCenterApp.setVersion(build.getShortVersion()); + appCenterApp.setBuild(build.getVersion()); + + ReleaseDetailsResponse appBuild = new DistributeApi(apiClient).releasesGetLatestByUser( + String.valueOf(latestBuildNumber), + app.getOwner().getName(), name, null, null); + + if (checkBuild(version, appBuild) && (checkTitleForCorrectPattern(buildType.toLowerCase(), appBuild) + || checkNotesForCorrectBuild( + buildType.toLowerCase(), appBuild))) { + LOGGER.debug("Print Build Info: {}", appBuild); + LOGGER.info("Fetching Build ID ({}) Version: {} ({})", latestBuildNumber, appCenterApp.getVersion(), + appCenterApp.getBuild()); + String buildUrl = appBuild.getDownloadUrl(); + LOGGER.info("Download URL For Build: {}", buildUrl); + appCenterApp.setDirectLink(buildUrl); + return appCenterApp; + } + } + } + } + throw new NotFoundException(String.format("Unable to find build to download, version provided (%s)", version)); + } + }) + .get(); + } catch (ConcurrentException e) { + return ExceptionUtils.rethrow(e); + } + } @Override public boolean download(String from, Path to) { - if (!ObjectUtils.allNotNull(from, to) || from.isEmpty()) { - throw new IllegalArgumentException("Arguments cannot be null or empty."); - } boolean isSuccessful = false; - Matcher matcher = APP_CENTER_ENDPOINT_PATTERN.matcher(from); + Matcher matcher = APP_CENTER_ENDPOINT_PATTERN.matcher(ObjectUtils.requireNonEmpty(from)); if (!matcher.find()) { - throw new IllegalArgumentException(String.format("AppCenter url is not correct: %s%n It should be like: %s.", - from, "appcenter://appName/platformName/buildType/version")); + throw new IllegalArgumentException(String.format(INVALID_LINK_MESSAGE, from, VALID_LINK)); } try { - getBuild(to.toFile().getAbsolutePath(), + getBuild(Objects.requireNonNull(to).toFile().getAbsolutePath(), matcher.group(APP_NAME), matcher.group(PLATFORM_NAME), matcher.group(BUILD_TYPE), matcher.group(APP_VERSION)); isSuccessful = true; } catch (Exception e) { - LOGGER.error("Something went wrong when try to download application from AppCenter.", e); + LOGGER.error("Cannot download application from AppCenter. Message: " + e.getMessage(), e); } return isSuccessful; } @@ -136,32 +257,22 @@ public boolean delete(String url) { @Override public String getDirectLink(String url) { - if (Objects.isNull(url) || url.isEmpty()) { - throw new IllegalArgumentException("Argument cannot be null or empty."); - } - Matcher matcher = APP_CENTER_ENDPOINT_PATTERN.matcher(url); - if (!matcher.find()) { - throw new IllegalArgumentException(String.format("AppCenter url is not correct: %s%n It should be like: %s.", - url, "appcenter://appName/platformName/buildType/version")); - } - return getDownloadUrl(matcher.group(APP_NAME), matcher.group(PLATFORM_NAME), matcher.group(BUILD_TYPE), matcher.group(APP_VERSION)); + return getAppInfo(url).getDirectLink(); } /** - * - * @param folder to which upload build artifact. - * @param appName takes in the AppCenter Name to look for. + * @param folder to which upload build artifact. + * @param appName takes in the AppCenter Name to look for. * @param platformName takes in the platform we wish to download for. - * @param buildType takes in the particular build to download (i.e. Prod.AdHoc, QA.Debug, Prod-Release, QA-Internal etc...) - * @param version takes in either "latest" to take the first build that matches the criteria or allows to consume a version to download that - * build. + * @param buildType takes in the particular build to download (i.e. Prod.AdHoc, QA.Debug, Prod-Release, QA-Internal etc...) + * @param version takes in either "latest" to take the first build that matches the criteria or allows to consume a version to download that + * build. * @return file to the downloaded build artifact */ public File getBuild(String folder, String appName, String platformName, String buildType, String version) { - String buildToDownload = getDownloadUrl(appName, platformName, buildType, version); - + AppCenterApp appCenterApp = getAppInfo(appName, platformName, buildType, version); //TODO: wrap below code into the public download method - String fileName = FilenameUtils.concat(folder, createFileName(appName, buildType, platformName)); + String fileName = FilenameUtils.concat(folder, createFileName(appName, buildType, platformName, appCenterApp)); File fileToLocate = null; try { @@ -184,7 +295,7 @@ public File getBuild(String folder, String appName, String platformName, String if (fileToLocate == null) { try { LOGGER.debug("Beginning Transfer of AppCenter Build"); - URL downloadLink = new URL(buildToDownload); + URL downloadLink = new URL(appCenterApp.getDirectLink()); int retryCount = 0; boolean retry = true; while (retry && retryCount <= 5) { @@ -203,13 +314,12 @@ public File getBuild(String folder, String appName, String platformName, String } /** - * - * @param fileName will be the name of the downloaded file. + * @param fileName will be the name of the downloaded file. * @param downloadLink will be the URL to retrieve the build from. * @return brings back a true/false on whether or not the build was successfully downloaded. * @throws IOException throws a non Interruption Exception up. */ - private boolean downloadBuild(String fileName, URL downloadLink) throws IOException { + private static boolean downloadBuild(String fileName, URL downloadLink) throws IOException { try (ReadableByteChannel readableByteChannel = Channels.newChannel(downloadLink.openStream()); FileOutputStream fos = new FileOutputStream(fileName)) { if (Thread.currentThread().isInterrupted()) { @@ -226,130 +336,48 @@ private boolean downloadBuild(String fileName, URL downloadLink) throws IOExcept } } - /** - * - * @param appName takes in the AppCenter Name to look for. - * @param platformName takes in the platform we wish to download for. - * @return Map<String, String> - */ - private Map getAppId(String appName, String platformName) { - Map appMap = new HashMap<>(); - - for (App node : new AccountApi(apiClient).appsList(null)) { - if (platformName.equalsIgnoreCase(node.getOs().toString()) && node.getName().toLowerCase().contains(appName.toLowerCase())) { - ownerName = node.getOwner().getName(); - String app = node.getName(); - LOGGER.info("Found Owner: {} App: {}", ownerName, app); - appMap.put(app, getLatestBuildDate(app, node.getUpdatedAt())); - } - } - - if (!appMap.isEmpty()) { - return appMap.entrySet() - .stream() - .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) - .collect(Collectors.toMap( - Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); - } - throw new NotFoundException(String.format("Application Not Found in AppCenter for Organization (%s) Name (%s), Platform (%s)", ownerName, appName, platformName)); - } - - /** - * - * @param apps takes in the application Ids - * @param buildType takes in the particular build to download (i.e. Prod.AdHoc, QA.Debug, Prod-Release, QA-Internal etc...) - * @param version takes in either "latest" to take the first build that matches the criteria or allows to consume a version to download that - * build. - * @return String - */ - private String scanAppForBuild(Map apps, String buildType, String version) { - for (String currentApp : apps.keySet()) { - LOGGER.info("Scanning App {}", currentApp); - List retrieveList = new DistributeApi(apiClient).releasesList(ownerName, currentApp, true, - "tester", null, - null); - - LOGGER.debug("Available Builds JSON: {}", retrieveList); - - if (!retrieveList.isEmpty()) { - int buildLimiter = 0; - for (ReleasesAvailableToTester build : retrieveList) { - - buildLimiter += 1; - if (buildLimiter >= 50) { - break; - } - - Integer latestBuildNumber = build.getId(); - versionShort = build.getShortVersion(); - versionLong = build.getVersion(); - - ReleaseDetailsResponse appBuild = new DistributeApi(apiClient).releasesGetLatestByUser(String.valueOf(latestBuildNumber), - ownerName, currentApp, null, null); - - if (checkBuild(version, appBuild) && (checkTitleForCorrectPattern(buildType.toLowerCase(), appBuild) || checkNotesForCorrectBuild( - buildType.toLowerCase(), appBuild))) { - LOGGER.debug("Print Build Info: {}", appBuild); - LOGGER.info("Fetching Build ID ({}) Version: {} ({})", latestBuildNumber, versionShort, versionLong); - String buildUrl = appBuild.getDownloadUrl(); - LOGGER.info("Download URL For Build: {}", buildUrl); - return buildUrl; - } - } - } - } - - throw new NotFoundException(String.format("Unable to find build to download, version provided (%s)", version)); - } - /** * The updated_at field returned by AppCenter doesn't contain the "latest time" a build was updated, so we grab the first build to do our sort. * - * @param app name of the app to check. + * @param app name of the app to check. * @param appUpdatedAt passing in of a backup date value if the app we look at doesn't have a build associated to it. * @return the date value to be used in sorting. */ - private String getLatestBuildDate(String app, String appUpdatedAt) { - List retrieveList = new DistributeApi(apiClient).releasesList(ownerName, app, true, "tester", null, - null); + private String getLatestBuildDate(String app, String appUpdatedAt, String ownerName) { + List retrieveList = new DistributeApi(apiClient).releasesList(ownerName, app, + true, "tester", null, null); if (!retrieveList.isEmpty()) { return retrieveList.get(0).getUploadedAt(); } return appUpdatedAt; } - private boolean checkBuild(String version, ReleaseDetailsResponse node) { - + private static boolean checkBuild(String version, ReleaseDetailsResponse node) { if ("latest".equalsIgnoreCase(version)) { return true; } - return version.equalsIgnoreCase( node.getShortVersion() + "." + node.getVersion()) || version.equalsIgnoreCase(node.getShortVersion()); } - private String createFileName(String appName, String buildType, String platformName) { - - String fileName = String.format("%s.%s.%s.%s", appName, buildType, versionShort, versionLong) + private static String createFileName(String appName, String buildType, String platformName, AppCenterApp appCenterApp) { + String fileName = String.format("%s.%s.%s.%s", appName, buildType, appCenterApp.getVersion(), appCenterApp.getBuild()) .replace(" ", ""); - if (platformName.toLowerCase().contains("ios")) { return fileName + ".ipa"; } return fileName + ".apk"; } - private boolean checkNotesForCorrectBuild(String pattern, ReleaseDetailsResponse node) { + private static boolean checkNotesForCorrectBuild(String pattern, ReleaseDetailsResponse node) { LOGGER.debug("\nPattern to be checked: {}", pattern); - - String nodeField = node.getReleaseNotes().toLowerCase(); + String nodeField = StringUtils.lowerCase(node.getReleaseNotes()); String[] splitPattern = pattern.split("\\."); LinkedList segmentsFound = new LinkedList<>(); for (String segment : splitPattern) { - segmentsFound.add(nodeField.contains(segment)); + segmentsFound.add(StringUtils.contains(nodeField, segment)); } - if (!segmentsFound.isEmpty() && !segmentsFound.contains(false)) { LOGGER.debug("\nPattern match found!! This is the buildType to be used: {}", nodeField); return true; @@ -358,37 +386,26 @@ private boolean checkNotesForCorrectBuild(String pattern, ReleaseDetailsResponse return !pattern.isEmpty() && scanningAllNotes(String.format(patternToReplace, pattern), nodeField); } - private boolean checkTitleForCorrectPattern(String pattern, ReleaseDetailsResponse node) { + private static boolean checkTitleForCorrectPattern(String pattern, ReleaseDetailsResponse node) { LOGGER.debug("\nPattern to be checked: {}", pattern); - String nodeField = node.getAppName().toLowerCase(); String[] splitPattern = pattern.split("\\."); LinkedList segmentsFound = new LinkedList<>(); for (String segment : splitPattern) { segmentsFound.add(nodeField.contains(segment)); } - if (!segmentsFound.isEmpty() && !segmentsFound.contains(false)) { LOGGER.debug("\nPattern match found!! This is the buildType to be used: {}", nodeField); return true; } String patternToReplace = ".*[ ->\\S]%s[ -<\\S].*"; - return !pattern.isEmpty() && scanningAllNotes(String.format(patternToReplace, pattern), nodeField); - + return !pattern.isEmpty() && + scanningAllNotes(String.format(patternToReplace, pattern), nodeField); } - private boolean searchFieldsForString(String pattern, String stringToSearch) { - Pattern p = Pattern.compile(pattern); - Matcher m = p.matcher(stringToSearch); - - return m.find(); - } - - private boolean scanningAllNotes(String pattern, String noteField) { - boolean foundMessages = false; - - foundMessages = searchFieldsForString(pattern, noteField); - - return foundMessages; + private static boolean scanningAllNotes(String pattern, String noteField) { + return Pattern.compile(pattern) + .matcher(noteField) + .find(); } } diff --git a/src/main/java/com/zebrunner/carina/commons/artifact/IAppInfo.java b/src/main/java/com/zebrunner/carina/commons/artifact/IAppInfo.java new file mode 100644 index 0000000..0451d1c --- /dev/null +++ b/src/main/java/com/zebrunner/carina/commons/artifact/IAppInfo.java @@ -0,0 +1,17 @@ +package com.zebrunner.carina.commons.artifact; + +// move to the carina-commons repo +public interface IAppInfo { + + String getDirectLink(); + + default String getVersion() { + throw new UnsupportedOperationException(); + + } + + default String getBuild() { + throw new UnsupportedOperationException(); + } + +}