Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<properties>
<!-- Name to display by the Extension Manager -->
<xwiki.extension.name>Scheduler API</xwiki.extension.name>
<xwiki.jacoco.instructionRatio>0.08</xwiki.jacoco.instructionRatio>
<xwiki.jacoco.instructionRatio>0.07</xwiki.jacoco.instructionRatio>
</properties>
<dependencies>
<dependency>
Expand All @@ -46,6 +46,11 @@
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<dependency>
<groupId>org.xwiki.commons</groupId>
<artifactId>xwiki-commons-classloader-api</artifactId>
<version>${commons.version}</version>
</dependency>
<dependency>
<groupId>org.xwiki.platform</groupId>
<artifactId>xwiki-platform-test-oldcore</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Set;
Expand All @@ -48,8 +47,10 @@
import org.xwiki.bridge.event.DocumentDeletedEvent;
import org.xwiki.bridge.event.DocumentUpdatedEvent;
import org.xwiki.bridge.event.WikiDeletedEvent;
import org.xwiki.classloader.internal.ClassLoaderResetEvent;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.context.concurrent.ExecutionContextRunnable;
import org.xwiki.model.namespace.WikiNamespace;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.observation.EventListener;
Expand All @@ -68,6 +69,7 @@
import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobClassDocumentInitializer;
import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobsInitializedEvent;
import com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobsInitializingEvent;
import com.xpn.xwiki.plugin.scheduler.internal.SchedulersClassLoaderManager;
import com.xpn.xwiki.plugin.scheduler.internal.StatusListener;
import com.xpn.xwiki.web.Utils;
import com.xpn.xwiki.web.XWikiResponse;
Expand Down Expand Up @@ -102,8 +104,13 @@ public class SchedulerPlugin extends XWikiDefaultPlugin implements EventListener
public static final EntityReference XWIKI_JOB_CLASSREFERENCE =
SchedulerJobClassDocumentInitializer.XWIKI_JOB_CLASSREFERENCE;

private static final List<Event> EVENTS = Arrays.<Event>asList(new DocumentCreatedEvent(),
new DocumentDeletedEvent(), new DocumentUpdatedEvent(), new WikiDeletedEvent());
private static final List<Event> EVENTS = List.of(
new DocumentCreatedEvent(),
new DocumentDeletedEvent(),
new DocumentUpdatedEvent(),
new WikiDeletedEvent(),
new ClassLoaderResetEvent()
);

/**
* Default Quartz scheduler instance.
Expand All @@ -112,6 +119,8 @@ public class SchedulerPlugin extends XWikiDefaultPlugin implements EventListener

private boolean enabled;

private SchedulersClassLoaderManager schedulersClassLoaderManager;

/**
* Default plugin constructor.
*
Expand All @@ -128,6 +137,8 @@ public void init(XWikiContext context)
// Check if the Scheduler plugin is enabled
this.enabled =
Utils.getComponent(ConfigurationSource.class, "xwikiproperties").getProperty("scheduler.enabled", true);
this.schedulersClassLoaderManager = Utils.getComponent(SchedulersClassLoaderManager.class);
this.schedulersClassLoaderManager.setSchedulerPlugin(this);

if (this.enabled) {
Thread thread = new Thread(new ExecutionContextRunnable(new Runnable()
Expand Down Expand Up @@ -397,13 +408,9 @@ public boolean scheduleJob(BaseObject object, XWikiContext context) throws Sched
try {
// compute the job unique Id
String xjob = getObjectUniqueId(object);

// Load the job class.
// Note: Remember to always use the current thread's class loader and not the container's
// (Class.forName(...)) since otherwise we will not be able to load classes installed with EM.
ClassLoader currentThreadClassLoader = Thread.currentThread().getContextClassLoader();
String jobClassName = object.getStringValue("jobClass");
Class<Job> jobClass = (Class<Job>) Class.forName(jobClassName, true, currentThreadClassLoader);
String jobClassName = object.getStringValue(SchedulerJobClassDocumentInitializer.FIELD_JOBCLASS);
Class<Job> jobClass = (Class<Job>) this.schedulersClassLoaderManager
.loadClassAndRegister(jobClassName, object.getReference());

// Build the new job.
JobBuilder jobBuilder = JobBuilder.newJob(jobClass);
Expand Down Expand Up @@ -570,6 +577,7 @@ private void deleteJob(BaseObject object) throws SchedulerPluginException
throw new SchedulerPluginException(SchedulerPluginException.ERROR_SCHEDULERPLUGIN_PAUSE_JOB,
"Error occured while trying to pause job " + object.getStringValue("jobName"), e);
}
this.schedulersClassLoaderManager.removeScheduler(object.getReference());
}

/**
Expand Down Expand Up @@ -733,13 +741,17 @@ public List<Event> getEvents()
@Override
public void onEvent(Event event, Object source, Object data)
{
if (event instanceof WikiDeletedEvent) {
String wikiId = ((WikiDeletedEvent) event).getWikiId();
if (event instanceof WikiDeletedEvent wikiDeletedEvent) {
String wikiId = wikiDeletedEvent.getWikiId();
try {
onWikiDeletedEvent(wikiId);
} catch (SchedulerException e) {
LOGGER.error("Failed to remove schedulers for wiki [{}]", wikiId, e);
}
this.schedulersClassLoaderManager.removeSchedulers(new WikiNamespace(wikiId).serialize());
} else if (event instanceof ClassLoaderResetEvent classLoaderResetEvent) {
String namespace = classLoaderResetEvent.getNamespace();
this.schedulersClassLoaderManager.onClassLoaderReset(namespace);
} else {
onDocumentEvent(source, data);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,15 @@ public class SchedulerJobClassDocumentInitializer extends AbstractMandatoryClass
public static final LocalDocumentReference XWIKI_JOB_CLASSREFERENCE =
new LocalDocumentReference(XWiki.SYSTEM_SPACE, "SchedulerJobClass");

/**
* Field containing the class name of the job.
*/
public static final String FIELD_JOBCLASS = "jobClass";

