/*
* Copyright 2010-2013 Ning, Inc.
*
* Ning licenses this file to you under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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 com.ning.billing.osgi.bundles.jruby;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import org.jruby.Ruby;
import org.jruby.RubyObject;
import org.jruby.embed.EvalFailedException;
import org.jruby.embed.ScriptingContainer;
import org.jruby.runtime.builtin.IRubyObject;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.log.LogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ning.billing.osgi.api.config.PluginRubyConfig;
import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
// Bridge between the OSGI bundle and the ruby plugin
public abstract class JRubyPlugin {
private final static Logger log = LoggerFactory.getLogger(JRubyPlugin.class);
// Killbill gem base classes
private static final String KILLBILL_PLUGIN_BASE = "Killbill::Plugin::PluginBase";
private static final String KILLBILL_PLUGIN_NOTIFICATION = "Killbill::Plugin::Notification";
private static final String KILLBILL_PLUGIN_PAYMENT = "Killbill::Plugin::Payment";
// Magic ruby variables
private static final String KILLBILL_SERVICES = "java_apis";
private static final String KILLBILL_PLUGIN_CLASS_NAME = "plugin_class_name";
protected final LogService logger;
protected final BundleContext bundleContext;
protected final String pluginGemName;
protected final String rubyRequire;
protected final String pluginMainClass;
protected final ScriptingContainer container;
protected final String pluginLibdir;
protected RubyObject pluginInstance;
private ServiceRegistration httpServletServiceRegistration = null;
private String cachedRequireLine = null;
public JRubyPlugin(final PluginRubyConfig config, final ScriptingContainer container,
final BundleContext bundleContext, final LogService logger) {
this.logger = logger;
this.bundleContext = bundleContext;
this.pluginGemName = config.getPluginName();
this.rubyRequire = config.getRubyRequire();
this.pluginMainClass = config.getRubyMainClass();
this.container = container;
this.pluginLibdir = config.getRubyLoadDir();
// Path to the gem
if (pluginLibdir != null) {
container.setLoadPaths(Arrays.asList(pluginLibdir));
}
}
public String getPluginMainClass() {
return pluginMainClass;
}
public String getPluginLibdir() {
return pluginLibdir;
}
public void instantiatePlugin(final Map<String, Object> killbillApis, final String pluginMain) {
checkValidPlugin();
// Register all killbill APIs
container.put(KILLBILL_SERVICES, killbillApis);
container.put(KILLBILL_PLUGIN_CLASS_NAME, pluginMainClass);
// Note that the KILLBILL_SERVICES variable will be available once only!
// Don't put any code here!
// Start the plugin
pluginInstance = (RubyObject) container.runScriptlet(pluginMain + ".new(" + KILLBILL_PLUGIN_CLASS_NAME + "," + KILLBILL_SERVICES + ")");
}
public void startPlugin(final BundleContext context) {
checkPluginIsStopped();
pluginInstance.callMethod("start_plugin");
checkPluginIsRunning();
registerHttpServlet();
}
public void stopPlugin(final BundleContext context) {
checkPluginIsRunning();
unregisterHttpServlet();
pluginInstance.callMethod("stop_plugin");
checkPluginIsStopped();
}
private void registerHttpServlet() {
// Register the rack handler
final IRubyObject rackHandler = pluginInstance.callMethod("rack_handler");
if (!rackHandler.isNil()) {
logger.log(LogService.LOG_INFO, String.format("Using %s as rack handler", rackHandler.getMetaClass()));
final JRubyHttpServlet jRubyHttpServlet = new JRubyHttpServlet(rackHandler);
final Hashtable<String, String> properties = new Hashtable<String, String>();
properties.put("killbill.pluginName", pluginGemName);
httpServletServiceRegistration = bundleContext.registerService(HttpServlet.class.getName(), jRubyHttpServlet, properties);
}
}
private void unregisterHttpServlet() {
if (httpServletServiceRegistration != null) {
httpServletServiceRegistration.unregister();
}
}
protected void checkPluginIsRunning() {
if (pluginInstance == null || !(Boolean) pluginInstance.callMethod("is_active").toJava(Boolean.class)) {
throw new IllegalStateException(String.format("Plugin %s didn't start properly", pluginMainClass));
}
}
protected void checkPluginIsStopped() {
if (pluginInstance == null || (Boolean) pluginInstance.callMethod("is_active").toJava(Boolean.class)) {
throw new IllegalStateException(String.format("Plugin %s didn't stop properly", pluginMainClass));
}
}
protected void checkValidPlugin() {
try {
container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_BASE));
} catch (EvalFailedException e) {
throw new IllegalArgumentException(e);
}
}
protected void checkValidNotificationPlugin() throws IllegalArgumentException {
try {
container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_NOTIFICATION));
} catch (EvalFailedException e) {
throw new IllegalArgumentException(e);
}
}
protected void checkValidPaymentPlugin() throws IllegalArgumentException {
try {
container.runScriptlet(checkInstanceOfPlugin(KILLBILL_PLUGIN_PAYMENT));
} catch (EvalFailedException e) {
throw new IllegalArgumentException(e);
}
}
protected String checkInstanceOfPlugin(final String baseClass) {
final StringBuilder builder = new StringBuilder(getRequireLine());
builder.append("raise ArgumentError.new('Invalid plugin: ")
.append(pluginMainClass)
.append(", is not a ")
.append(baseClass)
.append("') unless ")
.append(pluginMainClass)
.append(" <= ")
.append(baseClass);
return builder.toString();
}
private String getRequireLine() {
if (cachedRequireLine == null) {
final StringBuilder builder = new StringBuilder();
builder.append("ENV[\"GEM_HOME\"] = \"").append(pluginLibdir).append("\"").append("\n");
builder.append("ENV[\"GEM_PATH\"] = ENV[\"GEM_HOME\"]\n");
// Always require the Killbill gem
builder.append("gem 'killbill'\n");
builder.append("require 'killbill'\n");
// Assume the plugin is shipped as a Gem
builder.append("begin\n")
.append("gem '").append(pluginGemName).append("'\n")
.append("rescue Gem::LoadError\n")
.append("warn \"WARN: unable to load gem ").append(pluginGemName).append("\"\n")
.append("end\n");
builder.append("begin\n")
.append("require '").append(pluginGemName).append("'\n")
.append("rescue LoadError\n")
// Could be useful for debugging
//.append("warn \"WARN: unable to require ").append(pluginGemName).append("\"\n")
.append("end\n");
// Load the extra require file, if specified
if (rubyRequire != null) {
builder.append("begin\n")
.append("require '").append(rubyRequire).append("'\n")
.append("rescue LoadError\n")
.append("warn \"WARN: unable to require ").append(rubyRequire).append("\"\n")
.append("end\n");
}
// Require any file directly in the pluginLibdir directory (e.g. /var/tmp/bundles/ruby/foo/1.0/gems/*.rb).
// Although it is likely that any Killbill plugin will be distributed as a gem, it is still useful to
// be able to load individual scripts for prototyping/testing/...
builder.append("Dir.glob(ENV[\"GEM_HOME\"] + \"/*.rb\").each {|x| require x rescue warn \"WARN: unable to load #{x}\"}\n");
cachedRequireLine = builder.toString();
}
return cachedRequireLine;
}
protected Ruby getRuntime() {
return pluginInstance.getMetaClass().getRuntime();
}
public enum VALIDATION_PLUGIN_TYPE {
NOTIFICATION,
PAYMENT,
NONE
}
protected abstract class PluginCallback {
private final VALIDATION_PLUGIN_TYPE pluginType;
public PluginCallback(final VALIDATION_PLUGIN_TYPE pluginType) {
this.pluginType = pluginType;
}
public abstract <T> T doCall(final Ruby runtime) throws PaymentPluginApiException;
public VALIDATION_PLUGIN_TYPE getPluginType() {
return pluginType;
}
}
protected <T> T callWithRuntimeAndChecking(final PluginCallback cb) throws PaymentPluginApiException {
try {
checkPluginIsRunning();
switch(cb.getPluginType()) {
case NOTIFICATION:
checkValidNotificationPlugin();
break;
case PAYMENT:
checkValidPaymentPlugin();
break;
default:
break;
}
final Ruby runtime = getRuntime();
return cb.doCall(runtime);
} catch (RuntimeException e) {
log.warn("RuntimeException in jruby plugin ", e);
throw e;
}
}
}