/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.policy.js;
import org.candlepin.model.Rules;
import org.candlepin.model.Rules.RulesSourceEnum;
import org.candlepin.model.RulesCurator;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* Reads/compiles our javascript rules and the standard js objects only
* once across the JVM lifetime (and whenever the rules require a recompile), and creates
* lightweight execution scopes per thread/request.
*/
public class JsRunnerProvider implements Provider<JsRunner> {
private static Logger log = LoggerFactory.getLogger(JsRunnerProvider.class);
private RulesCurator rulesCurator;
private Provider<JsRunnerRequestCache> cacheProvider;
private Script script;
private Scriptable scope;
/**
* This date is basically a version of the rules that this
* JSRunnerProvider compiled. Note that in clustered environment,
* multiple nodes must compile same version of rules. Thats why
* this JsRunnerProvider uses database to make sure it compiles and
* uses the database dictated version.
*/
private volatile Date currentRulesUpdated;
// Store the version and source of the compiled rules:
private String rulesVersion;
private RulesSourceEnum rulesSource;
// Use this lock to access script, scope and updated
private ReadWriteLock scriptLock = new ReentrantReadWriteLock();
/**
* DynamicScopeContextFactory - replace the standard rhino context factory with one that
* enables dynamic scopes. Dynamic scopes allow us to define a global var (ie pools) in
* a thread/request local scope, and have the global scope (where our js code lives) be
* able to read it.
*/
static class DynamicScopeContextFactory extends ContextFactory {
@Override
protected boolean hasFeature(Context cx, int featureIndex) {
if (featureIndex == Context.FEATURE_DYNAMIC_SCOPE) {
return true;
}
return super.hasFeature(cx, featureIndex);
}
}
static {
ContextFactory.initGlobal(new DynamicScopeContextFactory());
}
@Inject
public JsRunnerProvider(RulesCurator rulesCurator, Provider<JsRunnerRequestCache> cacheProvider) {
this.rulesCurator = rulesCurator;
this.cacheProvider = cacheProvider;
log.debug("Compiling rules for initial load");
this.rulesCurator.updateDbRules();
this.compileRules();
}
/**
* These are the expensive operations (initStandardObjects and compileReader/exec).
* We do them once here, and define this provider as a singleton, so it's only
* done at provider creation or whenever rules are refreshed.
*
* @param rulesCurator
*/
public void compileRules() {
compileRules(false);
}
public void compileRules(boolean forceRefresh) {
scriptLock.writeLock().lock();
try {
// Check to see if we need to recompile. we do this inside the write lock
// just to avoid race conditions where we might double compile
Date newUpdated = rulesCurator.getUpdated();
if (!forceRefresh && newUpdated.equals(this.currentRulesUpdated)) {
return;
}
log.info("Recompiling rules with timestamp: " + newUpdated);
Context context = Context.enter();
context.setOptimizationLevel(9);
scope = context.initStandardObjects(null, true);
try {
Rules rules = rulesCurator.getRules();
rulesVersion = rules.getVersion();
rulesSource = rules.getRulesSource();
script = context.compileString(
rules.getRules(), "rules", 1, null);
script.exec(context, scope);
((ScriptableObject) scope).sealObject();
this.currentRulesUpdated = newUpdated;
}
finally {
Context.exit();
}
}
finally {
scriptLock.writeLock().unlock();
}
}
public JsRunner get() {
/**
* Even though JsRunnerProvider is singleton, the
* following cache is being retrieved fresh for
* every new HTTP Request
*/
JsRunnerRequestCache cache = cacheProvider.get();
Date updated = cache.getUpdated();
if (updated == null) {
updated = rulesCurator.getUpdated();
cache.setUpdated(updated);
}
/*
* Create a new thread/request local javascript scope for the JsRules,
* based on the preinitialized global one (which contains our js rules).
*/
// Avoid a write lock if we can
if (!updated.equals(this.currentRulesUpdated)) {
compileRules();
}
Scriptable rulesScope;
scriptLock.readLock().lock();
try {
Context context = Context.enter();
rulesScope = context.newObject(scope);
rulesScope.setPrototype(scope);
rulesScope.setParentScope(null);
Context.exit();
}
finally {
scriptLock.readLock().unlock();
}
return new JsRunner(rulesScope);
}
public String getRulesVersion() {
if (rulesVersion == null) {
compileRules();
}
return rulesVersion;
}
public RulesSourceEnum getRulesSource() {
if (rulesSource == null) {
compileRules();
}
return rulesSource;
}
}