Skip to content

Commit

Permalink
Notify the administrators when one of the installed licensed applica…
Browse files Browse the repository at this point in the history
…tions has a new version available #110 (#126)

* add NewExtensionVersionAvailable event and a job scheduler to send this event every week
* avoid multiplying notifications by adding an object of NewVersionNotificationClass each time a new notification is fired
* add possibility for users to access directly the upgrade page corresponding to a given notification
* update unit tests
  • Loading branch information
oanalavinia authored Aug 29, 2022
1 parent ca84155 commit 53cf696
Show file tree
Hide file tree
Showing 20 changed files with 1,420 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<xwiki.extension.category>API</xwiki.extension.category>
<xwiki.extension.namespaces>{root}</xwiki.extension.namespaces>
<checkstyle.suppressions.location>${basedir}/src/main/checkstyle/checkstyle-suppressions.xml</checkstyle.suppressions.location>
<xwiki.jacoco.instructionRatio>0.48</xwiki.jacoco.instructionRatio>
<xwiki.jacoco.instructionRatio>0.50</xwiki.jacoco.instructionRatio>
</properties>
<dependencies>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,21 +93,12 @@ public File getLocalStorePath()
}

@Override
@SuppressWarnings("unchecked")
public List<String> getAutoUpgradeAllowList()
{
// Since you cannot pass a default value and a target type to getProperty, the class of defaultValue is used
// for converting the result. In this case there is no converter for EmptyList, so we manage the result
// manually.
Object allowlist = this.automaticUpgradesConfig.getProperty("allowlist");
if (allowlist instanceof List) {
return ((List<Object>) allowlist).stream().map(item -> Objects.toString(item, null))
.collect(Collectors.toList());
} else if (allowlist == null) {
return Collections.emptyList();
} else {
throw new RuntimeException(String.format("Cannot convert [%s] to List", allowlist));
}
return convertObjectToStringList(this.automaticUpgradesConfig.getProperty("allowlist"));
}

@Override
Expand Down Expand Up @@ -140,4 +131,16 @@ public String getLicensingOwnerEmail()
return this.ownerConfig.getProperty("email");
}