private static final String FIELD_JOBNAME = "jobName";

private static final String FIELD_JOBDESCRIPTION = "jobDescription";

private static final String FIELD_JOBCLASS = "jobClass";

private static final String FIELD_STATUS = "status";

private static final String FIELD_CRON = "cron";
Expand Down
Original file line number Diff line number Diff line change
@@ -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.xpn.xwiki.plugin.scheduler.internal;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

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

import org.slf4j.Logger;
import org.xwiki.classloader.ClassLoaderManager;
import org.xwiki.classloader.NamespaceURLClassLoader;
import org.xwiki.component.annotation.Component;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.EntityReference;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.objects.BaseObjectReference;
import com.xpn.xwiki.plugin.scheduler.SchedulerPlugin;

/**
* Component dedicated to handle operations related to loading classes for Scheduler.
*
* @version $Id$
* @since 17.10.1
* @since 18.0.0RC1
*/
@Component(roles = SchedulersClassLoaderManager.class)
@Singleton
public class SchedulersClassLoaderManager
{
private SchedulerPlugin schedulerPlugin;

private final Map<String, Set<BaseObjectReference>> schedulersMapPerNamespace = new HashMap<>();

@Inject
private Provider<XWikiContext> contextProvider;

@Inject
private Logger logger;

@Inject
private ClassLoaderManager classLoaderManager;

/**
* Define the instance of the scheduler plugin to use.
* @param schedulerPlugin the scheduler plugin instance this instance should use.
*/
public void setSchedulerPlugin(SchedulerPlugin schedulerPlugin)
{
this.schedulerPlugin = schedulerPlugin;
}

private void registerScheduler(String namespace, BaseObjectReference objectReference)
{
if (!this.schedulersMapPerNamespace.containsKey(namespace)) {
this.schedulersMapPerNamespace.put(namespace, new HashSet<>());
}
this.schedulersMapPerNamespace.get(namespace).add(objectReference);
}

/**
* Remove scheduler information related to given object reference.
* @param objectReference the reference of a scheduler object.
*/
public void removeScheduler(BaseObjectReference objectReference)
{
for (Set<BaseObjectReference> objectReferenceSet : this.schedulersMapPerNamespace.values()) {
objectReferenceSet.remove(objectReference);
}
}

/**
* Remove all schedulers information associated to a namespace.
* @param namespace the namespace for which to remove information.
*/
public void removeSchedulers(String namespace)
{
this.schedulersMapPerNamespace.remove(namespace);
}

/**
* Perform operations when a classloader of a specific namespace is reset.
* @param namespace the namespace for which an event has been triggered.
*/
public void onClassLoaderReset(String namespace)
{
Set<BaseObjectReference> objectReferences =
new HashSet<>(schedulersMapPerNamespace.getOrDefault(namespace, Set.of()));
for (BaseObjectReference objectReference : objectReferences) {
this.reloadScheduler(objectReference);
}
}

private void reloadScheduler(BaseObjectReference objectReference)
{
XWikiContext context = contextProvider.get();
EntityReference documentReference = objectReference.extractReference(EntityType.DOCUMENT);
try {
XWikiDocument document = context.getWiki().getDocument(documentReference, context);
BaseObject jobObject = document.getXObject(SchedulerJobClassDocumentInitializer.XWIKI_JOB_CLASSREFERENCE);
this.schedulerPlugin.unscheduleJob(jobObject, context);
this.schedulerPlugin.scheduleJob(jobObject, context);
} catch (XWikiException e) {
this.logger.error("Error while trying to reload scheduler for object [{}]: ", objectReference, e);
}
}

/**
* Load a class for a scheduler and register it at the same time.
* @param className the name of the class to load.
* @param baseObjectReference the reference of the object of the scheduler.
* @return the instance of the given class name.
* @throws ClassNotFoundException if the class cannot be found.
*/
public Class<?> loadClassAndRegister(String className, BaseObjectReference baseObjectReference)
throws ClassNotFoundException
{
String namespace = null;

// Reload the root classloader if needed: it's important if it's been dropped.
NamespaceURLClassLoader classLoader = this.classLoaderManager.getURLClassLoader(null, true);
Class<?> result = Class.forName(className, true, classLoader);

// find the actual namespace of the classloader from where the class has been found.
if (result.getClassLoader() instanceof NamespaceURLClassLoader namespaceURLClassLoader) {
namespace = namespaceURLClassLoader.getNamespace();
}

this.registerScheduler(namespace, baseObjectReference);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
com.xpn.xwiki.plugin.scheduler.internal.SchedulerJobClassDocumentInitializer
com.xpn.xwiki.plugin.scheduler.internal.SchedulersClassLoaderManager
Loading