diff --git a/application-licensing-licensor/application-licensing-licensor-api/pom.xml b/application-licensing-licensor/application-licensing-licensor-api/pom.xml index 5639166e..bbae4a55 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/pom.xml +++ b/application-licensing-licensor/application-licensing-licensor-api/pom.xml @@ -37,7 +37,7 @@ API {root} ${basedir}/src/main/checkstyle/checkstyle-suppressions.xml - 0.48 + 0.50 diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/DefaultLicensingConfiguration.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/DefaultLicensingConfiguration.java index 2a4c0b92..8ea9afbc 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/DefaultLicensingConfiguration.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/DefaultLicensingConfiguration.java @@ -93,21 +93,12 @@ public File getLocalStorePath() } @Override - @SuppressWarnings("unchecked") public List 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) 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 @@ -140,4 +131,16 @@ public String getLicensingOwnerEmail() return this.ownerConfig.getProperty("email"); } + @SuppressWarnings("unchecked") + private List convertObjectToStringList(Object list) + { + if (list instanceof List) { + return ((List) 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)); + } + } } diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/AutomaticUpgradesConfigurationSource.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/AutomaticUpgradesConfigurationSource.java index 7a22b120..1908534f 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/AutomaticUpgradesConfigurationSource.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/AutomaticUpgradesConfigurationSource.java @@ -43,9 +43,15 @@ public class AutomaticUpgradesConfigurationSource extends AbstractDocumentConfig { private static final List 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"); diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/LicensingSchedulerListener.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/LicensingSchedulerListener.java index b273e1fd..687a7503 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/LicensingSchedulerListener.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/LicensingSchedulerListener.java @@ -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$ @@ -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 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 EVENTS = Arrays.asList(new ExtensionInstalledEvent()); @@ -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); @@ -127,7 +135,8 @@ 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); @@ -135,12 +144,13 @@ public void onEvent(Event event, Object source, Object data) } } - 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); diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableJob.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableJob.java new file mode 100644 index 00000000..d89f2191 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableJob.java @@ -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(); + } +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManager.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManager.java new file mode 100644 index 00000000..ad1ea822 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManager.java @@ -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 allowlist = licensingConfig.getAutoUpgradeAllowList(); + + for (ExtensionId extensionId : licensedExtensionManager.getLicensedExtensions()) { + if (allowlist.contains(extensionId.getId())) { + continue; + } + + InstalledExtension installedExtension = installedRepository.getInstalledExtension(extensionId); + Collection 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 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 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)); + } + } +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewVersionNotificationManager.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewVersionNotificationManager.java new file mode 100644 index 00000000..b8a04fd9 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/NewVersionNotificationManager.java @@ -0,0 +1,157 @@ +/* + * 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.Arrays; +import java.util.List; +import java.util.Objects; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.xwiki.component.annotation.Component; +import org.xwiki.model.reference.LocalDocumentReference; + +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.XWikiException; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.BaseObject; + +/** + * Check and update information about notifications sent when {@code NewExtensionVersionAvailableEvent} events are + * triggered. + * + * @version $Id$ + * @since 1.23 + */ +@Component(roles = NewVersionNotificationManager.class) +@Singleton +public class NewVersionNotificationManager +{ + protected static final String EXTENSION_ID = "extensionId"; + + protected static final String NAMESPACE = "namespace"; + + protected static final String VERSION = "version"; + + protected static final List CODE_SPACE = Arrays.asList("Licenses", "Code"); + + protected static final LocalDocumentReference LICENSING_CONFIG_DOC = + new LocalDocumentReference(CODE_SPACE, "LicensingConfig"); + + protected static final LocalDocumentReference NEW_VERSION_NOTIFICATION_CLASS = + new LocalDocumentReference(CODE_SPACE, "NewVersionNotificationClass"); + + @Inject + private Logger logger; + + @Inject + private Provider contextProvider; + + /** + * Check if a notification for this new extension version was already sent. Consider also the namespace since + * different versions can be installed on different namespaces. + * + * @param extensionId the id of the extension + * @param namespaceName the targeted namespace name + * @param version the new version of the extension + * @return {@code true} if a notification was already sent with this exact information, or {@code false} otherwise + */ + public boolean isNotificationAlreadySent(String extensionId, String namespaceName, String version) + { + try { + XWikiContext xcontext = contextProvider.get(); + List versionNotifObjects = xcontext.getWiki().getDocument(LICENSING_CONFIG_DOC, xcontext) + .getXObjects(NEW_VERSION_NOTIFICATION_CLASS); + + return versionNotifObjects != null && versionNotifObjects.stream().filter( + obj -> obj != null && objectHasValue(obj, EXTENSION_ID, extensionId) && objectHasValue(obj, + NAMESPACE, namespaceName) && objectHasValue(obj, VERSION, version)).count() > 0; + } catch (XWikiException e) { + logger.warn("Failed to check if a NewVersionNotification was already sent for [{}]. Root cause is: [{}]", + extensionId, ExceptionUtils.getRootCauseMessage(e)); + } + + return false; + } + + /** + * Mark that a notification was sent for this new extension version. + * + * @param extensionId the id of the extension + * @param namespaceName the targeted namespace name + * @param version the new version of the extension + */ + public void markNotificationAsSent(String extensionId, String namespaceName, String version) + { + try { + XWikiContext xcontext = contextProvider.get(); + XWikiDocument configDoc = xcontext.getWiki().getDocument(LICENSING_CONFIG_DOC, xcontext); + List versionNotifObjects = configDoc.getXObjects(NEW_VERSION_NOTIFICATION_CLASS); + + boolean notificationObjectExists = false; + if (versionNotifObjects != null) { + notificationObjectExists = + updateExistingNotification(versionNotifObjects, extensionId, namespaceName, version); + } + + if (!notificationObjectExists) { + int id = configDoc.createXObject(NEW_VERSION_NOTIFICATION_CLASS, xcontext); + BaseObject obj = configDoc.getXObject(NEW_VERSION_NOTIFICATION_CLASS, id); + obj.setStringValue(EXTENSION_ID, extensionId); + obj.setStringValue(NAMESPACE, namespaceName); + obj.setStringValue(VERSION, version); + } + + xcontext.getWiki().saveDocument(configDoc, + String.format("Added NewVersionNotificationClass object for %s.", extensionId), xcontext); + } catch (XWikiException e) { + logger.warn("Failed add NewVersionNotificationClass object for [{}]. Root cause is: [{}]", extensionId, + ExceptionUtils.getRootCauseMessage(e)); + } + } + + private boolean updateExistingNotification(List versionNotifObjects, String extensionId, + String namespaceName, String version) + { + boolean notificationObjectExists = false; + for (BaseObject obj : versionNotifObjects) { + if (obj == null) { + continue; + } + // If there is already a notification sent for this extensionId and namespace, update only the version. + if (objectHasValue(obj, EXTENSION_ID, extensionId) && objectHasValue(obj, NAMESPACE, namespaceName)) { + if (!objectHasValue(obj, VERSION, version)) { + obj.setStringValue(VERSION, version); + } + notificationObjectExists = true; + } + } + return notificationObjectExists; + } + + private boolean objectHasValue(BaseObject obj, String name, String value) + { + return Objects.equals(obj.getStringValue(name), value); + } +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEventConverter.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionEventConverter.java similarity index 80% rename from application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEventConverter.java rename to application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionEventConverter.java index 0b2ee216..159a624a 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEventConverter.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionEventConverter.java @@ -31,21 +31,24 @@ import org.xwiki.eventstream.RecordableEvent; import org.xwiki.eventstream.RecordableEventConverter; +import com.xwiki.licensing.internal.upgrades.notifications.newVersion.NewExtensionVersionAvailableEvent; + /** - * Add additional information to ExtensionAutoUpgradedEvent or ExtensionAutoUpgradedFailedEvent. + * Add additional information to {@link ExtensionAutoUpgradedEvent}, {@link ExtensionAutoUpgradedFailedEvent} or + * {@link NewExtensionVersionAvailableEvent}. * * @version $Id$ * @since 1.17 */ @Singleton -@Named(ExtensionAutoUpgradedEventConverter.NAME) +@Named(ExtensionEventConverter.NAME) @Component -public class ExtensionAutoUpgradedEventConverter implements RecordableEventConverter +public class ExtensionEventConverter implements RecordableEventConverter { /** * The name of this component. */ - public static final String NAME = "ExtensionAutoUpgradedEventConverter"; + public static final String NAME = "ExtensionEventConverter"; @Inject private RecordableEventConverter defaultConverter; @@ -66,6 +69,7 @@ public Event convert(RecordableEvent recordableEvent, String source, Object data @Override public List getSupportedEvents() { - return Arrays.asList(new ExtensionAutoUpgradedEvent(), new ExtensionAutoUpgradedFailedEvent()); + return Arrays.asList(new ExtensionAutoUpgradedEvent(), new ExtensionAutoUpgradedFailedEvent(), + new NewExtensionVersionAvailableEvent()); } } diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEvent.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEvent.java new file mode 100644 index 00000000..fe4bd431 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEvent.java @@ -0,0 +1,84 @@ +/* + * 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.notifications.newVersion; + +import org.xwiki.eventstream.RecordableEvent; +import org.xwiki.extension.ExtensionId; + +/** + * The event send when a new version of a licensed extension is available. + * + * @version $Id$ + * @since 1.23 + */ +public class NewExtensionVersionAvailableEvent implements RecordableEvent +{ + /** + * The name of this component. + */ + public static final String EVENT_TYPE = "NewExtensionVersionAvailableEvent"; + + private ExtensionId extensionId; + + private String namespace; + + /** + * The default constructor. + */ + public NewExtensionVersionAvailableEvent() + { + } + + /** + * Created a new instance with the given data. + * + * @param extensionId the extension id of the new extension version detected + * @param namespace the namespace where the new extension version was detected, where {@code null} means root + * namespace (i.e. all namespaces) + */ + public NewExtensionVersionAvailableEvent(ExtensionId extensionId, String namespace) + { + this.extensionId = extensionId; + this.namespace = namespace; + } + + @Override + public boolean matches(Object otherEvent) + { + return otherEvent instanceof NewExtensionVersionAvailableEvent; + } + + /** + * @return the extension id of the new version detected, containing information like id and version + */ + public ExtensionId getExtensionId() + { + return this.extensionId; + } + + /** + * @return the namespace where the new extension version was detected. {@code null} means root namespace (i.e all + * namespaces) + */ + public String getNamespace() + { + return this.namespace; + } +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEventDescriptor.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEventDescriptor.java new file mode 100644 index 00000000..1247fa90 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEventDescriptor.java @@ -0,0 +1,70 @@ +/* + * 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.notifications.newVersion; + +import javax.inject.Named; +import javax.inject.Singleton; + +import org.xwiki.component.annotation.Component; +import org.xwiki.eventstream.RecordableEventDescriptor; + +/** + * Description of the {@link NewExtensionVersionAvailableEvent}. Used for displaying settings in Notifications + * Preferences. + * + * @since 1.23 + * @version $Id$ + */ +@Component +@Singleton +@Named(NewExtensionVersionAvailableEventDescriptor.NAME) +public class NewExtensionVersionAvailableEventDescriptor implements RecordableEventDescriptor +{ + /** + * The name of this component. + */ + public static final String NAME = "NewExtensionVersionAvailableEventDescriptor"; + + @Override + public String getApplicationIcon() + { + return "arrow_up"; + } + + @Override + public String getApplicationName() + { + return "licensor.notification.newVersion.name"; + } + + @Override + public String getDescription() + { + return "licensor.notification.newVersion.description"; + } + + @Override + public String getEventType() + { + return NewExtensionVersionAvailableEvent.class.getCanonicalName(); + } + +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEventDisplayer.java b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEventDisplayer.java new file mode 100644 index 00000000..e639051a --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/java/com/xwiki/licensing/internal/upgrades/notifications/newVersion/NewExtensionVersionAvailableEventDisplayer.java @@ -0,0 +1,103 @@ +/* + * 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.notifications.newVersion; + +import java.util.Arrays; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; +import javax.script.ScriptContext; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.slf4j.Logger; +import org.xwiki.component.annotation.Component; +import org.xwiki.model.reference.LocalDocumentReference; +import org.xwiki.notifications.CompositeEvent; +import org.xwiki.notifications.NotificationException; +import org.xwiki.notifications.notifiers.NotificationDisplayer; +import org.xwiki.rendering.block.Block; +import org.xwiki.script.ScriptContextManager; +import org.xwiki.template.Template; +import org.xwiki.template.TemplateManager; + +import com.xpn.xwiki.XWikiContext; + +/** + * Display a custom template for {@link NewExtensionVersionAvailableEvent}. + * + * @since 1.23 + * @version $Id$ + */ +@Component +@Singleton +@Named(NewExtensionVersionAvailableEventDisplayer.NAME) +public class NewExtensionVersionAvailableEventDisplayer implements NotificationDisplayer +{ + protected static final String NAME = "NewExtensionVersionAvailableEventDisplayer"; + + protected static final List EVENTS = + Arrays.asList(NewExtensionVersionAvailableEvent.class.getCanonicalName()); + + protected static final LocalDocumentReference LICENSOR_DOC = new LocalDocumentReference("Licenses", "WebHome"); + + protected static final String EVENT_BINDING_NAME = "event"; + + @Inject + private TemplateManager templateManager; + + @Inject + private Provider contextProvider; + + @Inject + private ScriptContextManager scriptContextManager; + + @Inject + private Logger logger; + + @Override + public Block renderNotification(CompositeEvent eventNotification) throws NotificationException + { + XWikiContext xcontext = contextProvider.get(); + ScriptContext scriptContext = this.scriptContextManager.getScriptContext(); + Template customTemplate = this.templateManager.getTemplate("newVersionAvailable.vm"); + + try { + // Set a document in the context to act as the current document when the template is rendered. + xcontext.setDoc(xcontext.getWiki().getDocument(LICENSOR_DOC, xcontext)); + // Bind the event to some variable in the velocity context. + scriptContext.setAttribute(EVENT_BINDING_NAME, eventNotification, ScriptContext.ENGINE_SCOPE); + + return this.templateManager.execute(customTemplate); + } catch (Exception e) { + logger.warn("Failed to render template for NewExtensionVersionAvailableEvent. Root cause is: [{}]", + ExceptionUtils.getRootCauseMessage(e)); + } + return null; + } + + @Override + public List getSupportedEvents() + { + return EVENTS; + } +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/META-INF/components.txt b/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/META-INF/components.txt index 8c0e376e..d19ece8d 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/META-INF/components.txt +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/META-INF/components.txt @@ -19,8 +19,12 @@ com.xwiki.licensing.internal.helpers.LicensingOwnerConfigurationSource com.xwiki.licensing.internal.upgrades.UpgradeExtensionHandler com.xwiki.licensing.internal.upgrades.AutomaticUpgradesConfigurationSource com.xwiki.licensing.internal.upgrades.LicensedExtensionUpgradeManager +com.xwiki.licensing.internal.upgrades.NewExtensionVersionAvailableManager +com.xwiki.licensing.internal.upgrades.NewVersionNotificationManager com.xwiki.licensing.internal.upgrades.LicensingSchedulerListener com.xwiki.licensing.internal.upgrades.notifications.ExtensionAutoUpgradedEventDescriptor -com.xwiki.licensing.internal.upgrades.notifications.ExtensionAutoUpgradedEventConverter +com.xwiki.licensing.internal.upgrades.notifications.ExtensionEventConverter com.xwiki.licensing.internal.upgrades.notifications.ExtensionAutoUpgradedEventDisplayer -com.xwiki.licensing.internal.upgrades.notifications.ExtensionAutoUpgradedFailedEventDescriptor \ No newline at end of file +com.xwiki.licensing.internal.upgrades.notifications.ExtensionAutoUpgradedFailedEventDescriptor +com.xwiki.licensing.internal.upgrades.notifications.newVersion.NewExtensionVersionAvailableEventDescriptor +com.xwiki.licensing.internal.upgrades.notifications.newVersion.NewExtensionVersionAvailableEventDisplayer \ No newline at end of file diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/templates/newVersionAvailable.vm b/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/templates/newVersionAvailable.vm new file mode 100644 index 00000000..2035e40b --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/main/resources/templates/newVersionAvailable.vm @@ -0,0 +1,66 @@ +#template('notification/macros.vm') + +#set ($mainIcon = "bell") +#set ($smallIcon = "arrow_up") +#set ($compositeEventDate = $escapetool.xml($datetool.whenIs($event.dates.get(0)))) + +#macro (getExtensionUpgradeURL $extensionInfo) + #set ($extensionNamespace = + "#if ($extensionInfo.namespace == 'root')$xcontext.mainWikiName#{else}$extensionInfo.namespace#end") + #set ($queryParams = $escapetool.url({ + 'section': 'XWiki.Extensions', + 'extensionId': $event.getApplication(), + 'extensionVersion': $extensionInfo.version, + 'extensionNamespace': $extensionNamespace + })) + $xwiki.getURL($services.model.createDocumentReference($xcontext.mainWikiName, 'XWiki', 'XWikiPreferences'), + 'admin', $queryParams)## +#end + +#macro (displayEventDetails $event) + + + $escapetool.xml($event.user.name) + + ## The event body contains information about the extension name, version and targeted namespace. + #set ($extensionInfo = $jsontool.fromString($event.getBody())) + + + $extensionInfo.extensionName - $extensionInfo.namespace - $extensionInfo.version + + + $escapetool.xml($datetool.whenIs($event.date)) + +#end + +#define ($content) + #set ($document = $xwiki.getDocument($event.document)) +
+ ## Notifications are rendered in the context of their wiki, so we need to use the XWikiContext#originalWikiId + ## to actually know where the request comes from. + #if ($xcontext.getContext().getOriginalWikiId() != $event.document.wikiReference.name) + ($services.wiki.getById($event.document.wikiReference.name).prettyName) + #end +
+
+
+ #if ($event.events.size() == 1) + $services.localization.render("licensor.notification.newVersion.singular") + #else + $services.localization.render("licensor.notification.newVersion.plural") + #end +
+
$compositeEventDate
+
+#end + +#define ($details) + #define($rows) + #foreach($thisEvent in $event.events) + #displayEventDetails($thisEvent) + #end + #end + #displayNotificationEventDetailsSkeletons($events.size(), $rows) +#end + +#displayNotificationEventSkeleton($mainIcon $smallIcon $content $details) \ No newline at end of file diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/DefaultLicensingConfigurationTest.java b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/DefaultLicensingConfigurationTest.java index ef736628..76d0ed38 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/DefaultLicensingConfigurationTest.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/DefaultLicensingConfigurationTest.java @@ -40,6 +40,8 @@ import org.xwiki.test.junit5.mockito.InjectMockComponents; import org.xwiki.test.junit5.mockito.MockComponent; +import com.xpn.xwiki.objects.BaseObject; + @ComponentTest class DefaultLicensingConfigurationTest { diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManagerTest.java b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManagerTest.java new file mode 100644 index 00000000..26d09a6e --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewExtensionVersionAvailableManagerTest.java @@ -0,0 +1,198 @@ +/* + * 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +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.extension.version.internal.DefaultVersion; +import org.xwiki.observation.ObservationManager; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xwiki.licensing.LicensedExtensionManager; +import com.xwiki.licensing.LicensingConfiguration; +import com.xwiki.licensing.internal.upgrades.notifications.newVersion.NewExtensionVersionAvailableEvent; + +/** + * Unit tests for {@link NewExtensionVersionAvailableManager}. + * + * @version $Id$ + * @since 1.23 + */ +@ComponentTest +public class NewExtensionVersionAvailableManagerTest +{ + @InjectMockComponents + private NewExtensionVersionAvailableManager newVersionAvailableManager; + + @MockComponent + private InstalledExtensionRepository installedRepository; + + @MockComponent + private UpgradeExtensionHandler upgradeExtensionHandler; + + @MockComponent + private LicensedExtensionManager licensedExtensionManager; + + @MockComponent + private LicensingConfiguration licensingConfig; + + @MockComponent + private ObservationManager observationManager; + + @MockComponent + private NewVersionNotificationManager newVersionNotificationManager; + + @Mock + private InstalledExtension installedExtension1; + + @Mock + private InstalledExtension installedExtension2; + + private ExtensionId extensionId1; + + private ExtensionId extensionId2; + + @BeforeEach + public void configure() throws Exception + { + this.extensionId1 = new ExtensionId("extensionId1", new DefaultVersion("1.0")); + this.extensionId2 = new ExtensionId("extensionId2", new DefaultVersion("2.0")); + + when(this.installedExtension1.getName()).thenReturn("Application 1"); + when(this.installedExtension1.getId()).thenReturn(extensionId1); + when(this.installedRepository.getInstalledExtension(this.extensionId1)).thenReturn(this.installedExtension1); + + when(this.installedExtension2.getName()).thenReturn("Application 2"); + when(this.installedExtension2.getId()).thenReturn(extensionId2); + when(this.installedRepository.getInstalledExtension(this.extensionId2)).thenReturn(this.installedExtension2); + } + + @Test + void checkLicensedExtensionsAvailableVersionsWithMultipleExtensionsAndVersionsNotVerified() throws Exception + { + when(this.licensingConfig.getAutoUpgradeAllowList()).thenReturn(Collections.emptyList()); + when(this.licensedExtensionManager.getLicensedExtensions()) + .thenReturn(Arrays.asList(this.extensionId1, this.extensionId2)); + + String namespace = "wiki:test"; + when(this.installedExtension1.getNamespaces()).thenReturn(Collections.singletonList(namespace)); + when(this.installedRepository.getInstalledExtension(this.extensionId1.getId(), namespace)) + .thenReturn(this.installedExtension1); + + when(this.installedExtension2.getNamespaces()).thenReturn(null); + when(this.installedRepository.getInstalledExtension(this.extensionId2.getId(), null)) + .thenReturn(this.installedExtension2); + + when(this.upgradeExtensionHandler.getInstallableVersions(extensionId1)) + .thenReturn(Collections.singletonList((Version) new DefaultVersion("2.1"))); + when(this.upgradeExtensionHandler.getInstallableVersions(extensionId2)) + .thenReturn(Arrays.asList((Version) new DefaultVersion("3.1"), (Version) new DefaultVersion("2.1"))); + when(this.newVersionNotificationManager.isNotificationAlreadySent(this.extensionId1.getId(), namespace, + "2.1")).thenReturn(false); + when(this.newVersionNotificationManager.isNotificationAlreadySent(this.extensionId2.getId(), "root", + "3.1")).thenReturn(false); + + this.newVersionAvailableManager.checkLicensedExtensionsAvailableVersions(); + + verify(this.observationManager, times(1)).notify(any(NewExtensionVersionAvailableEvent.class), + eq(this.extensionId1.getId()), + eq("{\"extensionName\":\"Application 1\",\"namespace\":\"wiki:test\",\"version\":\"2.1\"}")); + verify(this.observationManager, times(1)).notify(any(NewExtensionVersionAvailableEvent.class), + eq(this.extensionId2.getId()), + eq("{\"extensionName\":\"Application 2\",\"namespace\":\"root\",\"version\":\"3.1\"}")); + } + + @Test + void checkLicensedExtensionsAvailableVersionsWithVerifiedVersions() throws Exception + { + when(this.licensingConfig.getAutoUpgradeAllowList()).thenReturn(Collections.emptyList()); + when(this.licensedExtensionManager.getLicensedExtensions()) + .thenReturn(Arrays.asList(this.extensionId1, this.extensionId2)); + + String namespace = "wiki:test"; + when(this.installedExtension1.getNamespaces()).thenReturn(Collections.singletonList(namespace)); + when(this.installedRepository.getInstalledExtension(this.extensionId1.getId(), namespace)) + .thenReturn(this.installedExtension1); + + when(this.installedExtension2.getNamespaces()).thenReturn(null); + when(this.installedRepository.getInstalledExtension(this.extensionId2.getId(), null)) + .thenReturn(this.installedExtension2); + + when(this.upgradeExtensionHandler.getInstallableVersions(extensionId1)) + .thenReturn(Arrays.asList((Version) new DefaultVersion("2.2"), (Version) new DefaultVersion("2.1"))); + when(this.upgradeExtensionHandler.getInstallableVersions(extensionId2)) + .thenReturn(Arrays.asList((Version) new DefaultVersion("3.1"), (Version) new DefaultVersion("3.0"))); + + when(this.newVersionNotificationManager.isNotificationAlreadySent(this.extensionId1.getId(), namespace, + "2.2")).thenReturn(true); + when(this.newVersionNotificationManager.isNotificationAlreadySent(this.extensionId2.getId(), null, + "3.0")).thenReturn(true); + + this.newVersionAvailableManager.checkLicensedExtensionsAvailableVersions(); + + verify(this.observationManager, never()).notify(any(NewExtensionVersionAvailableEvent.class), + eq(this.extensionId1.getId()), any(String.class)); + verify(this.observationManager, times(1)).notify(any(NewExtensionVersionAvailableEvent.class), + eq(this.extensionId2.getId()), + eq("{\"extensionName\":\"Application 2\",\"namespace\":\"root\",\"version\":\"3.1\"}")); + verify(this.observationManager, never()).notify(any(NewExtensionVersionAvailableEvent.class), + eq(this.extensionId2.getId()), + eq("{\"extensionName\":\"Application 2\",\"namespace\":\"root\",\"version\":\"3.0\"}")); + } + + @Test + void checkLicensedExtensionsAvailableVersionsWithoutNewVersions() throws Exception + { + when(this.licensingConfig.getAutoUpgradeAllowList()).thenReturn( + Collections.singletonList(this.extensionId2.getId())); + when(this.licensedExtensionManager.getLicensedExtensions()) + .thenReturn(Arrays.asList(this.extensionId1, this.extensionId2)); + + String namespace = "wiki:test"; + when(this.installedExtension1.getNamespaces()).thenReturn(Collections.singletonList(namespace)); + when(this.installedRepository.getInstalledExtension(this.extensionId1.getId(), namespace)) + .thenReturn(this.installedExtension1); + + when(this.upgradeExtensionHandler.getInstallableVersions(extensionId1)).thenReturn(Collections.emptyList()); + + this.newVersionAvailableManager.checkLicensedExtensionsAvailableVersions(); + + verify(this.observationManager, never()).notify(any(NewExtensionVersionAvailableEvent.class), + eq(this.extensionId1), any(String.class)); + verify(this.installedRepository, never()).getInstalledExtension(this.extensionId2); + } +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewVersionNotificationManagerTest.java b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewVersionNotificationManagerTest.java new file mode 100644 index 00000000..8f1b57c8 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/NewVersionNotificationManagerTest.java @@ -0,0 +1,188 @@ +/* + * 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.Arrays; +import java.util.Collections; + +import javax.inject.Provider; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.slf4j.Logger; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; + +import com.xpn.xwiki.XWiki; +import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.doc.XWikiDocument; +import com.xpn.xwiki.objects.BaseObject; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link NewVersionNotificationManager}. + * + * @version $Id$ + * @since 1.23 + */ +@ComponentTest +class NewVersionNotificationManagerTest +{ + @InjectMockComponents + private NewVersionNotificationManager newVersionNotificationManager; + + @MockComponent + private Provider xcontextProvider; + + @MockComponent + private Logger logger; + + @Mock + private XWikiContext xcontext; + + @Mock + private XWiki xwiki; + + @Mock + private XWikiDocument licensingDoc; + + @Mock + private BaseObject newVersionObject1; + + @Mock + private BaseObject newVersionObject2; + + @BeforeEach + void configure() throws Exception + { + when(this.xcontextProvider.get()).thenReturn(xcontext); + when(this.xcontext.getWiki()).thenReturn(xwiki); + when(this.xwiki.getDocument(NewVersionNotificationManager.LICENSING_CONFIG_DOC, xcontext)).thenReturn( + licensingDoc); + } + + @Test + void isNotificationAlreadySent() throws Exception + { + when(this.licensingDoc.getXObjects(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS)).thenReturn( + Arrays.asList(newVersionObject1, newVersionObject2)); + + when(newVersionObject1.getStringValue(NewVersionNotificationManager.EXTENSION_ID)).thenReturn("extension1"); + when(newVersionObject1.getStringValue(NewVersionNotificationManager.NAMESPACE)).thenReturn("root"); + when(newVersionObject1.getStringValue(NewVersionNotificationManager.VERSION)).thenReturn("1.1"); + + when(newVersionObject2.getStringValue(NewVersionNotificationManager.EXTENSION_ID)).thenReturn("extension1"); + when(newVersionObject2.getStringValue(NewVersionNotificationManager.NAMESPACE)).thenReturn("xwiki:test"); + when(newVersionObject2.getStringValue(NewVersionNotificationManager.VERSION)).thenReturn("1.1"); + + assertTrue(this.newVersionNotificationManager.isNotificationAlreadySent("extension1", "root", "1.1")); + } + + @Test + void isNotificationAlreadySentWithDifferentInfo() throws Exception + { + when(this.licensingDoc.getXObjects(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS)).thenReturn( + Arrays.asList(newVersionObject1, null, newVersionObject2)); + + when(newVersionObject1.getStringValue(NewVersionNotificationManager.EXTENSION_ID)).thenReturn("extension1"); + when(newVersionObject1.getStringValue(NewVersionNotificationManager.NAMESPACE)).thenReturn("root"); + when(newVersionObject1.getStringValue(NewVersionNotificationManager.VERSION)).thenReturn("1.0"); + + when(newVersionObject2.getStringValue(NewVersionNotificationManager.EXTENSION_ID)).thenReturn("extension1"); + when(newVersionObject2.getStringValue(NewVersionNotificationManager.NAMESPACE)).thenReturn("xwiki:test"); + when(newVersionObject2.getStringValue(NewVersionNotificationManager.VERSION)).thenReturn("1.1"); + + assertFalse(this.newVersionNotificationManager.isNotificationAlreadySent("extension1", "root", "1.1")); + } + + @Test + void markNotificationAsSent() throws Exception + { + when(this.licensingDoc.getXObjects(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS)).thenReturn( + null); + + when(this.licensingDoc.createXObject(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS, + xcontext)).thenReturn(0); + when(this.licensingDoc.getXObject(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS, 0)).thenReturn( + this.newVersionObject1); + + this.newVersionNotificationManager.markNotificationAsSent("extension1", "root", "2.1"); + + verify(this.newVersionObject1, times(1)).setStringValue(NewVersionNotificationManager.EXTENSION_ID, + "extension1"); + verify(this.newVersionObject1, times(1)).setStringValue(NewVersionNotificationManager.NAMESPACE, "root"); + verify(this.newVersionObject1, times(1)).setStringValue(NewVersionNotificationManager.VERSION, "2.1"); + verify(this.xwiki, times(1)).saveDocument(any(XWikiDocument.class), + eq("Added NewVersionNotificationClass object for extension1."), any(XWikiContext.class)); + } + + @Test + void markNotificationAsSentOnSameNamespace() throws Exception + { + when(this.licensingDoc.getXObjects(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS)).thenReturn( + Collections.singletonList(newVersionObject1)); + + when(newVersionObject1.getStringValue(NewVersionNotificationManager.EXTENSION_ID)).thenReturn("extension1"); + when(newVersionObject1.getStringValue(NewVersionNotificationManager.NAMESPACE)).thenReturn("root"); + when(newVersionObject1.getStringValue(NewVersionNotificationManager.VERSION)).thenReturn("1.0"); + + this.newVersionNotificationManager.markNotificationAsSent("extension1", "root", "1.1"); + + verify(this.newVersionObject1, times(1)).setStringValue(NewVersionNotificationManager.VERSION, "1.1"); + verify(this.xwiki, times(1)).saveDocument(any(XWikiDocument.class), + eq("Added NewVersionNotificationClass object for extension1."), any(XWikiContext.class)); + } + + @Test + void markNotificationAsSentOnDifferentNamespace() throws Exception + { + when(this.licensingDoc.getXObjects(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS)).thenReturn( + Arrays.asList(newVersionObject1, null)); + + when(newVersionObject1.getStringValue(NewVersionNotificationManager.EXTENSION_ID)).thenReturn("extension1"); + when(newVersionObject1.getStringValue(NewVersionNotificationManager.NAMESPACE)).thenReturn("root"); + when(newVersionObject1.getStringValue(NewVersionNotificationManager.VERSION)).thenReturn("1.0"); + + when(this.licensingDoc.createXObject(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS, + xcontext)).thenReturn(1); + when(this.licensingDoc.getXObject(NewVersionNotificationManager.NEW_VERSION_NOTIFICATION_CLASS, 1)).thenReturn( + this.newVersionObject2); + + this.newVersionNotificationManager.markNotificationAsSent("extension1", "xwiki:test", "1.1"); + + verify(this.newVersionObject1, never()).setStringValue(NewVersionNotificationManager.VERSION, "1.1"); + verify(this.newVersionObject2, times(1)).setStringValue(NewVersionNotificationManager.EXTENSION_ID, + "extension1"); + verify(this.newVersionObject2, times(1)).setStringValue(NewVersionNotificationManager.NAMESPACE, "xwiki:test"); + verify(this.newVersionObject2, times(1)).setStringValue(NewVersionNotificationManager.VERSION, "1.1"); + verify(this.xwiki, times(1)).saveDocument(any(XWikiDocument.class), + eq("Added NewVersionNotificationClass object for extension1."), any(XWikiContext.class)); + } +} diff --git a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEventConverterTest.java b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionEventConverterTest.java similarity index 69% rename from application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEventConverterTest.java rename to application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionEventConverterTest.java index 38528c49..cf391465 100644 --- a/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionAutoUpgradedEventConverterTest.java +++ b/application-licensing-licensor/application-licensing-licensor-api/src/test/java/com/xwiki/licensing/internal/upgrades/notifications/ExtensionEventConverterTest.java @@ -19,40 +19,34 @@ */ package com.xwiki.licensing.internal.upgrades.notifications; -import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.xwiki.eventstream.Event; import org.xwiki.eventstream.RecordableEvent; import org.xwiki.eventstream.RecordableEventConverter; -import org.xwiki.test.mockito.MockitoComponentMockingRule; +import org.xwiki.test.junit5.mockito.ComponentTest; +import org.xwiki.test.junit5.mockito.InjectMockComponents; +import org.xwiki.test.junit5.mockito.MockComponent; /** - * Unit tests for {@link ExtensionAutoUpgradedEventConverter}. + * Unit tests for {@link ExtensionEventConverter}. * * @version $Id$ * @since 1.17 */ -public class ExtensionAutoUpgradedEventConverterTest +@ComponentTest +class ExtensionEventConverterTest { - @Rule - public MockitoComponentMockingRule mocker = - new MockitoComponentMockingRule<>(ExtensionAutoUpgradedEventConverter.class); + @InjectMockComponents + ExtensionEventConverter extensionEventConverter; + @MockComponent private RecordableEventConverter defaultConverter; - - @Before - public void configure() throws Exception - { - this.defaultConverter = this.mocker.getInstance(RecordableEventConverter.class); - } - @Test public void convert() throws Exception { @@ -62,7 +56,7 @@ public void convert() throws Exception when(this.defaultConverter.convert(event, null, message)).thenReturn(convertedEvent); - assertEquals(convertedEvent, this.mocker.getComponentUnderTest().convert(event, null, message)); + assertEquals(convertedEvent, this.extensionEventConverter.convert(event, null, message)); verify(convertedEvent).setBody(message); } diff --git a/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/NewExtensionVersionAvailableJob.xml b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/NewExtensionVersionAvailableJob.xml new file mode 100644 index 00000000..878367c1 --- /dev/null +++ b/application-licensing-licensor/application-licensing-licensor-ui/src/main/resources/Licenses/Code/NewExtensionVersionAvailableJob.xml @@ -0,0 +1,168 @@ + + + + + + Licenses.Code + NewExtensionVersionAvailableJob + + + 0 + xwiki:XWiki.Admin + Licenses.Code.WebHome + xwiki:XWiki.Admin + xwiki:XWiki.Admin + 1.1 + NewExtensionVersionAvailableJob + + false + xwiki/2.1 + false + + + Licenses.Code.NewExtensionVersionAvailableJob + 0 + XWiki.SchedulerJobClass + 5bdc26a3-8be2-421d-9a2f-e3baaa6ff4b1 + + XWiki.SchedulerJobClass + + + + + + + + + 0 + contextDatabase + 9 + Job execution context database + 30 + 0 + com.xpn.xwiki.objects.classes.StringClass + + + 0 + contextLang + 8 + Job execution context lang + 30 + 0 + com.xpn.xwiki.objects.classes.StringClass + + + 0 + contextUser + 7 + Job execution context user + 30 + 0 + com.xpn.xwiki.objects.classes.StringClass + + + 0 + cron + 5 + Cron Expression + 30 + 0 + com.xpn.xwiki.objects.classes.StringClass + + + 0 + jobClass + 3 + Job Class + 60 + 0 + com.xpn.xwiki.objects.classes.StringClass + + + 0 + jobDescription + 2 + Job Description + 10 + 45 + 0 + com.xpn.xwiki.objects.classes.TextAreaClass + + + 0 + jobName + 1 + Job Name + 60 + 0 + com.xpn.xwiki.objects.classes.StringClass + + + + 0 + status + 4 + Status + 30 + 0 + com.xpn.xwiki.objects.classes.StringClass + + + + xwiki + + + en + + + XWiki.superadmin + + + 0 0 0 ? * SUN + + + com.xwiki.licensing.internal.upgrades.NewExtensionVersionAvailableJob + + + + + + New version available + + +