/**
*
*/
package com.trendrr.strest.doc;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.trendrr.oss.DynMap;
import com.trendrr.oss.DynMapFactory;
import com.trendrr.oss.FileHelper;
import com.trendrr.oss.Reflection;
import com.trendrr.oss.Regex;
import com.trendrr.oss.StringHelper;
import com.trendrr.strest.doc.renderer.FileRenderer;
import com.trendrr.strest.doc.renderer.JSONFileRenderer;
/**
* @author Dustin Norlander
* @created Feb 18, 2011
*
*/
public class StrestDocParser {
protected Log log = LogFactory.getLog(StrestDocParser.class);
protected HashMap<String, AbstractDocTag> tags = new HashMap<String, AbstractDocTag>();
protected Set<TemplateRenderer> renderers = new HashSet<TemplateRenderer>();
protected int abstractLength = 256;
protected Set<String> annotationNames = new HashSet<String>();
public StrestDocParser() {
this.addTags("com.trendrr.strest.doc.tags", true);
this.annotationNames.add("Strest");
this.addTemplateRenderer(new JSONFileRenderer());
}
public static void main(String ...strings) {
StrestDocParser parser = new StrestDocParser();
// List<DynMap> routes = parser.parseDirectory("src");
// System.out.println("*****************************");
// for(DynMap route : routes) {
// System.out.println(route.toJSONString());
//
// }
// System.out.println(parser.createIndex(routes));
//
//
parser.parseAndSave("src", "strestdoc");
}
public void addAnnotationName(String ann) {
this.annotationNames.add(ann);
}
/**
* parses the given src directories and saves the resulting files in the save directory.
* @param srcDirectories
* @param saveDirectory
*/
public void parseAndSave(List<String> srcDirectories, String saveDirectory) {
List<DynMap> routes = new ArrayList<DynMap>();
for (String dir : srcDirectories) {
routes.addAll(this.parseDirectory(dir));
}
List<DynMap> indexes = this.createIndexes(routes);
//save the index.
for (DynMap index : indexes) {
for (TemplateRenderer rend : this.renderers) {
if (rend instanceof FileRenderer) {
((FileRenderer)rend).setSaveDirectory(saveDirectory);
}
rend.renderIndex(index);
}
}
for (DynMap route : routes) {
String r = route.get(String.class, "route");
if (r.isEmpty())
r = "/index";
for (TemplateRenderer rend : this.renderers) {
rend.renderPage(r, route);
}
}
}
/**
* parses the given src directory and saves the resulting files in the save directory.
* @param docDirectory
* @param saveDirectory
*/
public void parseAndSave(String srcDirectory, String saveDirectory) {
List<String> dirs = new ArrayList<String>();
dirs.add(srcDirectory);
this.parseAndSave(dirs, saveDirectory);
}
/**
* creates a indexes
*
* where each entry looks like:
* {
* "name" : "default", //the default index
* "categories" : [
* {
* "category" : "admin", //name of the category
* "routes" : [
* {"route" : "/" , "abstract" : "blah blah"}
* ]
* }
* ]
* }
*
*
*
*
* individual entries are created by the createIndexEntry method.
*
* @param routes
* @return
*/
public List<DynMap> createIndexes(List<DynMap> routes) {
DynMap indexes = new DynMap();
for (DynMap route : routes) {
route.putIfAbsent("index", "default");
for (String indexName : route.getList(String.class, "index", ",")) {
indexes.putIfAbsent(indexName, new DynMap("name", indexName));
DynMap index = indexes.getMap(indexName);
String category = route.getString("category", "default");
DynMap mp = this.createIndexEntry(route);
if (mp == null)
continue;
index.addToListWithDot("categories." + category, mp);
}
}
List<DynMap> inds = new ArrayList<DynMap>();
// now need to sort the categories
for(String ind : indexes.keySet()) {
DynMap cats = indexes.getMap(ind + ".categories");
List<DynMap> catList = new ArrayList<DynMap>();
for (String c : cats.keySet()) {
DynMap ct = this.createIndexCategory(c, cats.getList(DynMap.class, c));
if (ct != null) {
catList.add(ct);
}
}
//sort the categories
Collections.sort(catList, new Comparator<DynMap>() {
@Override
public int compare(DynMap o1, DynMap o2) {
return o1.getString("category").compareTo(o2.getString("category"));
}
});
//add back to the index.
indexes.getMap(ind).put("categories", catList);
inds.add(indexes.getMap(ind));
}
return inds;
}
public DynMap createIndexCategory(String category, List<DynMap> routes) {
if (routes == null || routes.isEmpty()) {
return null;
}
Collections.sort(routes, new Comparator<DynMap>(){
@Override
public int compare(DynMap o1, DynMap o2) {
String r1 = o1.getString("route", "");
String r2 = o2.getString("route", "");
return r1.compareToIgnoreCase(r2);
}
});
DynMap cat = new DynMap();
cat.put("category", category);
cat.put("routes", routes);
return cat;
}
/**
* creates the entry for the index page based on the map from the route.
*
* return null to skip this entry in the index.
* @param route
* @return
*/
public DynMap createIndexEntry(DynMap route) {
DynMap mp = new DynMap();
mp.put("route", route.get("route"));
String abs = route.get(String.class, "abstract");
if (abs == null) {
abs = route.get(String.class, "description", "");
System.out.println(abs);
abs = abs.substring(0, Math.min(this.abstractLength, abs.length()));
}
mp.put("abstract", abs);
if (route.containsKey("method")) {
mp.put("method", route.get("method"));
}
return mp;
}
/**
* recursively parses this directory and all others below it.
*
* Each member of the returned list contains a single route. List is sorted by route.
*
* @param dir
* @throws Exception
*/
public List<DynMap> parseDirectory(String dir) {
try {
List<String> filenames = FileHelper.listDirectory(dir, true);
List<DynMap> routes = new ArrayList<DynMap>();
for (String filename : filenames) {
if (!filename.endsWith(".java")) {
continue;
}
String java = FileHelper.loadString(filename);
routes.addAll(this.parse(java));
}
Collections.sort(routes, new Comparator<DynMap>() {
@Override
public int compare(DynMap o1, DynMap o2) {
String r1 = o1.get(String.class, "route") + "_" + o1.get(String.class, "method", "");
String r2 = o1.get(String.class, "route") + "_" + o2.get(String.class, "method", "");
return r1.compareTo(r2);
}
});
return routes;
} catch (Exception x) {
log.error("Caught", x);
}
return null;
}
/**
* adds a new template renderer
* @param renderer
*/
public void addTemplateRenderer(TemplateRenderer renderer) {
this.renderers.add(renderer);
}
/**
* sets the list of template renderers
* @param renderers
*/
public void setTemplateRenderers(Collection<TemplateRenderer> renderers) {
this.renderers.clear();
this.renderers.addAll(renderers);
}
public void addTag(AbstractDocTag tag) {
tags.put(tag.tagName(), tag);
}
public void addTags(String packageName, boolean recure) {
try {
List<AbstractDocTag> tags = Reflection.defaultInstances(AbstractDocTag.class, packageName, recure);
for (AbstractDocTag t : tags) {
this.addTag(t);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* returns a list of maps,
*
* Each map represents a single route.
*
* returns empty list if the java contains no useful documentation
* @param java
* @return
*/
public List<DynMap> parse(String java) {
DynMap mp = this.parseAnnotation(java);
List<DynMap> docs = this.getStrestDoc(java);
if (mp.isEmpty() && docs.isEmpty()) {
return docs;
}
if (docs.isEmpty()) {
docs.add(mp);
}
List<DynMap> vals = new ArrayList<DynMap>();
for(DynMap v : docs) {
DynMap m = new DynMap().extend(mp,v);
List<String> routes = m.getList(String.class, "route");
if (routes != null) {
for(String route : routes) {
DynMap v1 = new DynMap();
v1.putAll(m);
v1.put("route", route);
v1 = this.cleanUpRoute(v1);
vals.add(v1);
}
}
}
return vals;
}
/**
* does final processing on a route map (the data associated with a single route)
*
*
*
* @param route
* @return
*/
protected DynMap cleanUpRoute(DynMap route) {
List<DynMap> params = route.getList(DynMap.class, "param");
if (params == null)
return route;
//Make unify the requiredParams annotation with the markup.
List<String> requiredParams = route.getList(String.class, "requiredParams");
if (requiredParams != null) {
HashMap<String, DynMap> pmsMap = new HashMap<String,DynMap>();
for (DynMap p : params) {
pmsMap.put(p.getString("param"), p);
}
for (String rp : requiredParams) {
if (pmsMap.containsKey(rp)) {
pmsMap.get(rp).put("required", true);
} else {
DynMap p = new DynMap();
p.put("param", rp);
p.put("required", true);
p.put("description", "");
params.add(p);
}
}
}
Collections.sort(params, new Comparator<DynMap>(){
@Override
public int compare(DynMap o1, DynMap o2) {
if (o1.getBoolean("required", false) != o2.getBoolean("required", false)) {
if (o1.getBoolean("required", false)) {
return -1;
} else {
return 1;
}
}
return o1.getString("param").compareTo(o1.getString("param"));
}
});
System.out.println(params);
route.put("params", params);
route.removeAll("requiredParams", "param");
return route;
}
/**
* parses the Strest annotation into a map.
* @param java
* @return
*/
public DynMap parseAnnotation(String java) {
/*
* This is all ugly as hell, and pretty slow, but it works :)
*/
DynMap mp = new DynMap();
for (String annotationName : this.annotationNames) {
String ann = Regex.matchFirst(java, "\\@" + annotationName + "\\s*\\([^\\)]+\\)", false);
if (ann == null) {
continue;
}
ann = ann.replaceFirst("\\@" + annotationName + "\\s*\\(", "");
ann = ann.replaceAll("\\)$", "");
String[] tokens = ann.split("\\s*\\=\\s*");
String key = null;
String value = null;
for (String t : tokens) {
if (key == null) {
key = t;
continue;
}
//else parse the value.
String nextKey = Regex.matchFirst(t, "[\\n\\r]+\\s*[^\\s]+\\s*$", false);
value = t.replaceFirst("[\\n\\r]+\\s*[^\\s]+\\s*$", "").trim();
boolean isList = value.startsWith("{");
value = StringHelper.trim(value, ",");
value = StringHelper.trim(value, "{");
value = StringHelper.trim(value, "}");
value = StringHelper.trim(value.trim(), "\"");
if (isList) {
List<String> values = new ArrayList<String>();
for (String v : value.split(",")) {
values.add(StringHelper.trim(v.trim(), "\""));
}
mp.put(key.trim(), values);
} else {
mp.put(key.trim(), value);
}
if (nextKey != null) {
key = nextKey;
}
}
}
System.out.println(mp.toJSONString());
return mp;
}
public List<DynMap> getStrestDoc(String java) {
// String doc = Regex.matchFirst(java, "\\/\\*\\/\\/(?s).*?\\*\\/", false);
List<DynMap> results = new ArrayList<DynMap>();
for (String doc : Regex.matchAll(java, "\\/\\*\\/\\/(?s).*?\\*\\/", false)) {
//now clean it up.
doc = doc.replaceFirst("^\\/\\*\\/\\/", "");
doc = doc.replaceAll("(?m)^\\s*\\*", "");
doc = doc.replaceAll("\\/$", "");
doc = doc.trim();
DynMap mp = new DynMap();
//now split into key values
for (String tmp : doc.split("[\\r\\n]\\s*\\@")) {
tmp = StringHelper.trim(tmp, "@");
int ind = tmp.indexOf(' ');
if (ind < 0)
continue;
String key = tmp.substring(0, ind).trim();
String value = tmp.substring(ind).trim();
if (value.isEmpty()) {
log.warn("TAG: " + key + " IS EMPTY!");
continue;
}
Object v = this.processTag(key, value);
if (v == null) {
log.warn("TAG: " + key + " IS EMPTY!");
continue;
}
if (mp.containsKey(key)) {
List values = mp.get(List.class, key);
values.add(v);
mp.put(key, values);
} else {
mp.put(key, v);
}
}
if (mp.get("method") == null) {
mp.put("method", this.parseMethod(java));
} else {
mp.put("method", mp.getList(String.class, "method", ","));
}
results.add(mp);
}
return results;
}
/**
* attempts to figure out if the route is GET, POST, DELETE, ect.
* @param java
* @return
*/
protected List<String> parseMethod(String java) {
List<String> methods = new ArrayList<String>();
if (java.contains("handleGET")) {
methods.add("GET");
}
if (java.contains("handlePOST")) {
methods.add("POST");
}
if (java.contains("handleDELETE")) {
methods.add("DELETE");
}
if (java.contains("handlePUT")) {
methods.add("PUT");
}
return methods;
}
/**
* processes the text associated with a tag.
*
* by default just returns the value.
*
* @param tag
* @param value
* @return
*/
public Object processTag(String tag, String value) {
AbstractDocTag t = this.tags.get(tag);
if (t == null)
return value;
return t.process(this, value);
}
}