JsonUtil.java

/***************************************************************************
   Copyright 2013 Emily Estes

   Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0

   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 net.metanotion.web.concrete;


import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.inject.Named;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.metanotion.functor.Block;

import net.metanotion.io.ClassLoaderFileSystem;
import net.metanotion.io.File;
import net.metanotion.io.FileSystem;

import net.metanotion.json.JsonArray;
import net.metanotion.json.JsonObject;

import net.metanotion.util.Json;
import net.metanotion.util.OutputWrapperFilter;
import net.metanotion.util.Pair;
import net.metanotion.util.Wrap;
import net.metanotion.util.types.JsonP;
import net.metanotion.util.types.ToJsonMagicP;
import net.metanotion.util.types.Parser;

import net.metanotion.web.HttpAny;
import net.metanotion.web.HttpDelete;
import net.metanotion.web.HttpGet;
import net.metanotion.web.HttpHead;
import net.metanotion.web.HttpNone;
import net.metanotion.web.HttpPost;
import net.metanotion.web.HttpPut;
import net.metanotion.web.HttpStatus;
import net.metanotion.web.HttpValues;
import net.metanotion.web.Param;
import net.metanotion.web.Response;
import net.metanotion.web.Request;

public final class JsonUtil {
	private static final Logger logger = LoggerFactory.getLogger(JsonUtil.class);

	private static void makeAPIMethod(JsonObject api, String name, String prefix, AnnotatedElement e) {
		final JsonObject m = new JsonObject();
		final Annotation[] ann = e.getAnnotations();
		m.put("uri", prefix + name);
		if(e.isAnnotationPresent(HttpAny.class)) {
			return; //m.put("method", "POST");
		} else {
			for(final Annotation a: ann) {
				if(a instanceof HttpDelete) { m.put("method", "DELETE"); }
				if(a instanceof HttpGet) { m.put("method", "GET"); }
				if(a instanceof HttpHead) { m.put("method", "HEAD"); }
				if(a instanceof HttpPost) { m.put("method", "POST"); }
				if(a instanceof HttpPut) { m.put("method", "PUT"); }
			}
		}
		// do result mime type
		for(final Annotation a: ann) {
			if(a instanceof Json) {
				m.put("result", "json");
			} else if(a instanceof Response) {
				final Param[] ps = ((Response) a).value();
				for(Param p: ps) {
					if("mime".equals(p.name())) {
						m.put("result", p.value());
					}
				}
			}
			if(a instanceof Request) {
				final Param[] ps = ((Request) a).value();
				for(Param p: ps) {
					if("mime".equals(p.name())) {
						m.put("mime", p.value());
					}
				}
			}
		}
		api.put(name, m);
	}

	public static JsonObject makeWebApi(final Class klass, final String prefix) {
		return makeWebApi(klass, prefix, new HashMap<String,Object>());
	}

	public static JsonObject makeWebApi(final Class klass, final String prefix, final Map<String,Object> extraFields) {
		final JsonObject api = new JsonObject();
		for(final Field f: klass.getFields()) {
			if(f.isAnnotationPresent(HttpNone.class)) { continue; }
			final Named n = f.getAnnotation(Named.class);
			makeAPIMethod(api, n==null ? f.getName() : n.value(), prefix, f);
		}
		// method, uri = prefix
		for(final Method m: klass.getMethods()) {
			if(m.isAnnotationPresent(HttpNone.class)) { continue; }
			final Named n = m.getAnnotation(Named.class);
			makeAPIMethod(api, n==null ? m.getName() : n.value(), prefix, m);
		}
		for(final Map.Entry<String,Object> element: extraFields.entrySet()) {
			api.put(element.getKey(), element.getValue());
		}
		return api;
	}

	public static FileSystem<File> getJavaScript() {
		return new ClassLoaderFileSystem(JsonUtil.class.getClassLoader(), "js");
	}

	private static final class JsonTransformer implements Block<Object,Object> {
		private final Parser p;
		public JsonTransformer(final Parser p) { this.p = p; }
		@Override public Object eval(final Object o) throws Exception {
			logger.debug("Transform: {}", o);
			if(o == null) { return null; }
			if((String.class.equals(o.getClass())) && ("".equals((String) o))) { return ""; }
			try {
				return p.parse(o);
			} catch (final Exception e) {
				logger.debug("Error!", e);
				return null;
			}
		}
	}

	/** An empty list of headers. */
	private static final List<Map.Entry<String,Object>> EMPTY_LIST =
		Collections.unmodifiableList(new LinkedList<Map.Entry<String,Object>>());
	/** JSON responses need the Content-Type set to JSON. */
	private static final List<Map.Entry<String,Object>> JSON_HEADER_LIST =
		Collections.unmodifiableList(Arrays.asList((Map.Entry<String,Object>)
			new Pair<String,Object>("Content-Type", "application/json; charset=utf-8")));

	private static final class CheckWrap implements Block<Object, Object> {
		private final Block wrapped;
		private final Block unwrapped;
		public CheckWrap(Block wrapped, Block unwrapped) {
			this.wrapped = wrapped;
			this.unwrapped = unwrapped;
		}

		@Override public Object eval(final Object o) throws Exception {
			logger.debug("CheckWrap: {}", o);
			final Object result = (o instanceof Wrap) ? wrapped.eval(o) : unwrapped.eval(o);
			if(result instanceof HttpValues) {
				logger.debug("wrapped {}", ((HttpValues) result).unwrap());
				return new DelegatingHttpValues(JsonUtil.JSON_HEADER_LIST, JsonUtil.EMPTY_LIST, (HttpValues) result);
			} else {
				logger.debug("unwrapped {}", result);
				return new SimpleHttpValues(JsonUtil.JSON_HEADER_LIST, JsonUtil.EMPTY_LIST, result, HttpStatus.OK.codeNumber());
			}
		}
	}

	private static final class ToJson implements Parser<JsonObject> {
		private final Map<Class, Parser<JsonObject>> map = new HashMap<>();
		@Override public JsonObject parse(final Object o) throws Exception {
			final Class c = o.getClass();
			logger.debug("Parsing {} - {}", c, o);
			Parser<JsonObject> m = map.get(c);
			if(m == null) {
				logger.debug("no parser");
				if(JsonP.parseable(c)) {
					logger.debug("Parseable Json object");
					m = JsonP.INSTANCE;
				} else {
					logger.debug("Reflectively generated Json object");
					m = new ToJsonMagicP(c);
				}
				map.put(c,m);
			}
			logger.debug("parsing object now");
			return m.parse(o);
		}

		@Override public Class<JsonObject> outputInterface() { return JsonObject.class; }
	}

	private static Block getResultXfmr(final Class retType) {
		logger.debug("Result type is {}", retType);
		if(!JsonObject.class.isAssignableFrom(retType)) {
			Parser p = null;
			if(JsonP.parseable(retType)) {
				logger.debug("parseable as a JSON Object");
				p = JsonP.INSTANCE;
			} else {
				logger.debug("ToJson");
				p = new ToJson();
			}
			return new JsonTransformer(p);
		}
		return null;
	}

	public static Map<String,Block> generateJsonResultTransformers(final Class klazz) {
		final Map<String, Block> map = new HashMap<>();

		for(final Field f: klazz.getFields()) {
			if(f.getAnnotation(Json.class) != null) {
				final Block tx = getResultXfmr(f.getType());
				if(tx != null) {
					map.put(f.getName(),new CheckWrap(new OutputWrapperFilter(tx), tx));
				}
			}
		}

		for(final Method m: klazz.getMethods()) {
			if(m.getAnnotation(Json.class) != null) {
				final Block tx = getResultXfmr(m.getReturnType());
				if(tx != null) {
					map.put(m.getName(),new CheckWrap(new OutputWrapperFilter(tx), tx));
				}
			}
		}
		return map;
	}
}