package com.networknt.service;
import com.networknt.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.Introspector;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;
/**
* Created by steve on 2016-11-26.
*/
public class SingletonServiceFactory {
static String CONFIG_NAME = "service";
static Logger logger = LoggerFactory.getLogger(SingletonServiceFactory.class);
private static Map<Class, Object> serviceMap = new HashMap<>();
// map is statically loaded to make sure there is only one instance per jvm
static {
ServiceConfig serviceConfig =
(ServiceConfig) Config.getInstance().getJsonObjectConfig(CONFIG_NAME, ServiceConfig.class);
List<Map<String, List<Object>>> singletons = serviceConfig.getSingletons();
//logger.debug("singletons " + singletons);
try {
if(singletons != null && singletons.size() > 0) {
for(Map<String, List<Object>> singleton: singletons) {
Iterator it = singleton.entrySet().iterator();
if (it.hasNext()) {
Map.Entry<String, List<Object>> pair = (Map.Entry)it.next();
String key = pair.getKey();
key = key.replaceAll("\\s+","");
List<Object> value = pair.getValue();
handleSingleton(key, value);
}
}
}
} catch (Exception e) {
logger.error("Exception:", e);
}
}
public static void handleSingleImpl(List<Class> interfaceClasses, List<Object> value) throws Exception {
// only one object should be defined in value. TODO throws exception if number of object is not correct.
Object object = value.get(0);
if(object instanceof String) {
Class implClass = Class.forName((String)object);
Object obj = construct(implClass);
for(Class c: interfaceClasses) {
serviceMap.put(c, obj); // all interfaces share the same impl
}
} else {
// map of impl class and properties.
Map<String, Map<String, Object>> map = (Map<String, Map<String, Object>>)object;
//logger.debug("map = " + map);
// construct it using default construct and call all set methods with values defined in the properties
Iterator it = map.entrySet().iterator();
if (it.hasNext()) {
Map.Entry<String, Map<String, Object>> pair = (Map.Entry) it.next();
String key = pair.getKey();
Class implClass = Class.forName(key);
Object mapOrList = pair.getValue();
// at this moment, pair.getValue() has two scenarios,
// 1. map that can be used to set properties after construct the object with reflection.
// 2. list that can be used by matched constructor to create the instance.
Object obj = null;
if(mapOrList instanceof Map) {
obj = construct(implClass);
Method[] allMethods = implClass.getMethods();
for(Method method : allMethods) {
if(method.getName().startsWith("set")) {
//logger.debug("method name " + method.getName());
Object [] o = new Object [1];
String propertyName = Introspector.decapitalize(method.getName().substring(3));
Object v = ((Map)mapOrList).get(propertyName);
if(v == null) {
// it is not primitive type, so find the object in service map.
Class<?>[] pType = method.getParameterTypes();
v = serviceMap.get(pType[0]);
}
if(v != null) {
o[0] = v;
method.invoke(obj, o);
}
}
}
} else if(mapOrList instanceof List){
obj = constructWithParameters(implClass, (List)mapOrList);
} else {
throw new RuntimeException("Only Map or List is allowed for implementation parameters " + mapOrList);
}
for(Class c: interfaceClasses) {
serviceMap.put(c, obj); // all interfaces share the same impl
}
}
}
}
public static void handleMultipleImpl(List<Class> interfaceClasses, List<Object> value) throws Exception {
List<Object> arrays = interfaceClasses.stream().map(c -> Array.newInstance(c, value.size())).collect(Collectors.toList());
for(int i = 0; i < value.size(); i++) {
Object object = value.get(i);
if(object instanceof String) {
Class implClass = Class.forName((String)value.get(i));
for (Object array : arrays) {
Array.set(array, i, construct(implClass));
}
} else {
// TODO map of impl class and properties.
/*
Map<String, Map<String, Object>> map = (Map<String, Map<String, Object>>)object;
// construct it using default construct and call all set methods with values defined in the properties
Iterator it = map.entrySet().iterator();
while(it.hasNext()) {
Map.Entry<String, Map<String, Object>> pair = (Map.Entry) it.next();
String key = pair.getKey();
Map<String, Object> properties = pair.getValue();
Class implClass = Class.forName(key);
Object obj = construct(implClass);
Method[] allMethods = implClass.getMethods();
for(Method method : allMethods) {
if(method.getName().startsWith("set")) {
Object [] o = new Object [1];
String propertyName = Introspector.decapitalize(method.getName().substring(3));
Object v = properties.get(propertyName);
if(v == null) {
// it is not primitive type, so find the object in service map.
Class<?>[] pType = method.getParameterTypes();
v = serviceMap.get(pType[0]);
}
o[0] = v;
method.invoke(obj, o);
}
}
for(Class c: interfaceClasses) {
serviceMap.put(c, obj); // all interfaces share the same impl
}
}
*/
}
}
for(int i = 0; i < interfaceClasses.size(); i++) {
serviceMap.put(interfaceClasses.get(i), arrays.get(i));
}
}
/**
* For each singleton definition, create object and push it into the service map
* @param key String interface or multiple interface separated by ","
* @param value List of implementations of interface(s) defined in the key
* @throws Exception exception thrown from the object creation
*/
public static void handleSingleton(String key, List<Object> value) throws Exception {
List<Class> interfaceClasses = new ArrayList();
if(key.contains(",")) {
String[] interfaces = key.split(",");
for (String anInterface : interfaces) {
interfaceClasses.add(Class.forName(anInterface));
}
} else {
interfaceClasses.add(Class.forName(key));
}
// the value can be a list of implementation class names or a map.
if(value != null && value.size() == 1) {
handleSingleImpl(interfaceClasses, value);
} else {
handleMultipleImpl(interfaceClasses, value);
}
}
public static Object construct(Class clazz) throws Exception {
// find out how many constructors this class has.
Object instance = null;
Constructor[] allConstructors = clazz.getDeclaredConstructors();
// iterate all constructors of this class and try each non-default one with parameters from service map
// also flag if there is default constructor without argument.
boolean hasDefaultConstructor = false;
for (Constructor ctor : allConstructors) {
Class<?>[] pType = ctor.getParameterTypes();
if(pType.length > 0) {
boolean beanFound = true;
Object[] params = new Object[pType.length];
for (int j = 0; j < pType.length; j++) {
//System.out.println("pType = " + pType[j]);
Object obj = getBean(pType[j]);
if(obj != null) {
params[j] = obj;
} else {
// cannot find parameter in singleton service map, skip this constructor.
beanFound = false;
break;
}
}
if(!beanFound) {
// could not find all parameters, try another constructor.
continue;
} else {
// this constructor parameters are found.
instance = ctor.newInstance(params);
break;
}
} else {
hasDefaultConstructor = true;
continue;
}
}
if(instance != null) {
return instance;
} else {
if(hasDefaultConstructor) {
return clazz.getConstructor().newInstance();
} else {
// error that no instance can be created.
throw new Exception("No instance can be created for class " + clazz);
}
}
}
public static Object constructWithParameters(Class clazz, List parameters) throws Exception {
// find out how many constructors this class has and match the one with the same sequence of
// parameters.
Object instance = null;
Constructor[] allConstructors = clazz.getDeclaredConstructors();
// iterate all constructors of this class and try each non-default one with parameters
// from parameter list also flag if there is default constructor without argument.
boolean hasDefaultConstructor = false;
for (Constructor ctor : allConstructors) {
Class<?>[] pType = ctor.getParameterTypes();
if(pType.length > 0) {
if(pType.length != parameters.size()) {
continue;
} else {
// number of parameters is matched. Make sure that each parameter type is matched.
boolean matched = true;
Object[] params = new Object[pType.length];
for (int j = 0; j < pType.length; j++) {
//System.out.println("pType = " + pType[j]);
Map<String, Object> parameter = (Map)parameters.get(j);
Iterator it = parameter.entrySet().iterator();
if (it.hasNext()) { // there is only one object in each item.
Map.Entry<String, Object> pair = (Map.Entry) it.next();
String key = pair.getKey();
Object value = pair.getValue();
if(pType[j].getName().equals(key)) {
params[j] = value;
} else {
matched = false;
break;
}
}
}
if(!matched) {
// could not find all parameters, try another constructor.
continue;
} else {
// this constructor parameters are found.
instance = ctor.newInstance(params);
break;
}
}
} else {
hasDefaultConstructor = true;
continue;
}
}
if(instance != null) {
return instance;
} else {
if(hasDefaultConstructor) {
return clazz.getConstructor().newInstance();
} else {
// error that no instance can be created.
throw new Exception("No instance can be created for class " + clazz);
}
}
}
public static Object getBean(Class interfaceClass) {
return serviceMap.get(interfaceClass);
}
}