/**
* 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.engage.theodul.manager.impl;
import static org.joda.time.DateTimeConstants.MILLIS_PER_SECOND;
import org.opencastproject.engage.theodul.api.EngagePlugin;
import org.opencastproject.engage.theodul.api.EngagePluginManager;
import org.opencastproject.engage.theodul.api.EngagePluginRegistration;
import org.opencastproject.engage.theodul.api.EngagePluginRestService;
import org.opencastproject.kernel.rest.RestPublisher;
import org.opencastproject.rest.StaticResource;
import org.opencastproject.util.OsgiUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URL;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
/**
* A service that tracks the de-/registration of Theodul Player Plugins and
* de-/installs static resource and REST endpoint servlets under a shared URL.
*/
public class EngagePluginManagerImpl implements EngagePluginManager, ServiceListener {
private static final Logger log = LoggerFactory.getLogger(EngagePluginManagerImpl.class);
static final String PLUGIN_URL_PREFIX = "/engage/theodul/plugin/";
private BundleContext bundleContext;
private static final String pluginServiceFilter = "(objectClass=" + EngagePlugin.class.getName() + ")";
private final PluginDataStore plugins = new PluginDataStore();
protected void activate(BundleContext bc) {
bundleContext = bc;
try {
bundleContext.addServiceListener(this, pluginServiceFilter);
} catch (InvalidSyntaxException ex) {
log.error("Could not register as ServiceListener: " + ex.getMessage());
throw new RuntimeException(ex);
}
log.info("Activated. Listening for Theodul Plugins. filter=" + pluginServiceFilter);
}
private BundleContext getKernelBundleContext() {
BundleContext context = FrameworkUtil.getBundle(RestPublisher.class).getBundleContext();
while (context == null) {
log.info("Waiting for the kernel bundle to become active...");
try {
Thread.sleep(MILLIS_PER_SECOND);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for kernel bundle");
}
context = FrameworkUtil.getBundle(RestPublisher.class).getBundleContext();
}
return context;
}
protected void deactivate(ComponentContext cc) {
cc.getBundleContext().removeServiceListener(this);
uninstallAll();
log.info("Deactivated.");
}
@Override
public void serviceChanged(ServiceEvent se) {
ServiceReference sref = se.getServiceReference();
switch (se.getType()) {
case ServiceEvent.REGISTERED:
try {
installPlugin(sref);
} catch (Exception e) {
log.error("Failed to install Theodul Plugin: " + e.getMessage(), e);
}
break;
case ServiceEvent.UNREGISTERING:
try {
uninstallPlugin(sref);
} catch (Exception e) {
log.error("Error while uninstalling Theodul Plugin: " + e.getMessage(), e);
}
break;
default:
break;
}
}
private void installPlugin(ServiceReference sref) throws IllegalArgumentException {
PluginData plugin = new PluginData(sref);
// try to install static resources if available
if (plugin.providesStaticResources()) {
try {
plugin.setStaticResourceRegistration(installStaticResources(plugin));
} catch (Exception ex) {
log.warn("Unable to install static resources.", ex);
}
}
// try to install REST endpoint if available
if (plugin.providesRestEndpoint()) {
try {
plugin.setRestEndpointRegistration(installRestEndpoint(plugin));
} catch (Exception ex) {
log.warn("Unable to install REST endpoint.", ex);
}
}
// make sure we have no useless plugin after all
if (plugin.getStaticResourceRegistration() == null
&& plugin.getRestEndpointRegistration() == null) {
throw new IllegalStateException("Neither static resources nor a REST endpoint were registered, canceling plugin installation");
}
plugins.add(plugin);
// construct and log success message
log.info("Installed Theodul plugin {} (static: {} REST: {})", new Object[] {
plugin.getName(),
(plugin.getStaticResourceRegistration() != null) ? plugin.getStaticResourcesPath() : "no",
(plugin.getRestEndpointRegistration() != null) ? plugin.getRestPath() : "no"
});
}
/** Registers a <code>StaticResource</code> that serves the contents of the
* plugins /static resource directory.
*
* @return ServiceRegistration for the StaticResource
*/
private ServiceRegistration installStaticResources(PluginData plugin) throws Exception {
StaticResource staticResource = new StaticResource(
new BundleDelegatingClassLoader(plugin.getServiceReference().getBundle()),
EngagePlugin.STATIC_RESOURCES_PATH, plugin.getStaticResourcesPath(), null);
return OsgiUtil.registerServlet(getKernelBundleContext(), staticResource, PLUGIN_URL_PREFIX + plugin.getStaticResourcesPath());
}
/** Publishes the REST endpoint implemented by the plugin bundle.
*
* @return ServiceRegistration for the REST endpoint
*/
private ServiceRegistration installRestEndpoint(PluginData plugin) throws Exception {
EngagePlugin service = (EngagePlugin) bundleContext.getService(plugin.getServiceReference());
Dictionary<String, String> props = new Hashtable<String, String>();
props.put("service.description", plugin.getDescription());
props.put("opencast.service.type", "org.opencast.engage.plugin." + Integer.toString(plugin.getPluginID()));
props.put("opencast.service.path", PLUGIN_URL_PREFIX + plugin.getRestPath());
// Note: Due to a limitation in Pax Web 3.x which does not allow to share a HTTP client between bundles, the
// servlets all need to be registered in the context of the kernel bundle.
// See https://ops4j1.jira.com/browse/PAXWEB-558 for more details.
return getKernelBundleContext().registerService(EngagePluginRestService.class.getName(), service, props);
}
private void uninstallPlugin(ServiceReference sref) {
PluginData plugin = plugins.getByServiceReference(sref);
if (plugin != null) {
// uninstall static resources
ServiceRegistration staticReg = plugin.getStaticResourceRegistration();
if (staticReg != null) {
log.info("Unregistering static resources for plugin " + plugin.getName());
staticReg.unregister();
}
// uninstall REST endpoint
ServiceRegistration restReg = plugin.getRestEndpointRegistration();
if (restReg != null) {
log.info("Unregistering REST endpoint for plugin " + plugin.getName());
restReg.unregister();
}
plugins.remove(plugin);
} else {
throw new IllegalArgumentException("Unable to uninstall plugin. No plugin registered with the given ServiceReference.");
}
}
private void uninstallAll() {
for (PluginData plugin : plugins.getAll()) {
uninstallPlugin(plugin.getServiceReference());
}
}
@Override
public List<EngagePluginRegistration> getAllRegisteredPlugins() {
synchronized (plugins) {
List<EngagePluginRegistration> list = new ArrayList<EngagePluginRegistration>();
for (PluginData plugin : plugins.getAll()) {
EngagePluginRegistrationImpl reg = new EngagePluginRegistrationImpl(
plugin.getPluginID(), plugin.getName(), plugin.getDescription(),
plugin.providesStaticResources() ? plugin.getStaticResourcesPath() : null,
plugin.providesRestEndpoint() ? plugin.getRestPath() : null);
list.add(reg);
}
return list;
}
}
class BundleDelegatingClassLoader extends ClassLoader {
private Bundle bundle;
BundleDelegatingClassLoader(Bundle bundle) {
super();
this.bundle = bundle;
}
@Override
public URL getResource(String path) {
return bundle.getResource(path);
}
}
class PluginDataStore {
private final Set<PluginData> data = new HashSet<PluginData>();
public synchronized int size() {
return data.size();
}
public synchronized void add(PluginData p) {
data.add(p);
}
public synchronized void remove(PluginData p) {
data.remove(p);
}
public synchronized PluginData[] getAll() {
return data.toArray(new PluginData[0]);
}
public synchronized PluginData getByName(String name) {
for (PluginData p : data) {
if (p.getName().equals(name)) {
return p;
}
}
return null;
}
public boolean containsWithName(String name) {
return null != getByName(name);
}
public synchronized PluginData getByPath(String path) {
for (PluginData p : data) {
if (p.getName().equals(path)) {
return p;
}
}
return null;
}
public boolean containsWithPath(String path) {
return null != getByPath(path);
}
public synchronized PluginData getByServiceReference(ServiceReference sref) {
for (PluginData p : data) {
if (p.getServiceReference().equals(sref)) {
return p;
}
}
return null;
}
}
}