/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.authorization.xacml.manager.impl;
import static org.opencastproject.util.data.Option.some;
import static org.opencastproject.util.data.Prelude.unexhaustiveMatch;
import org.opencastproject.authorization.xacml.manager.api.ACLTransition;
import org.opencastproject.authorization.xacml.manager.api.AclService;
import org.opencastproject.authorization.xacml.manager.api.AclServiceException;
import org.opencastproject.authorization.xacml.manager.api.AclServiceFactory;
import org.opencastproject.authorization.xacml.manager.api.EpisodeACLTransition;
import org.opencastproject.authorization.xacml.manager.api.SeriesACLTransition;
import org.opencastproject.authorization.xacml.manager.api.TransitionQuery;
import org.opencastproject.authorization.xacml.manager.api.TransitionResult;
import org.opencastproject.security.api.Organization;
import org.opencastproject.security.api.OrganizationDirectoryService;
import org.opencastproject.security.util.SecurityContext;
import org.opencastproject.util.NeedleEye;
import org.opencastproject.util.data.Collections;
import org.opencastproject.util.data.Effect0;
import org.opencastproject.util.data.Function0;
import org.opencastproject.util.data.Option;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.List;
/** Time control for the {@link AclServiceImpl}. */
public abstract class AbstractAclScheduler {
/** Log facility */
private static final Logger logger = LoggerFactory.getLogger(AbstractAclScheduler.class);
private static final String JOB_NAME = "mh-acl-job";
private static final String JOB_GROUP = "mh-acl-job-group";
private static final String TRIGGER_NAME = "mh-acl-trigger";
private static final String TRIGGER_GROUP = "mh-acl-trigger-group";
private static final String JOB_PARAM_PARENT = "parent";
private final org.quartz.Scheduler quartz;
public abstract AclServiceFactory getAclServiceFactory();
public abstract OrganizationDirectoryService getOrganizationDirectoryService();
/** Get a system administrator context for the given organization id. */
public abstract SecurityContext getAdminContextFor(String orgId);
protected AbstractAclScheduler() {
try {
quartz = new StdSchedulerFactory().getScheduler();
quartz.start();
// create and set the job. To actually run it call schedule(..)
final JobDetail job = new JobDetail(JOB_NAME, JOB_GROUP, Runner.class);
job.setDurability(false);
job.setVolatility(true);
job.getJobDataMap().put(JOB_PARAM_PARENT, this);
quartz.addJob(job, true);
} catch (org.quartz.SchedulerException e) {
throw new RuntimeException(e);
}
}
/**
* Set the schedule and start or restart the scheduler.
*/
public void schedule() {
logger.info("Run AclScheduler every minute");
try {
final Trigger trigger = TriggerUtils.makeMinutelyTrigger();
trigger.setStartTime(new Date());
trigger.setName(TRIGGER_NAME);
trigger.setGroup(TRIGGER_GROUP);
trigger.setJobName(JOB_NAME);
trigger.setJobGroup(JOB_GROUP);
if (quartz.getTriggersOfJob(JOB_NAME, JOB_GROUP).length == 0) {
quartz.scheduleJob(trigger);
} else {
quartz.rescheduleJob(TRIGGER_NAME, TRIGGER_GROUP, trigger);
}
} catch (Exception e) {
logger.error("Error scheduling Quartz job", e);
}
}
/** Shutdown the scheduler. */
public void shutdown() {
try {
quartz.shutdown();
} catch (org.quartz.SchedulerException ignore) {
}
}
/** Trigger the scheduler once independent of it's actual schedule. */
public void trigger() {
try {
quartz.triggerJobWithVolatileTrigger(JOB_NAME, JOB_GROUP);
} catch (Exception e) {
logger.error("Error triggering Quartz job", e);
}
}
// just to make sure Quartz is being shut down...
@Override
protected void finalize() throws Throwable {
super.finalize();
shutdown();
}
// --
/** Quartz work horse. */
public static class Runner extends TypedQuartzJob<AbstractAclScheduler> {
private static final NeedleEye eye = new NeedleEye();
public Runner() {
super(some(eye));
}
@Override
protected void execute(final AbstractAclScheduler parameters, JobExecutionContext ctx) {
logger.debug("Running ACL scheduler");
// iterate all organizations
for (final Organization org : parameters.getOrganizationDirectoryService().getOrganizations()) {
// set the org on the current thread... this approach should be deprecated
// todo check if after the heavy refactoring this is still necessary.
parameters.getAdminContextFor(org.getId()).runInContext(new Effect0() {
@Override
protected void run() {
// create an ACL service for the org. This approach should be used in favour of thread bound orgs.
final AclService aclMan = parameters.getAclServiceFactory().serviceFor(org);
final Date now = new Date();
final TransitionQuery q = TransitionQuery.query().before(now).withDone(false);
try {
final TransitionResult r = aclMan.getTransitions(q);
final List<ACLTransition> transitions = Collections.<ACLTransition>concat(r.getEpisodeTransistions(), r.getSeriesTransistions());
logger.debug("Found {} transition/s for organization {}", transitions.size(), org.getId());
for (final ACLTransition t : transitions) {
if (t instanceof EpisodeACLTransition) {
final EpisodeACLTransition et = (EpisodeACLTransition) t;
logger.info("Apply transition to episode {}", et.getEpisodeId());
aclMan.applyEpisodeAclTransition(et);
} else if (t instanceof SeriesACLTransition) {
final SeriesACLTransition st = (SeriesACLTransition) t;
logger.info("Apply transition to series {}", st.getSeriesId());
aclMan.applySeriesAclTransition(st);
} else {
unexhaustiveMatch();
}
}
} catch (AclServiceException e) {
logger.error("Error executing runner", e);
}
}
});
}
logger.debug("Finished ACL scheduling");
}
}
/**
* Please remember that derived classes need a no-arg constructor in order to work with Quartz. Sample usage:
*
* <pre>
* public class Runner extends TypedQuartzJob<Scheduler> {
* protected abstract void execute(Scheduler parameters, JobExecutionContext ctx) {
* // type safe parameter access
* parameters.getConfig();
* }
* }
*
* public class Scheduler {
* ...
* // create the job
* final JobDetail job = new JobDetail(JOB_NAME, JOB_GROUP, Runner.class);
* // set the scheduler as parameter source
* job.getJobDataMap().put(JOB_PARAM_PARENT, this);
* // add to Quartz scheduler
* quartz.addJob(job, true);
* ...
* // provide a config string
* public String getConfig() {...}
* }
* </pre>
*/
public abstract static class TypedQuartzJob<A> implements Job {
private final Option<NeedleEye> allowParallel;
/**
* @param allowParallel
* Pass a needle eye if only one job may be run at a time. Make the needle eye static to the inheriting
* class.
*/
protected TypedQuartzJob(Option<NeedleEye> allowParallel) {
this.allowParallel = allowParallel;
}
@Override
public final void execute(final JobExecutionContext ctx) throws JobExecutionException {
for (NeedleEye eye : allowParallel) {
eye.apply(executeF(ctx));
return;
}
executeF(ctx).apply();
}
/** Typesafe replacement for {@link #execute(org.quartz.JobExecutionContext)}. */
protected abstract void execute(A parameters, JobExecutionContext ctx);
private Function0<Integer> executeF(final JobExecutionContext ctx) {
return new Function0.X<Integer>() {
@Override
public Integer xapply() throws Exception {
try {
execute((A) ctx.getJobDetail().getJobDataMap().get(JOB_PARAM_PARENT), ctx);
return 0;
} catch (Exception e) {
logger.error("An error occurred while harvesting schedule", e);
throw new JobExecutionException("An error occurred while harvesting schedule", e);
}
}
};
}
}
}