package com.ibm.sbt.opensocial.domino.container;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.shindig.common.uri.Uri;
import org.apache.shindig.common.uri.Uri.UriException;
import org.apache.shindig.config.ContainerConfigException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionDelta;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IRegistryChangeEvent;
import org.eclipse.core.runtime.IRegistryChangeListener;
import com.google.caja.util.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Singleton;
import com.ibm.sbt.opensocial.domino.internal.OpenSocialPlugin;
/**
* Class that manages container extension points. If an application wishes to add a container to the
* OpenSocial implementation they first need to implement link com.ibm.sbt.opensocial.domino.container.ContainerExtPoint.
* Applications can contribute information about their containers via two mechanisms.
*
* <ol>
* <li>Via the OSGi extension point com.ibm.sbt.opensocial.domino.container.</li>
* <li>Via link #registerContainer(ContainerExtPoint). When using link #registerContainer(ContainerExtPoint)
* make sure you also call link #unregisterContainer(ContainerExtPoint) when the app is terminated.</li>
* </ol>
*
*/
@Singleton
public class ContainerExtPointManager {
private static final String EXT_PT_ID = "com.ibm.sbt.opensocial.domino.container";
private static final String CLASS_ATR = "class";
private static final String CLASS = ContainerExtPointManager.class.getName();
private static Map<String, ContainerExtPoint> containers = Maps.newConcurrentMap();
private static List<ContainerExtPointListener> listeners =
Collections.synchronizedList(new ArrayList<ContainerExtPointListener>());
private IExtensionRegistry registry;
private Logger log;
private IRegistryChangeListener registryChangeListener = new IRegistryChangeListener() {
@Override
public void registryChanged(IRegistryChangeEvent event) {
IExtensionDelta[] deltas = event.getExtensionDeltas(OpenSocialPlugin.ID, EXT_PT_ID);
for(int i = 0; i < deltas.length; i++) {
IExtensionDelta delta = deltas[i];
IExtension ext = delta.getExtension();
modifyContainers(ext.getConfigurationElements(), delta.getKind() == IExtensionDelta.ADDED);
}
}
};
/**
* Creates a new container extension point manager.
*/
public ContainerExtPointManager(IExtensionRegistry registry, Logger logger) {
this.registry = registry;
this.log = logger;
loadOSGiExtPoints();
}
private void loadOSGiExtPoints() {
registry.addRegistryChangeListener(registryChangeListener, OpenSocialPlugin.ID);
IConfigurationElement[] elements = registry.getConfigurationElementsFor(EXT_PT_ID);
modifyContainers(elements, true);
}
private void modifyContainers(IConfigurationElement[] elements, boolean add) {
final String method = "modifyContainers";
List<ContainerExtPoint> containers = Lists.newArrayList();
for(int i = 0; i < elements.length; i++) {
IConfigurationElement element = elements[i];
try {
Object containerObject = element.createExecutableExtension(CLASS_ATR);
if(containerObject instanceof ContainerExtPoint) {
containers.add((ContainerExtPoint)containerObject);
}
} catch (CoreException e) {
log.logp(Level.WARNING, CLASS, method,
"Error adding container extension point from OSGi extension point.", e);
}
}
if(add) {
try {
registerContainers(containers);
} catch (ContainerConfigException e) {
log.logp(Level.WARNING, CLASS, method, "Error registering containers from extension point.", e);
}
} else {
try {
unregisterContainers(containers);
} catch (ContainerConfigException e) {
log.logp(Level.WARNING, CLASS, method, "Error un-registering containers from extension point.", e);
}
}
}
/**
* Gets a unmodifiable copy of all the container extension points.
* @return All the container extension points. The key to the map is the container name.
*/
public Map<String, ContainerExtPoint> getExtPoints() {
return Collections.unmodifiableMap(containers);
}
/**
* Gets a container extension point.
* @param container The container name.
* @return A container extension point.
*/
public ContainerExtPoint getExtPoint(String container) {
return containers.get(container);
}
/**
* Registers containers with the OpenSocial implementation.
* @param containers The container to register.
* @throws ContainerConfigException Thrown if there is a container extension point that is not valid.
*/
public static void registerContainers(Collection<ContainerExtPoint> containers) throws ContainerConfigException {
for(ContainerExtPoint extPoint : containers) {
try {
validateContainerId(extPoint.getId());
ContainerExtPointManager.containers.put(extPoint.getId(), extPoint);
} catch (ContainerExtPointException e) {
throw new ContainerConfigException(e);
}
}
synchronized(listeners) {
for(ContainerExtPointListener listener : listeners) {
listener.added(containers);
}
}
}
/**
* Validates that the container IDs. Container IDs MUST
* <ul>
* <li>be URL encoded</li>
* <li>NOT contain a colon</li>
* </ul>
* This is due to limitations in Shindig. We enforce this here to make sure consumers
* do not harm themselves and then be left scratching their heads when things go wrong.
* Container IDs are part of the security token in Shindig and there are places in Shindig where
* the assumption is made that the security token is safe to place in a URL. So if the container ID
* has a space in it for example we will get errors because Shindig does not try to URL encoded the
* security token. Container IDs cannot container colons because Shindig uses colons as separators
* for the different parts of the security token. If a container ID contains a colon then the code
* in Shindig that parses the security token will be confused by the extra colons.
* @param id The container ID to validate.
* @throws ContainerConfigException Thrown when the container ID is not valid.
*/
private static void validateContainerId(String id) throws ContainerConfigException {
try{
//This is an easy way to make sure the id is URL encoded, if parse throws an error
//than there is a problem.
Uri.parse("http://foo/" + id);
} catch(UriException e) {
throw new ContainerConfigException("Container IDs must be URL encoded. ID: " + id, e);
}
if(id.contains(":")) {
throw new ContainerConfigException("Container IDs cannot contain colons. ID: " + id);
}
}
/**
* Unregisters containers with the OpenSocial implementation.
* @param containers The container to unregister.
* @throws ContainerConfigException Thrown if there is an error un-registering the containers.
*/
public static void unregisterContainers(Collection<ContainerExtPoint> containers) throws ContainerConfigException {
for(ContainerExtPoint extPoint : containers) {
try {
ContainerExtPointManager.containers.remove(extPoint.getId());
} catch (ContainerExtPointException e) {
throw new ContainerConfigException(e);
}
}
synchronized(listeners) {
for(ContainerExtPointListener listener : listeners) {
listener.removed(containers);
}
}
}
/**
* Adds an extension point listener to be notified when new extension points are added and removed.
* @param listener The listener to be notified.
*/
public void addExtensionPointListener(ContainerExtPointListener listener) {
listeners.add(listener);
}
/**
* Removes an extension point listener.
* @param listener The listener to be removed.
*/
public void removeExtensionPointListener(ContainerExtPointListener listener) {
listeners.remove(listener);
}
/**
* Listener to be notified when new container extension points are added or removed.
*
*/
public interface ContainerExtPointListener {
/**
* Called when new container extension points are added.
* @param extPoints The new extension points that were added.
*/
public void added(Collection<ContainerExtPoint> extPoints);
/**
* Called when container extension points are removed.
* @param extPoints The extension points that were removed.
*/
public void removed(Collection<ContainerExtPoint> extPoints);
}
}