package glaze.spi;
import glaze.GlazeException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
import org.apache.http.entity.ContentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.module.afterburner.AfterburnerModule;
/**
* <p>
* Simple pluggable registry for services, mappers and hooks. To plug in a
* service you need to create the convenient
* 'META-INF/services/full.interface.name' containing the full class names of
* your implementations separated by new lines.
* </p>
* Example:<br/>
*
* <strong>Custom HttpClient</strong>
*
* 1) implement the ServiceContrib
*
* <pre>
* package mysvc.pkg;
*
* import ...;
*
* public class HttpClientContrib implements ServiceContrib {
* public Class<?> serviceClass() {
* return HttpClient.class;
* }
*
* public Object serviceImpl() {
* return new DefaultHttpClient();
* }
* }
* </pre>
*
* 2) in '<em>META-INF/services/marmalade.spi.ServiceContrib</em>' add the
* following line:
*
* <pre>
* mysvc.pkg.HttpClientContrib
* </pre>
*
* <strong>Register an ObjectMapper for a content-type</strong>
*
* <pre>
* [TODO document]
* </pre>
*
* @see ServiceLoader
*/
public class Registry
{
private static final RegistryShutdownHook REGISTRY_SHUTDOWN_HOOK = new RegistryShutdownHook();
public static final String NS_DEFAULT = "default";
private final static ServiceLoader<HookProvider> hookContribs = ServiceLoader.load(HookProvider.class);;
@SuppressWarnings("rawtypes")
private final static ServiceLoader<ServiceProvider> serviceContribs = ServiceLoader.load(ServiceProvider.class);
private final static ServiceLoader<MapperProvider> mapperContribs = ServiceLoader.load(MapperProvider.class);
private static final Logger LOGGER = LoggerFactory.getLogger(Registry.class);
private static final Map<String, Registry> instances = new HashMap<String, Registry>();
private final Map<Class<?>, Object> services;
private final Map<String, ObjectMapper> mappers;
private final String namespace;
static {
synchronized (instances) {
initialize();
}
}
public static Registry defaultedInstance(String namespace)
{
return instances.containsKey(namespace) ? instances.get(namespace) : instance();
}
public static Registry instance()
{
return instances.get(NS_DEFAULT);
}
public static Registry instance(String namespace)
{
return instances.get(namespace);
}
public static Collection<Registry> instances()
{
return instances.values();
}
public static boolean isRegitered(Class<?> type)
{
return isRegitered(NS_DEFAULT, type);
}
public static boolean isRegitered(String namespace, Class<?> type)
{
return instance(namespace).services.containsKey(type);
}
public static <T> T lookup(Class<T> type)
{
return lookup(NS_DEFAULT, type);
}
@SuppressWarnings("unchecked")
public static <T> T lookup(String namespace, Class<T> type)
{
Object service = instance(namespace).services.get(type);
if (service == null) {
throw new GlazeException(String.format("Service '%s' not found.\n%s", type, instance(namespace)));
}
return (T) service;
}
public static ObjectMapper lookupMapper(ContentType type)
{
return lookupMapper(NS_DEFAULT, type.getMimeType());
}
public static ObjectMapper lookupMapper(String contentType)
{
return lookupMapper(NS_DEFAULT, contentType);
}
public static ObjectMapper lookupMapper(String namespace, ContentType type)
{
ObjectMapper mapper = instance(namespace).mappers.get(type.getMimeType());
return mapper == null ? instance().mappers.get(type.getMimeType()) : mapper;
}
public static ObjectMapper lookupMapper(String namespace, String contentType)
{
return lookupMapper(namespace, ContentType.parse(contentType));
}
static Registry getOrCreate(Object contrib)
{
Named named = contrib.getClass().getAnnotation(Named.class);
Registry registry = named == null ? instance() : instance(named.value());
if (registry == null && named != null) {
LOGGER.info("Creating registry for namespace: '{}'", named.value());
registry = new Registry(named.value());
instances.put(named.value(), registry);
}
return registry;
}
static void reset()
{
synchronized (instances) {
instances.clear();
}
initialize();
}
private static void initialize()
{
if (!instances.containsKey(NS_DEFAULT)) {
Registry defaultInstance = new Registry(NS_DEFAULT);
instances.put(NS_DEFAULT, defaultInstance);
Runtime.getRuntime().removeShutdownHook(REGISTRY_SHUTDOWN_HOOK);
Runtime.getRuntime().addShutdownHook(REGISTRY_SHUTDOWN_HOOK);
registerServices();
registerMappers();
registerHooks();
logInstancesState();
}
}
private static void logInstancesState()
{
for (Registry registry : instances.values()) {
registry.logState();
}
}
private static void registerHooks()
{
// TODO advise hooks on registration?
for (HookProvider hook : hookContribs) {
Registry registry = getOrCreate(hook);
for (Map.Entry<Class<?>, Object> entry : registry.services.entrySet()) {
if (hook.acceptService(entry.getKey())) {
hook.visitService(entry.getKey(), entry.getValue());
}
}
for (Map.Entry<String, ObjectMapper> entry : registry.mappers.entrySet()) {
if (hook.acceptMapper(entry.getKey())) {
hook.visitMapper(entry.getKey(), entry.getValue());
}
}
}
}
private static void registerMappers()
{
// Register mapper contribs
for (MapperProvider contrib : mapperContribs) {
Registry registry = getOrCreate(contrib);
registry.registerMapper(contrib.mimeType(), contrib.mapper());
}
Registry defaultRegistry = instance();
// Assure that mappers for JSON and XML are available
if (!defaultRegistry.isMapperRegistered(ContentType.APPLICATION_JSON)) {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
mapper.disable(SerializationFeature.WRITE_NULL_MAP_VALUES);
mapper.setSerializationInclusion(Include.NON_NULL);
mapper.registerModule(new AfterburnerModule());
defaultRegistry.registerMapper(ContentType.APPLICATION_JSON, mapper);
}
if (!defaultRegistry.isMapperRegistered(ContentType.APPLICATION_XML)) {
JacksonXmlModule module = new JacksonXmlModule();
module.setDefaultUseWrapper(false);
XmlMapper mapper = new XmlMapper(module);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
defaultRegistry.registerMapper(ContentType.APPLICATION_XML, mapper);
}
}
private static void registerServices()
{
for (ServiceProvider<?> contrib : serviceContribs) {
Registry registry = getOrCreate(contrib);
registry.register(contrib.serviceClass(), contrib.serviceImpl());
}
}
private Registry(String namespace)
{
this.namespace = namespace;
this.services = new HashMap<Class<?>, Object>();
this.mappers = new HashMap<String, ObjectMapper>();
}
public boolean isMapperRegistered(ContentType type)
{
return mappers.containsKey(type.getMimeType());
}
public String namespace()
{
return namespace;
}
public void register(Class<?> serviceClass, Object serviceImpl)
{
synchronized (services) {
services.put(serviceClass, serviceImpl);
}
}
public void registerMapper(ContentType type, ObjectMapper mapper)
{
registerMapper(type.getMimeType(), mapper);
}
public void registerMapper(String mimeType, ObjectMapper mapper)
{
synchronized (mappers) {
mappers.put(mimeType, mapper);
}
}
@Override
public String toString()
{
return "Registry [services=" + services + ", mappers=" + mappers + "]";
}
@SuppressWarnings("unchecked")
public <T> T unregister(Class<?> type)
{
synchronized (services) {
return (T) services.remove(type);
}
}
public ObjectMapper unregisterMapper(ContentType type)
{
return unregisterMapper(type.getMimeType());
}
public ObjectMapper unregisterMapper(String mime)
{
synchronized (mappers) {
return mappers.remove(mime);
}
}
void logState()
{
String title = "Glaze Registry";
StringBuilder msg = new StringBuilder("\n");
msg.append(title);
msg.append("\n");
msg.append(new String(new char[title.length()]).replace("\0", "="));
msg.append("\nNamespace: '");
msg.append(namespace);
msg.append("'\n\nServices:\n");
if (services.isEmpty()) {
msg.append("* [Empty]\n");
}
for (Map.Entry<Class<?>, Object> entry : services.entrySet()) {
msg.append(String.format("* %s => %s\n", entry.getKey().getSimpleName(), entry.getValue().getClass()));
}
msg.append("\nMappers:\n");
if (mappers.isEmpty()) {
msg.append("* [Empty]\n");
}
for (String mtype : mappers.keySet()) {
msg.append(String.format("* %s\n", mtype));
}
msg.append("\n~\n\n");
LOGGER.info(msg.toString());
}
protected Map<Class<?>, Object> services()
{
return services;
}
}