AddonService.groovy
/*
* Copyright (C) 2003-2014 eXo Platform SAS.
*
* This file is part of eXo Platform - Add-ons Manager.
*
* eXo Platform - Add-ons Manager is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* eXo Platform - Add-ons Manager software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with eXo Platform - Add-ons Manager; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see <http://www.gnu.org/licenses/>.
*/
package org.exoplatform.platform.am
import groovy.json.JsonSlurper
import groovy.time.TimeCategory
import groovy.transform.Canonical
import groovy.util.slurpersupport.GPathResult
import groovy.xml.StreamingMarkupBuilder
import groovy.xml.XmlUtil
import org.eclipse.aether.util.version.GenericVersionScheme
import org.eclipse.aether.version.VersionScheme
import org.exoplatform.platform.am.ex.AddonNotFoundException
import org.exoplatform.platform.am.ex.CompatibilityException
import org.exoplatform.platform.am.ex.InvalidJSONException
import org.exoplatform.platform.am.ex.UnknownErrorException
import org.exoplatform.platform.am.settings.EnvironmentSettings
import org.exoplatform.platform.am.settings.PlatformSettings
import org.exoplatform.platform.am.utils.FileUtils
import org.exoplatform.platform.am.utils.Logger
import java.security.MessageDigest
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import static org.exoplatform.platform.am.AddonService.ParsingErrorType.INVALID_ENTRY
import static org.exoplatform.platform.am.AddonService.ParsingErrorType.MALFORMED_ENTRY
import static org.exoplatform.platform.am.utils.FileUtils.copyFile
import static org.exoplatform.platform.am.utils.FileUtils.downloadFile
/**
* All services related to add-ons
* @author Arnaud Héritier <aheritier@exoplatform.com>
*/
class AddonService {
/**
* Logger
*/
private static final Logger LOG = Logger.getInstance()
/**
* The identifier used in the catalog for the add-ons manager
*/
private static final String ADDONS_MANAGER_CATALOG_ID = "exo-addons-manager"
private static final STATUS_FILE_EXT = ".status"
/**
* Version Scheme to compare/sort versions
*/
private static final VersionScheme VERSION_SCHEME = new GenericVersionScheme()
/**
* Singleton
*/
private static final AddonService singleton = new AddonService()
/**
* Factory
* @return The {@link AddonService} singleton instance
*/
static AddonService getInstance() {
return singleton
}
/**
* You should use the singleton
*/
private AddonService() {
}
/**
* Load add-ons from local and remote catalogs
* @param env The execution environment
* @param alternateCatalog The alternate remote catalog URL
* @param noCache If the 1h cache must be used for the remote catalog
* @param offline If the operation must be done offline (nothing will be downloaded)
* @param allowSnapshot allow add-ons with snapshot version
* @param allowUnstable allow add-ons with unstable version
* @return a list of add-ons
*/
protected List<Addon> loadAddons(
EnvironmentSettings env,
URL alternateCatalog,
Boolean noCache,
Boolean offline,
Boolean allowSnapshot,
Boolean allowUnstable) {
URL remoteCatalogUrl = alternateCatalog ?: env.remoteCatalogUrl
List<Addon> allAddons = mergeCatalogs(
loadAddonsFromUrl(remoteCatalogUrl, noCache, offline, env.catalogsCacheDirectory),
loadAddonsFromFile(env.localAddonsCatalogFile)
)
// check for new addons manager version availability
def allStableAddons = filterAddonsByVersion(allAddons, true, false, false)
Addon newerAddonManager = findAddonsNewerThan(
new Addon(id: ADDONS_MANAGER_CATALOG_ID, version: env.manager.version),
filterCompatibleAddons(allStableAddons, env.platform))?.max()
if (newerAddonManager) {
LOG.info(
"New Add-ons Manager version @|yellow,bold ${newerAddonManager.version}|@ found. It will be automatically updated " +
"after its restart.")
// Backup the current library
File backupDirectory = new File(env.versionsDirectory, env.manager.version)
if (!backupDirectory.exists()) {
FileUtils.mkdirs(backupDirectory)
}
copyFile("Backing up current add-ons manager library", new File(env.addonsDirectory, "addons-manager.jar"), new File(backupDirectory, "addons-manager.jar"), false)
// Let's download the new one
File newAddonsManagerArchive = new File(env.archivesDirectory, "${newerAddonManager.id}-${newerAddonManager.version}.zip")
FileUtils.downloadFile("Downloading Add-ons Manager version @|yellow,bold ${newerAddonManager.version}|@", newerAddonManager.downloadUrl, newAddonsManagerArchive)
LOG.withStatus("Extracting Add-ons Manager version @|yellow,bold ${newerAddonManager.version}|@") {
ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(newAddonsManagerArchive))
zipInputStream.withStream {
ZipEntry entry
while (entry = zipInputStream.nextEntry) {
if (entry.name == "addons/addons-manager.jar") {
FileOutputStream output = new FileOutputStream(new File(env.addonsDirectory, "addons-manager.jar.new"))
output.withStream {
int len = 0;
byte[] buffer = new byte[4096]
while ((len = zipInputStream.read(buffer)) > 0) {
output.write(buffer, 0, len);
}
}
}
}
}
}
}
return filterAddonsByVersion(
allAddons.findAll { !ADDONS_MANAGER_CATALOG_ID.equals(it.id) },
true,
allowUnstable,
allowSnapshot)
}
/**
* Load add-ons list from a remote Url (JSON formatted)
* @param catalogUrl The remote catalog URL
* @param noCache If the 1h cache must be used for the remote catalog
* @param offline If the operation must be done offline (nothing will be downloaded)
* @param catalogCacheDir The directory where are stored catalogs caches
* @return a list of Add-ons
*/
protected List<Addon> loadAddonsFromUrl(
URL catalogUrl,
Boolean noCache,
Boolean offline,
File catalogCacheDir) {
List<Addon> addons = new ArrayList<Addon>()
String catalogContent
File catalogCacheFile = new File(catalogCacheDir, "${convertUrlToFilename(catalogUrl)}.json");
LOG.debug("Catalog cache file for ${catalogUrl} : ${catalogCacheFile}")
if (noCache && catalogCacheFile.exists()) {
// AM-102 : Let's drop the catalog cache with --no-cache
catalogCacheFile.delete()
}
// If there is no local cache of the remote catalog or if it is older than 1h
use([TimeCategory]) {
if ((!catalogCacheFile.exists() || new Date(catalogCacheFile.lastModified()) < 1.hours.ago) && !offline) {
// Load the remote list
File tempFile
try {
// Create a temporary file in which we will download the remote catalog
tempFile = File.createTempFile("addons-manager-remote-catalog", ".json", catalogCacheDir)
// Don't forget to always delete it even in case of error
tempFile.deleteOnExit()
// Download the remote catalog
downloadFile("Downloading catalog ${catalogUrl}", catalogUrl, tempFile)
// Read the catalog content
catalogContent = tempFile.text
} catch (FileNotFoundException fne) {
throw new UnknownErrorException("Catalog ${catalogUrl} not found", fne)
}
try {
addons.addAll(createAddonsFromJsonText(catalogContent))
// Everything was ok, let's store the cache
copyFile("Updating cache for catalog ${catalogUrl}", tempFile, catalogCacheFile, false)
} catch (groovy.json.JsonException je) {
throw new InvalidJSONException("Invalid JSON content from URL : ${catalogUrl}", je)
} finally {
// Delete the temp file
tempFile.delete()
}
} else {
if (catalogCacheFile.exists()) {
// Let's load add-ons from the cache
LOG.withStatus("Reading catalog cache for ${catalogUrl}") {
catalogContent = catalogCacheFile.text
}
try {
addons.addAll(createAddonsFromJsonText(catalogContent))
} catch (groovy.json.JsonException je) {
catalogCacheFile.delete()
throw new InvalidJSONException("Invalid JSON content in cache file : ${catalogCacheFile.name}. Deleting it.", je)
}
} else {
LOG.warn("No remote catalog cache and offline mode activated")
}
}
}
return addons
}
/**
* Load add-ons list from a local file (JSON formatted)
* @param catalogFile The catalog file to read
* @return a list of Add-ons. Empty if the file doesn't exist.
*/
protected List<Addon> loadAddonsFromFile(
File catalogFile) {
List<Addon> addons = new ArrayList<Addon>()
String catalogContent
if (catalogFile.exists()) {
LOG.debug("Loading catalog from ${catalogFile}")
LOG.withStatus("Reading catalog ${catalogFile.name}") {
catalogContent = catalogFile.text
}
try {
addons.addAll(createAddonsFromJsonText(catalogContent))
} catch (groovy.json.JsonException je) {
throw new InvalidJSONException("Invalid JSON content in file : ${catalogFile}", je)
}
} else {
LOG.debug("No local catalog to load from ${catalogFile}")
}
return addons
}
/**
* Returns the list of add-ons installed in the current environment @{code env}.
* @param env The environment where the add-on
* must be uninstalled
* @return A list of @{link Addon}
*/
protected List<Addon> getInstalledAddons(
EnvironmentSettings env) {
List<Addon> result = new ArrayList<>();
env.statusesDirectory.list(
{ dir, file ->
file ==~ /.*?\${AddonService.STATUS_FILE_EXT}/
} as FilenameFilter
).toList().each { statusFile ->
try {
LOG.withStatus("Loading add-on details from ${statusFile}") {
result << createAddonFromJsonText(new File(env.statusesDirectory, statusFile).text)
}
} catch (InvalidJSONException ije) {
LOG.debug(ije)
LOG.warn("${statusFile} isn't readable")
}
}
return result
}
/**
* Returns the list of outdated add-ons by comparing the list of @{code installedAddons} with the one of
* @{code availableAddons}.
* @param installedAddons The list of installed add-ons
* @param availableAddons The list of available add-ons
* @return The list of outdated add-ons
*/
protected List<Addon> getOutdatedAddons(
List<Addon> installedAddons,
List<Addon> availableAddons) {
return installedAddons.findAll { installedAddon ->
findAddonsNewerThan(installedAddon, availableAddons).size() > 0
}
}
/**
* Find in the @{code addons} list the one with the current @{code addonId} and @{code addonVersion}. If
* @{code addonVersion} isn't set it will find the more recent version (stable per default excepted if @{code allowUnstable},
* or @{code allowSnapshot} are set.
* @param addons The list of add-ons in wich to do the search
* @param addonId The Identifier of the add-on to find
* @param addonVersion The version of the add-on to find
* @param allowSnapshot allows add-ons with snapshot version Allow to retrieve a snapshot version if it is the most recent and
* @{code addonVersion} isn't set
* @param allowUnstable allows add-ons with snapshot version Allow to retrieve an unstable version if it is the most recent and
* @{code addonVersion} isn't set
* @return the add-on or null if not found
*/
protected Addon findAddon(
final List<Addon> addons,
final String addonId,
final String addonVersion,
final Boolean allowSnapshots,
final Boolean allowUnstable
) {
// Let's find the add-on with the given id and version
Addon result
if (addonVersion == null) {
// No version specified thus we need to find the newer version available
// Let's find the first add-on with the given id (including or not snapshots depending of the option)
result = findNewestAddon(addonId,
filterAddonsByVersion(addons, true, allowUnstable, allowSnapshots))
if (result == null) {
if (!addons.find { it.id == addonId }) {
throw new AddonNotFoundException(addonId)
} else {
// Let's try to find an unstable version of the addon
if (!allowUnstable && findNewestAddon(addonId,
filterAddonsByVersion(addons, true, true, allowSnapshots))) {
LOG.error(
"This add-on exists but doesn't have a stable released version yet! add --unstable option to use an unstable version")
}
// Let's try to find a snapshot version of the add-on
if (!allowSnapshots && findNewestAddon(addonId,
filterAddonsByVersion(addons, true, allowUnstable, true))) {
LOG.error(
"This add-on exists but doesn't have a stable released version yet! add --snapshots option to use a development version")
}
throw new AddonNotFoundException(addonId)
}
}
} else {
result = addons.find { it.id == addonId && it.version == addonVersion }
if (result == null) {
if (!addons.find { it.id == addonId }) {
throw new AddonNotFoundException(addonId)
} else {
List<Addon> stableAddons = filterAddonsByVersion(addons.findAll { it.id == addonId }, true, false, false)
if (!stableAddons.empty) {
LOG.error "Stable version(s) available for add-on ${addonId} : ${stableAddons.sort().reverse().collect { it.version }.join(', ')}"
}
List<Addon> unstableAddons = filterAddonsByVersion(addons.findAll { it.id == addonId }, false, true, false)
if (!unstableAddons.empty) {
LOG.error "Unstable version(s) available for add-on ${addonId} : ${unstableAddons.sort().reverse().collect { it.version }.join(', ')}"
}
List<Addon> snapshotAddons = filterAddonsByVersion(addons.findAll { it.id == addonId }, false, false, true)
if (!snapshotAddons.empty) {
LOG.error "Development version(s) available for add-on ${addonId} : ${snapshotAddons.sort().reverse().collect { it.version }.join(', ')}"
}
throw new AddonNotFoundException(addonId, addonVersion)
}
}
}
return result
}
/**
* Find in the list {@code addons} all add-ons with the same identifier {@link Addon#id} and a higher version number
* {@link Addon#version} than {@code addonRef}
* @param addonRef The add-on reference
* @param addons The list to filter
* @return A list of add-ons
*/
protected List<Addon> findAddonsNewerThan(
Addon addonRef,
List<Addon> addons) {
assert addonRef
assert addonRef.id
assert addonRef.version
return addons.findAll { it.id == addonRef.id && it > addonRef }
}
/**
* Find in the list {@code addons} the add-on with the identifier {@code addonId} and the highest version number
* @param addonId The add-on identifier
* @param addons The list to filter
* @return The add-on matching constraints or null if none.
*/
protected Addon findNewestAddon(
String addonId,
List<Addon> addons) {
assert addonId
return addons.findAll { it.id == addonId }.max()
}
/**
* Filter entries in {@code addons} to keep only versions matching some criteria.
* @param addons The list of add-ons to filter
* @param allowStable Return add-ons with stable versions
* @param allowUnstable Return add-ons with unstable versions (alpha, beta, RC, ...)
* @param allowSnapshot Return add-ons with snapshot versions (-SNAPSHOT)
* @return the filtered list of add-ons.
*/
protected List<Addon> filterAddonsByVersion(
List<Addon> addons,
Boolean allowStable,
Boolean allowUnstable,
Boolean allowSnapshot
) {
return addons.findAll {
!it.unstable && !it.isSnapshot() && allowStable ||
it.unstable && !it.isSnapshot() && allowUnstable ||
it.isSnapshot() && allowSnapshot
}
}
/**
* Returns all add-ons supporting a platform instance (distributionType, appServerType, version)
* @param addons The catalog to filter entries
* @param plfSettings The settings about the platform instance
* @return the filtered list
*/
protected List<Addon> filterCompatibleAddons(
final List<Addon> addons,
PlatformSettings plfSettings) {
return addons.findAll {
isCompatible(it, plfSettings)
}
}
/**
* Verify if an add-on is compatible with the platform instance (distributionType, appServerType, version)
* @param addon The add-on to verify
* @param plfSettings The settings about the platform instance
* @throws CompatibilityException if the add-on isn't compatible
*/
protected void validateCompatibility(Addon addon, PlatformSettings plfSettings) throws CompatibilityException {
LOG.withStatus("Checking compatibility of the add-on with your eXo platform instance") {
if (!isCompatible(addon, plfSettings)) {
throw new CompatibilityException(addon, plfSettings)
}
}
}
/**
* Verify if an add-on is compatible with the platform instance (distributionType, appServerType, version)
* @param addon The add-on to verify
* @param plfSettings The settings about the platform instance
* @return true is the add-on is compatible
*/
protected Boolean isCompatible(Addon addon, PlatformSettings plfSettings) {
return addon.supportedDistributions.contains(plfSettings.distributionType) &&
testAppServerTypeCompatibility(plfSettings.appServerType, addon.supportedApplicationServers) &&
testVersionCompatibility(plfSettings.version, addon.compatibility)
}
/**
* [AM_CAT_07] At merge, de-duplication of add-on entries of the local and remote catalogs is
* done using ID, Version, Distributions, Application Servers as the identifier.
* In case of duplication, the remote entry takes precedence
* TODO : Only ID+Version are used in comparison. It should take care of Distributions, Application Servers.
* @param remoteCatalog
* @param localCatalog
* @return a list of add-ons
*/
protected List<Addon> mergeCatalogs(
final List<Addon> remoteCatalog,
final List<Addon> localCatalog) {
// Let's initiate a new list from the remote catalog content
List<Addon> mergedCatalog = remoteCatalog.clone()
if (localCatalog) {
List<Addon> duplicatedEntries = new ArrayList<>();
// Let's add entries from the local catalog which aren't already in the catalog (based on id+version identifiers)
LOG.withStatus("Merging local and remote catalogs") {
localCatalog.each {
if (!mergedCatalog.contains(it)) {
mergedCatalog.add(it)
} else {
duplicatedEntries.add(it)
}
}
}
if (duplicatedEntries) {
duplicatedEntries.each {
LOG.error("Ignored invalid entry ${it.id}:${it.version} in local catalog: already existing in remote catalog")
}
}
}
return mergedCatalog
}
/**
* Parse a JSON String representing an Add-on to build an {@link Addon} object
* @param text the JSON text to parse
* @return an Addon object
* @throws InvalidJSONException if there is at least one error while reading an add-on
*/
protected Addon createAddonFromJsonText(String text) throws InvalidJSONException {
Addon result
ParsingErrors errors = new ParsingErrors();
try {
result = createAddonFromJsonObject(new JsonSlurper().parseText(text), errors)
} finally {
printMessages(errors)
}
return result
}
/**
* Loads a list of Add-on from its JSON text representation
* @param text The JSON text to parse
* @return A List of add-ons
*/
protected List<Addon> createAddonsFromJsonText(
String text) {
List<Addon> addonsList = new ArrayList<Addon>();
ParsingErrors errors = new ParsingErrors();
LOG.withStatus("Loading add-ons list") {
new JsonSlurper().parseText(text).each { anAddon ->
try {
Addon addonToAdd = createAddonFromJsonObject(anAddon, errors)
if (!addonsList.contains(addonToAdd)) {
addonsList.add(addonToAdd)
} else {
errors.addInvalid("${addonToAdd.id}:${addonToAdd.version}", "Duplicated entry")
}
} catch (InvalidJSONException ije) {
// skip it
}
}
}
printMessages(errors)
return addonsList
}
/**
* Loads an Add-on from its object representation created by the JsonSlurper
* @param anAddon An Object built from JsonSlurper
* @param errors Error messages to populate while reading
* @return an Addon or null if there are some errors
* @throws InvalidJSONException if there is at least one error while reading an add-on
*/
protected Addon createAddonFromJsonObject(
Object anAddon,
ParsingErrors errors) throws InvalidJSONException {
Addon addonObj = new Addon(
id: anAddon.id,
version: anAddon.version);
addonObj.unstable = anAddon.unstable
addonObj.name = anAddon.name
addonObj.description = anAddon.description
addonObj.releaseDate = anAddon.releaseDate
addonObj.sourceUrl = anAddon.sourceUrl
addonObj.screenshotUrl = anAddon.screenshotUrl
addonObj.thumbnailUrl = anAddon.thumbnailUrl
addonObj.documentationUrl = anAddon.documentationUrl
addonObj.downloadUrl = anAddon.downloadUrl
addonObj.vendor = anAddon.vendor
addonObj.author = anAddon.author
addonObj.authorEmail = anAddon.authorEmail
addonObj.license = anAddon.license
addonObj.licenseUrl = anAddon.licenseUrl
addonObj.mustAcceptLicense = anAddon.mustAcceptLicense
addonObj.supported = anAddon.supported
if (anAddon.supportedDistributions instanceof String) {
addonObj.supportedDistributions = anAddon.supportedDistributions.split(',').collect {
String it ->
try {
PlatformSettings.DistributionType.valueOf(it.trim().toUpperCase())
} catch (IllegalArgumentException iae) {
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Unknown distribution type <${it}>")
PlatformSettings.DistributionType.UNKNOWN
}
}
} else {
addonObj.supportedDistributions = anAddon.supportedDistributions ? anAddon.supportedDistributions.collect {
String it ->
try {
PlatformSettings.DistributionType.valueOf(it.trim().toUpperCase())
} catch (IllegalArgumentException iae) {
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Unknown distribution type <${it}>")
PlatformSettings.DistributionType.UNKNOWN
}
} : []
}
addonObj.supportedDistributions.removeAll(PlatformSettings.DistributionType.UNKNOWN)
if (anAddon.supportedApplicationServers instanceof String) {
addonObj.supportedApplicationServers = anAddon.supportedApplicationServers.split(',').collect {
String it ->
try {
PlatformSettings.AppServerType.valueOf(it.trim().toUpperCase())
}
catch (IllegalArgumentException iae) {
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Unknown application server type <${it}>")
PlatformSettings.AppServerType.UNKNOWN
}
}
} else {
addonObj.supportedApplicationServers = anAddon.supportedApplicationServers ? anAddon.supportedApplicationServers.collect {
String it ->
try {
PlatformSettings.AppServerType.valueOf(it.trim().toUpperCase())
} catch (IllegalArgumentException iae) {
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Unknown application server type <${it}>")
PlatformSettings.AppServerType.UNKNOWN
}
} : []
}
addonObj.supportedApplicationServers.removeAll(PlatformSettings.AppServerType.UNKNOWN)
addonObj.compatibility = anAddon.compatibility
addonObj.installedLibraries = anAddon.installedLibraries
addonObj.installedWebapps = anAddon.installedWebapps
addonObj.installedProperties = anAddon.installedProperties
addonObj.installedOthersFiles = anAddon.installedOthersFiles
addonObj.overwrittenFiles = anAddon.overwrittenFiles
if (!addonObj.id) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "No id")
}
if (!addonObj.version) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "No version")
}
if (!addonObj.name) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "No name")
}
if (!addonObj.downloadUrl) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "No downloadUrl")
} else {
try {
new URL(addonObj.downloadUrl)
} catch (MalformedURLException mue) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "Invalid downloadUrl <${addonObj.downloadUrl}>")
}
}
if (addonObj.sourceUrl) {
try {
new URL(addonObj.sourceUrl)
} catch (MalformedURLException mue) {
// Not critical. Just a debug error
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Invalid sourceUrl <${addonObj.sourceUrl}>")
}
}
if (addonObj.screenshotUrl) {
try {
new URL(addonObj.screenshotUrl)
} catch (MalformedURLException mue) {
// Not critical. Just a debug error
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Invalid screenshotUrl <${addonObj.screenshotUrl}>")
}
}
if (addonObj.thumbnailUrl) {
try {
new URL(addonObj.thumbnailUrl)
} catch (MalformedURLException mue) {
// Not critical. Just a debug error
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Invalid thumbnailUrl <${addonObj.thumbnailUrl}>")
}
}
if (addonObj.documentationUrl) {
try {
new URL(addonObj.documentationUrl)
} catch (MalformedURLException mue) {
// Not critical. Just a debug error
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Invalid documentationUrl <${addonObj.documentationUrl}>")
}
}
if (addonObj.licenseUrl) {
try {
new URL(addonObj.licenseUrl)
} catch (MalformedURLException mue) {
// Not critical. Just a debug error
errors.addMalformed("${addonObj.id}:${addonObj.version}", "Invalid licenseUrl <${addonObj.licenseUrl}>")
}
}
if (!addonObj.vendor) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "No vendor")
}
if (!addonObj.license) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "No license")
}
if (addonObj.supportedApplicationServers.size() == 0) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "No supportedApplicationServers")
}
if (addonObj.supportedDistributions.size() == 0) {
errors.addInvalid("${addonObj.id}:${addonObj.version}", "No supportedDistributions")
}
// Reject it only it is marked as invalid
if (errors?.findAll { String key, List<ParsingError> value ->
key == "${addonObj?.id}:${addonObj?.version}" && value?.findAll { ParsingError pe ->
pe?.type == INVALID_ENTRY
}?.size()
}?.size()) {
throw new InvalidJSONException(anAddon)
}
return addonObj
}
/**
* Displays the list of @{code errors}.
* @param errors The list of errors to display
*/
void printMessages(ParsingErrors errors) {
errors.each { id, msgs ->
if (msgs.findAll { it.type == MALFORMED_ENTRY }) {
LOG.warn(
"Malformed descriptor ${id} : ${msgs.findAll { it.type == MALFORMED_ENTRY }*.content.join(', ')}")
}
if (msgs.findAll { it.type == INVALID_ENTRY }) {
LOG.error(
"Ignored invalid entry ${id} : ${msgs.findAll { it.type == INVALID_ENTRY }*.content.join(', ')}")
}
}
}
/**
* Returns the local archive file of an add-on
* @param archivesDirectory The archives directory
* @param addon The add-on
* @return a File (existing or not)
*/
protected File getAddonLocalArchive(
File archivesDirectory,
Addon addon) {
return new File(archivesDirectory, "${addon.id}-${addon.version}.zip")
}
/**
* Returns the status File for a given add-on
* @param statusesDirectory The directory where statuses are stored
* @param addonId The identifier of the add-on to find
* @return a File (existing or not)
*/
protected File getAddonStatusFile(
File statusesDirectory,
String addonId) {
return new File(statusesDirectory, "${addonId}${STATUS_FILE_EXT}")
}
/**
* Returns the status File for a given add-on
* @param statusesDirectory The directory where statuses are stored
* @param addon The add-on to find
* @return a File (existing or not)
*/
protected File getAddonStatusFile(
File statusesDirectory,
Addon addon) {
return getAddonStatusFile(statusesDirectory, addon.id)
}
/**
* Returns the License File for a given add-on
* @param statusesDirectory The directory where statuses are stored
* @param addon The add-on to find
* @return a File (existing or not)
*/
protected File getAddonLicenseFile(
File statusesDirectory,
Addon addon) {
new File(statusesDirectory, "${addon.id}-${convertUrlToFilename(new URL(addon.licenseUrl))}.license")
}
/**
* Checks if the given add-on is installed
* @param statusesDirectory The directory where are stored status files
* @param addon The add-on to check
* @return True if the add-on is installed (thus if its status file exists)
*/
protected Boolean isAddonInstalled(
File statusesDirectory,
Addon addon) {
return getAddonStatusFile(statusesDirectory, addon).exists()
}
/**
* Serializes XML
* @param xml The XML content
* @return a String representation of the XML
*/
protected String serializeXml(
GPathResult xml) {
XmlUtil.serialize(new StreamingMarkupBuilder().bind {
mkp.yield xml
})
}
/**
* Applies a conversion on a text file
* @param file The file to change
* @param processText The conversion to apply
*/
protected void processFileInplace(
File file,
Closure processText) {
String text = file.text
file.write(processText(text))
}
/**
* Build the cache filename from the URL using a MD5 conversion
* @param catalogUrl The catalog URL
* @return The filename associated to the given URL
*/
protected String convertUrlToFilename(
URL catalogUrl) {
return new BigInteger(1, MessageDigest.getInstance("MD5").digest(catalogUrl.toString().getBytes()))
.toString(16).padLeft(32, "0").toUpperCase()
}
/**
* Test if a version is compatible with the given constraint
* @param version A version
* @param constraint A constraint (range)
* @return true if the version is compatible with the constraint
*/
protected Boolean testVersionCompatibility(
String version,
String constraint
) {
assert version
!constraint || VERSION_SCHEME.parseVersionConstraint(constraint).containsVersion(VERSION_SCHEME.parseVersion(version))
}
/**
* Test if the Application Server type is supported
* @param appServerType An application server type
* @param supportedServerType A list of supported server type
*/
protected Boolean testAppServerTypeCompatibility(
PlatformSettings.AppServerType appServerType,
List<PlatformSettings.AppServerType> supportedServerType
) {
return supportedServerType.contains(appServerType) ||
supportedServerType.contains(PlatformSettings.AppServerType.TOMCAT) && appServerType.equals(PlatformSettings.AppServerType.BITNAMI)
}
private enum ParsingErrorType {
INVALID_ENTRY, MALFORMED_ENTRY
}
/**
* An inner class used to store messages
*/
@Canonical
private class ParsingError {
ParsingErrorType type
String content
}
@Canonical
private class ParsingErrors {
@Delegate
Map<String, List<ParsingError>> errors = new TreeMap<>()
void addMalformed(String identifier, String reason) {
if (this.errors.containsKey(identifier)) {
this.errors.get(identifier) << new ParsingError(MALFORMED_ENTRY, reason)
} else {
this.errors[identifier] = [new ParsingError(MALFORMED_ENTRY, reason)]
}
}
void addInvalid(String identifier, String reason) {
if (this.errors.containsKey(identifier)) {
this.errors.get(identifier) << new ParsingError(INVALID_ENTRY, reason)
} else {
this.errors[identifier] = [new ParsingError(INVALID_ENTRY, reason)]
}
}
}
}