package com.threealike.life.core;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.threealike.life.thirdparty.org.json.JSONArray;
import com.threealike.life.thirdparty.org.json.JSONObject;
import com.threealike.life.util.Arrays;
import com.threealike.life.util.Strings;
public class Env
{
public static final String MODE;
public static final EnvTypes ENV_TYPE;
public static final String APP_ROOT;
public static final String JAVA_HOME;
public static final String[] APP_LABELS;
public static final String VALID_APP_LABEL_PATTERN = "^[a-zA-Z_0-9\\-$]+$";
public static final String[] SERVICE_LABELS;
/*
* tmp is a temporary JSONObject for purposes of ensuring atomic assignment to CONF
* this is necessary if hot re-deploy is ever implemented
*
* calls to register() must my synchronized on Env.class to ensure atomicity
*/
private static JSONObject tmp;
private static List<String> checked;
private static final String[] STACK = new String[100];
private static final JSONObject CONF;
private static final JSONObject[] APP_CONFS;
private static final Map<String, List<RequestListener>> REQUEST_LISTENERS
= new HashMap<String, List<RequestListener>>();
static final Map<String, NicheService> SERVICES = new HashMap<String, NicheService>();
static final Map<String, List<ServiceStopListener>> SHUT_LISTENERS = new HashMap<String, List<ServiceStopListener>>();
static
{
/*
* get the mode we're in
*/
InputStream is = ClassLoader.getSystemResourceAsStream(".mode");
if(is == null)
throw new RuntimeException("\".mode\" file not found");
byte[] data = null;
try {
data = new byte[is.available()];
is.read(data);
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
MODE = new String(data).trim();
/*
* register main application environment vars
*/
CONF = parseEnv(null);
if(CONF == null) {
throw new RuntimeException("Could not register main Application. Verify \"env\" and \"env.possible\" are on the classpath");
}
ENV_TYPE = EnvTypes.valueOf(CONF.getString("env-type"));
APP_ROOT = CONF.optString("app-root");
JAVA_HOME = CONF.optString("java-home");
/*
* register app info
*/
if(CONF.has("apps")) {
JSONArray apps;
int len;
APP_LABELS = new String[(len=(apps=CONF.getJSONArray("apps")).length())];
System.out.println("(niche) apps="+apps);
APP_CONFS = new JSONObject[len];
for(int i=0; i < apps.length(); i++) {
String appLabel = apps.getJSONObject(i).getString("label");
APP_LABELS[i] = appLabel;
if(!appLabel.matches(VALID_APP_LABEL_PATTERN))
throw new RuntimeException(
"Invalid application label, \""+appLabel+"\". Must match "+VALID_APP_LABEL_PATTERN);
APP_CONFS[i] = parseEnv(appLabel);
}
} else {
APP_LABELS = null;
APP_CONFS = null;
}
if(CONF.has("services")) {
SERVICE_LABELS = new String[CONF.getJSONArray("services").length()];
for(int i=0; i < CONF.getJSONArray("services").length(); i++) {
SERVICE_LABELS[i] = CONF.getJSONArray("services").getJSONObject(i).getString("label");
}
} else {
SERVICE_LABELS = null;
}
//APP_LABELS[APP_LABELS.length-1] = null;
}
public static final void stopService(String label) {
if(!SERVICES.containsKey(label)) return;
if(Env.SHUT_LISTENERS.get(label) != null) {
for(Iterator<ServiceStopListener> it = Env.SHUT_LISTENERS.get(label).iterator(); it.hasNext();)
it.next().handleShutdown();
}
SERVICES.get(label).shutdown();
}
public static final void addServiceStopListener(String label, ServiceStopListener listener) {
if(Env.SHUT_LISTENERS.get(label) == null)
Env.SHUT_LISTENERS.put(label, new ArrayList<ServiceStopListener>());
Env.SHUT_LISTENERS.get(label).add(listener);
}
public static final JSONObject getServiceConfig(String serviceLabel) {
JSONArray services = getConfig().getJSONArray("services");
for(int i=0; i < services.length(); i++) {
JSONObject service = services.getJSONObject(i);
if(service.getString("label").equals(serviceLabel)) {
return service;
}
}
return null;
}
public static final void addRequestListener(String serviceLabel, RequestListener requestListener) {
List<RequestListener> listeners = REQUEST_LISTENERS.get(serviceLabel);
if(listeners == null) listeners = new ArrayList<RequestListener>();
listeners.add(requestListener);
REQUEST_LISTENERS.put(serviceLabel, listeners);
}
public static final JSONObject getConfig() {
return CONF;
}
public static final JSONObject getConfig(String appLabel) {
if(appLabel == null) return getConfig();
int i=0;
for(String s : APP_LABELS) {
if(s == null) continue;
if(s.equals(appLabel)) {
return APP_CONFS[i];
}
i++;
}
return null;
}
static final List<RequestListener> getRequestListeners(String serviceLabel) {
return REQUEST_LISTENERS.get(serviceLabel);
}
@SuppressWarnings("unchecked")
public static final JSONObject parseEnv(String appLabel)
{
String configFileLabel = appLabel == null ? "env" : appLabel+".env";
System.out.print("(niche) loading "+configFileLabel+".possible");
InputStream resource = ClassLoader.getSystemResourceAsStream(configFileLabel+".possible");
URL res = ClassLoader.getSystemResource(configFileLabel+".possible");
System.out.println(" file="+(res == null ? "null" : res.getFile()));
if(resource == null) return null;
JSONObject possible = new JSONObject(resource, true);
if(possible == null && appLabel == null)
throw new RuntimeException("Could not load *.possible file for "+appLabel);
if(possible == null) return null;
System.out.print("(niche) loading "+configFileLabel);
InputStream configResource = ClassLoader.getSystemResourceAsStream(configFileLabel);
res = ClassLoader.getSystemResource(configFileLabel);
System.out.println(" file="+(res == null ? "null" : res.getFile()));
// if(configResource == null) return null;
// JSONArray allActual = new JSONArray(configResource);
// if(allActual == null)
// allActual = new JSONArray();
JSONArray allActual = null;
if(configResource == null)
allActual = new JSONArray("[{'apply':['"+MODE+"']}]");
else
allActual = new JSONArray(configResource, true);
tmp = new JSONObject();
for(int i=0; i < allActual.length(); i++) {
JSONObject next = allActual.getJSONObject(i);
if(!next.has("apply")) {
throw new RuntimeException("required key, \"apply\" not found in config block");
}
JSONArray apply = next.getJSONArray("apply");
inner:
for(int j=0; j < apply.length(); j++) {
if(apply.getString(j).equals(MODE)) {
// if(CONF.get(appLabel) == null)
// CONF.put(appLabel, next);
// else {
for(Iterator it = next.keys(); it.hasNext();) {
String next1 = (String)it.next();
tmp.put(next1, next.get(next1));
}
// }
break inner;
}
}
}
/*
* apply all default config keys (which are global across all modes)
*/
for(Iterator it = possible.keys(); it.hasNext();) {
String next = (String)it.next();
JSONObject desc = possible.getJSONObject(next);
if(desc.has("default")) {
assign(null, possible, 0, explodeKey(next.split("\\.")));
}
}
checked = new ArrayList<String>();
validate(possible, tmp, 0);
for(Iterator it = possible.keys(); it.hasNext();) {
String next = (String)it.next();
String parent = getParent(next);
if(parent != null && !checked.contains(parent)) {
continue;
}
JSONObject desc = possible.getJSONObject(next);
boolean required = true;
if(desc.has("required"))
required = desc.getBoolean("required");
if(required && !checked.contains(next))
throw new RuntimeException("Key, \""+configFileLabel+"."+next+"\", is required");
}
return tmp;
}
private static final String[] explodeKey(String[] parts)
{
for(int i=0; i < parts.length; i++) {
int index;
if((index=parts[i].indexOf("[]")) > 0) {
int brackets = Strings.countMatches(parts[i], "[]");
parts[i] = parts[i].substring(0,index);
String[][] subs = Arrays.split(parts, i);
String[] brkts = new String[brackets];
java.util.Arrays.fill(brkts, "[]");
String[] newa = Arrays.merge(subs[0], brkts);
parts = Arrays.merge(newa, subs[1]);
}
}
return parts;
}
private static final void assign(Object parent, JSONObject possible, int index, String[] key)
{
assign(parent, possible, index, key, -1);
}
private static final void assign(Object parent, JSONObject possible, int index, String[] key, int arrayIndex)
{
String sub = "";
for(int i=0; i <= index; i++) {
sub += key[i];
if(i < index && !key[i+1].equals("[]")) sub += ".";
}
JSONObject desc = possible.getJSONObject(sub);
Types type = Types.String;
if(desc.has("type"))
type = Types.valueOf(desc.getString("type"));
Object apply = null;
if(type == Types.Array) {
if(parent == null) {
if(tmp.has(key[index]))
apply = tmp.getJSONArray(key[index]);
} else if(parent instanceof JSONObject) {
if(((JSONObject )parent).has(key[index])) {
apply = ((JSONObject)parent).getJSONArray(key[index]);
}
} else if(parent instanceof JSONArray) {
for(int i=0; i < ((JSONArray)parent).length(); i++) {
if(((JSONArray)parent).get(i) instanceof JSONArray) {
apply = ((JSONArray)parent).get(i);
break;
}
}
}
if(apply == null)
apply = new JSONArray();
} else if(type == Types.Object) {
if(parent == null) {
if(tmp.has(key[index]))
apply = tmp.getJSONObject(key[index]);
} else if(parent instanceof JSONObject) {
apply = ((JSONObject)parent).opt(key[index]);
} else if(parent instanceof JSONArray) {
for(int i=0; i < ((JSONArray)parent).length(); i++) {
if(((JSONArray)parent).get(i) instanceof JSONObject && i == arrayIndex) {
apply = ((JSONArray)parent).get(i);
break;
}
}
}
if(apply == null)
apply = new JSONObject();
} else if(type == Types.String)
apply = desc.get("default");
else if(type == Types.Int) {
apply = desc.get("default");
} else if(type == Types.Boolean)
apply = desc.get("default");
if(apply == null)
throw new RuntimeException("Could not determine type for key, \""+sub+"\"");
if(parent == null) {
if(!tmp.has(key[index]))
tmp.put(key[index], apply);
} else if(parent instanceof JSONObject) {
if(!((JSONObject)parent).has(key[index]))
((JSONObject)parent).put(key[index], apply);
} else {
boolean there = false;
for(int i=0; i < ((JSONArray)parent).length(); i++) {
if(((JSONArray)parent).get(i).equals(apply) && i == arrayIndex) {
there = true;
break;
}
}
if(!there) {
((JSONArray)parent).put(apply);
}
}
if(index+1 == key.length) return;
if(apply instanceof JSONArray) {
for(int k=0; k < ((JSONArray)apply).length(); k++) {
assign(apply, possible, index+1, key, k);
}
return;
}
assign(apply, possible, index+1, key);
}
private static final String getParent(String key)
{
String out = null;
if(key.length() > 2 && key.substring(key.length()-2).equals("[]"))
return key.substring(0,key.length()-2);
else if(key.indexOf(".") != -1) {
String[] parts = key.split("\\.");
out = "";
for(int i=0; i < parts.length-1; i++) {
out += parts[i];
if(i < parts.length-2)
out += ".";
}
}
return out;
}
@SuppressWarnings("unchecked")
private static final void validate(JSONObject possible, Object node, int level)
{
Types nextNodeType = null;
Object nextNode = null;
if(node instanceof JSONObject) {
JSONObject _node = (JSONObject)node;
for(Iterator it = _node.keys(); it.hasNext();) {
String next = (String)it.next();
if(next.equals("apply") && level == 0)
continue;
if(next.matches(".*[^a-zA-Z_0-9\\-$].*"))
throw new RuntimeException("Bad characters found in key, \""+next+"\"");
STACK[level] = next;
nextNodeType = checkType(possible, (nextNode=_node.get(next)), level);
if(nextNodeType == Types.Object || nextNodeType == Types.Array)
validate(possible, nextNode, level+1);
}
}
if(node instanceof JSONArray) {
STACK[level] = "[]";
JSONArray _node = (JSONArray)node;
for(int i=0; i < _node.length(); i++) {
nextNodeType = checkType(possible, (nextNode=_node.get(i)), level);
if(nextNodeType == Types.Object || nextNodeType == Types.Array)
validate(possible, nextNode, level+1);
}
}
}
private static final Types checkType(JSONObject possible, Object node, int level)
{
String sig = "";
for(int i=0; i < level+1; i++) {
sig += STACK[i];
if(i < level && !STACK[i+1].equals("[]")) {
sig += ".";
}
}
if(!possible.has(sig))
throw new RuntimeException("No config descriptor found for key, \""+sig+"\"");
checked.add(sig);
JSONObject descr = possible.getJSONObject(sig);
Types type = Types.String;
if(descr.has("type"))
type = Types.valueOf(descr.getString("type"));
boolean failed = false;
if(type == Types.String) {
if(!(node instanceof String))
failed = true;
} else if(type == Types.Array) {
if(!(node instanceof JSONArray))
failed = true;
} else if(type == Types.Int) {
if(!(node instanceof Integer))
failed = true;
} else if(type == Types.Object) {
if(!(node instanceof JSONObject))
failed = true;
} else if(type == Types.Boolean) {
if(!(node instanceof Boolean))
failed = true;
} else
failed = true;
if(failed)
throw new RuntimeException("Type validation failed for key, \""+sig+"\"");
return type;
}
private static enum Types {
String, Array, Object, Int, Boolean
}
public static enum EnvTypes {
dev, test, prod
}
}