@SuppressWarnings("unchecked")
private List<String> convertObjectToStringList(Object list)
{
if (list instanceof List) {
return ((List<Object>) list).stream().map(item -> Objects.toString(item, null))
.collect(Collectors.toList());
} else if (list == null) {
return Collections.emptyList();
} else {
throw new RuntimeException(String.format("Cannot convert [%s] to List", list));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ public class AutomaticUpgradesConfigurationSource extends AbstractDocumentConfig
{
private static final List<String> CODE_SPACE = Arrays.asList("Licenses", "Code");

/**
* Reference of the document containing licensing configurations.
*/
protected static final LocalDocumentReference LICENSING_CONFIG_DOC =
new LocalDocumentReference(CODE_SPACE, "LicensingConfig");

/**
* Reference of the class that contains configurations related to automatic upgrades.
*/
protected static final LocalDocumentReference AUTO_UPGRADES_CLASS =
new LocalDocumentReference(CODE_SPACE, "AutomaticUpgradesClass");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@
import com.xpn.xwiki.plugin.scheduler.SchedulerPlugin;

/**
* Ensure that LicensedExtensionUpgradeJob is scheduled after licensing install. Reschedule LicensedExtensionUpgradeJob
* to work around https://jira.xwiki.org/browse/XWIKI-14494. The unschedule / schedule process should be removed once
* the issue is fixed and licensing depends on a version of XWiki >= the version where is fixed.
* Ensure that {@link LicensedExtensionUpgradeJob} and {@link NewExtensionVersionAvailableJob} are scheduled after
* licensing install. Reschedule {@link LicensedExtensionUpgradeJob} and {@link NewExtensionVersionAvailableJob} to work
* around XWIKI-14494: Java scheduler job coming from an extension is not rescheduled when the extension is upgraded.
* The unschedule / schedule process should be removed once the issue is fixed and licensing depends on a version of
* XWiki >= the version where is fixed.
*
* @since 1.17
* @version $Id$
Expand All @@ -71,8 +73,13 @@ public class LicensingSchedulerListener extends AbstractEventListener implements
*/
protected static final String LICENSOR_API_ID = "com.xwiki.licensing:application-licensing-licensor-api";

protected static final LocalDocumentReference JOB_DOC =
new LocalDocumentReference(Arrays.asList("Licenses", "Code"), "LicensedExtensionUpgradeJob");
protected static final List<String> CODE_SPACE = Arrays.asList("Licenses", "Code");

protected static final LocalDocumentReference EXTENSION_UPGRADE_JOB_DOC =
new LocalDocumentReference(CODE_SPACE, "LicensedExtensionUpgradeJob");

protected static final LocalDocumentReference NEW_VERSION_JOB_DOC =
new LocalDocumentReference(CODE_SPACE, "NewExtensionVersionAvailableJob");

private static final List<Event> EVENTS = Arrays.asList(new ExtensionInstalledEvent());

Expand Down Expand Up @@ -113,7 +120,8 @@ public void initialize() throws InitializationException
try {
// Don't trigger the rescheduling process at xwiki startup time.
if (this.contextProvider.get() != null) {
scheduleAutomaticUpgradesJob(true);
scheduleJob(true, EXTENSION_UPGRADE_JOB_DOC);
scheduleJob(true, NEW_VERSION_JOB_DOC);
}
} catch (XWikiException | SchedulerException e) {
throw new InitializationException("Error while rescheduling LicensedExtensionUpgradeJob", e);
Expand All @@ -127,20 +135,22 @@ public void onEvent(Event event, Object source, Object data)

if (event instanceof ExtensionInstalledEvent && extensionId.equals(LICENSOR_API_ID)) {
try {
scheduleAutomaticUpgradesJob(false);
scheduleJob(false, EXTENSION_UPGRADE_JOB_DOC);
scheduleJob(false, NEW_VERSION_JOB_DOC);
} catch (XWikiException | SchedulerException e) {
throw new RuntimeException("Error while scheduling LicensedExtensionUpgradeJob after licensing install",
e);
}
}
}

protected void scheduleAutomaticUpgradesJob(boolean doReschedule) throws XWikiException, SchedulerException
protected void scheduleJob(boolean doReschedule, LocalDocumentReference jobDocReference)
throws XWikiException, SchedulerException
{
XWikiContext xcontext = contextProvider.get();

SchedulerPlugin scheduler = (SchedulerPlugin) xcontext.getWiki().getPluginManager().getPlugin("scheduler");
XWikiDocument jobDoc = xcontext.getWiki().getDocument(JOB_DOC, xcontext);
XWikiDocument jobDoc = xcontext.getWiki().getDocument(jobDocReference, xcontext);
BaseObject job = jobDoc.getXObject(SchedulerPlugin.XWIKI_JOB_CLASSREFERENCE);
JobState jobState = scheduler.getJobStatus(job, xcontext);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This 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 this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.xwiki.licensing.internal.upgrades;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import com.xpn.xwiki.plugin.scheduler.AbstractJob;
import com.xpn.xwiki.web.Utils;

/**
* Scheduler job that sends a notification when a new version is available for a licensed extension.
*
* @since 1.23
* @version $Id$
*/
public class NewExtensionVersionAvailableJob extends AbstractJob implements Job
{
@SuppressWarnings("deprecation")
@Override
protected void executeJob(JobExecutionContext jobContext) throws JobExecutionException
{
NewExtensionVersionAvailableManager newVersionManager =
Utils.getComponent(NewExtensionVersionAvailableManager.class);
newVersionManager.checkLicensedExtensionsAvailableVersions();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This 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 2.1 of
* the License, or (at your option) any later version.
*
* This 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 this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package com.xwiki.licensing.internal.upgrades;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.extension.ExtensionId;
import org.xwiki.extension.InstalledExtension;
import org.xwiki.extension.repository.InstalledExtensionRepository;
import org.xwiki.extension.version.Version;
import org.xwiki.observation.ObservationManager;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xwiki.licensing.LicensedExtensionManager;
import com.xwiki.licensing.LicensingConfiguration;
import com.xwiki.licensing.internal.upgrades.notifications.newVersion.NewExtensionVersionAvailableEvent;

/**
* Check licensed extensions for new available versions and send a notification, without sending multiple notifications
* for the same version.
*
* @version $Id$
* @since 1.23
*/
@Component(roles = NewExtensionVersionAvailableManager.class)
@Singleton
public class NewExtensionVersionAvailableManager
{
@Inject
private InstalledExtensionRepository installedRepository;

@Inject
private UpgradeExtensionHandler upgradeExtensionHandler;

@Inject
private LicensedExtensionManager licensedExtensionManager;

@Inject
private ObservationManager observationManager;

@Inject
private LicensingConfiguration licensingConfig;

@Inject
private Logger logger;

@Inject
private NewVersionNotificationManager newVersionNotificationManager;

/**
* Notify the administrators when one of the installed licensed applications has a new version available. Do nothing
* for extensions that have auto upgrades enabled.
*/
public void checkLicensedExtensionsAvailableVersions()
{
List<String> allowlist = licensingConfig.getAutoUpgradeAllowList();

for (ExtensionId extensionId : licensedExtensionManager.getLicensedExtensions()) {
if (allowlist.contains(extensionId.getId())) {
continue;
}

InstalledExtension installedExtension = installedRepository.getInstalledExtension(extensionId);
Collection<String> namespaces = installedExtension.getNamespaces();
if (namespaces == null) {
notifyExtensionVersionAvailable(installedExtension.getId(), null);
} else {
for (String namespace : installedExtension.getNamespaces()) {
notifyExtensionVersionAvailable(installedExtension.getId(), namespace);
}
}
}
}

private void notifyExtensionVersionAvailable(ExtensionId extensionId, String namespace)
{
InstalledExtension installedExtension =
installedRepository.getInstalledExtension(extensionId.getId(), namespace);
// Get the list of versions that can be installed, with the first one being the most recent.
List<Version> installableVersions = upgradeExtensionHandler.getInstallableVersions(installedExtension.getId());
if (installableVersions.isEmpty()) {
return;
}

try {
String namespaceName = namespace != null ? namespace : "root";
if (!this.newVersionNotificationManager.isNotificationAlreadySent(extensionId.getId(), namespaceName,
installableVersions.get(0).getValue()))
{
Map<String, String> extensionInfo = new HashMap<>();
extensionInfo.put("extensionName", installedExtension.getName());
extensionInfo.put("namespace", namespaceName);
extensionInfo.put("version", installableVersions.get(0).getValue());

this.observationManager.notify(new NewExtensionVersionAvailableEvent(
new ExtensionId(extensionId.getId(), installableVersions.get(0)), namespace),
extensionId.getId(), (new ObjectMapper()).writeValueAsString(extensionInfo));
this.newVersionNotificationManager.markNotificationAsSent(extensionId.getId(), namespaceName,
installableVersions.get(0).getValue());
}
} catch (JsonProcessingException e) {
this.logger.warn("Failed to send a NewExtensionVersionAvailableEvent for [{}]. Root cause is [{}]",
extensionId.getId(), ExceptionUtils.getRootCauseMessage(e));
}
}
}
Loading

0 comments on commit 53cf696

Please sign in to comment.