├── war ├── public │ └── css │ │ └── dummy.css ├── skins │ ├── header.html │ ├── person.html │ ├── index.html │ └── form.html ├── WEB-INF │ ├── lib │ │ └── js.jar │ ├── appengine-web.xml │ ├── modules │ │ ├── memcache.js │ │ ├── apejs.js │ │ ├── googlestore.js │ │ └── select.js │ └── web.xml ├── main.js └── common │ └── mustache.js ├── README.md └── src └── apejs └── ApeServlet.java /war/public/css/dummy.css: -------------------------------------------------------------------------------- 1 | /* dummy */ -------------------------------------------------------------------------------- /war/skins/header.html: -------------------------------------------------------------------------------- 1 | ApeJS test page 2 | -------------------------------------------------------------------------------- /war/WEB-INF/lib/js.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmatteis/apejs/HEAD/war/WEB-INF/lib/js.jar -------------------------------------------------------------------------------- /war/skins/person.html: -------------------------------------------------------------------------------- 1 | {{name}} 2 | ({{age}}) - {{gender}} 3 | (delete - 4 | edit)
5 | -------------------------------------------------------------------------------- /war/skins/index.html: -------------------------------------------------------------------------------- 1 |

Add person

2 |
3 | Name:
4 | Age:
5 | Gender:
6 | 7 |
8 | 9 | See the other test page for filtering and sorting.

10 | -------------------------------------------------------------------------------- /war/WEB-INF/appengine-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | apejs 5 | 1 6 | /public 7 | true 8 | true 9 | 10 | -------------------------------------------------------------------------------- /war/skins/form.html: -------------------------------------------------------------------------------- 1 |

Filtering and sorting

2 |
3 | Filter by: 4 | 5 | 6 |

7 | Sort by: 8 | 9 |

10 | 11 |
12 | -------------------------------------------------------------------------------- /war/WEB-INF/modules/memcache.js: -------------------------------------------------------------------------------- 1 | importPackage(com.google.appengine.api.memcache); 2 | 3 | var memcache = { 4 | memcacheService: MemcacheServiceFactory.getMemcacheService(), 5 | get: function(key) { 6 | return this.memcacheService.get(key); 7 | }, 8 | /** 9 | * key: the string you want to identify this cache item with 10 | * value: the value stored in the cache (can be object or other types) 11 | * seconds: after how many seconds this cache will expire 12 | */ 13 | put: function(key, value, seconds) { 14 | if(seconds) 15 | return this.memcacheService.put(key, value, Expiration.byDeltaSeconds(seconds)); 16 | else 17 | return this.memcacheService.put(key, value); 18 | }, 19 | clearAll: function() { 20 | this.memcacheService.clearAll(); 21 | } 22 | }; 23 | exports = memcache; 24 | -------------------------------------------------------------------------------- /war/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | ApeServlet 9 | 10 | apejs.ApeServlet 11 | 12 | 18 | 19 | 20 | 21 | ApeServlet 22 | /* 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /war/WEB-INF/modules/apejs.js: -------------------------------------------------------------------------------- 1 | importPackage(java.io); 2 | 3 | var apejs = { 4 | urls: {}, 5 | getQueryParameters: function(req) { 6 | var ret = {}; 7 | var parameterNames = req.getParameterNames(); 8 | while (parameterNames.hasMoreElements()) { 9 | var paramName = parameterNames.nextElement(); 10 | var paramValues = req.getParameterValues(paramName); 11 | if(paramValues.length == 1) { 12 | ret[paramName] = ''+paramValues[0]; 13 | } else if(paramValues.length > 1) { 14 | ret[paramName] = []; 15 | for(var i = 0; i< paramValues.length; i++) { 16 | ret[paramName].push(''+paramValues[i]); 17 | } 18 | } 19 | } 20 | return ret; 21 | }, 22 | run: function(request, response) { 23 | var path = request.getPathInfo(); 24 | var httpMethod = request.getMethod().toLowerCase(); 25 | 26 | // before running the http verb method run the before handler 27 | if(this.before) 28 | this.before(request, response); 29 | 30 | 31 | var matchedUrl = false; 32 | for(var i in this.urls) { 33 | var regex = "^"+i+"/?$"; 34 | var matches = path.match(new RegExp(regex)); 35 | if(matches && matches.length) { // matched! 36 | // turn the query into a JS object 37 | var query = apejs.getQueryParameters(request); 38 | this.urls[i][httpMethod](request, response, query, matches); 39 | matchedUrl = true; 40 | break; // we found it, stop searching 41 | } 42 | } 43 | 44 | if(!matchedUrl) 45 | return response.sendError(response.SC_NOT_FOUND); 46 | } 47 | }; 48 | exports = apejs; 49 | -------------------------------------------------------------------------------- /war/main.js: -------------------------------------------------------------------------------- 1 | var apejs = require("apejs.js"); 2 | var select = require('select.js'); 3 | 4 | var mustache = require("./common/mustache.js"); 5 | 6 | apejs.urls = { 7 | "/": { 8 | get: function(request, response, query) { 9 | var html = mustache.to_html(render("skins/index.html")); 10 | print(response).html(html); 11 | 12 | select("person") 13 | .find() 14 | .sort("name", "ASC") 15 | .each(function(id) { 16 | var person = mustache.to_html(render("skins/person.html"), { 17 | id: id, 18 | name: this["name"], 19 | age: this["age"], 20 | gender: this["gender"] 21 | }); 22 | print(response).html(person); 23 | }); 24 | }, 25 | post: function(request, response, query) { 26 | select('person') 27 | .add({ 28 | "name" : query.name, 29 | "gender": query.gender, 30 | "age": parseInt(query.age, 10), 31 | "jobs": ["fanner", "fut", "fab"], 32 | "json": {"foo":"bar"} 33 | }); 34 | response.sendRedirect("/"); 35 | } 36 | }, 37 | "/test" : { 38 | get: function(request, response) { 39 | var form = mustache.to_html(render("skins/form.html")); 40 | print(response).html(form); 41 | 42 | select("person") 43 | .find() 44 | .sort("name", "ASC") 45 | .each(function(id) { 46 | var person = mustache.to_html(render("skins/person.html"), { 47 | id: id, 48 | name: this["name"], 49 | age: this["age"], 50 | gender: this["gender"] 51 | }); 52 | print(response).html(person); 53 | }); 54 | }, 55 | post: function(request, response) { 56 | var par = param(request); 57 | 58 | var form = mustache.to_html(render("skins/form.html")); 59 | print(response).html(form); 60 | 61 | // filter value can be string or number 62 | var filter_val = par("filter_val"); 63 | 64 | // select... XXX defaults always to = for now 65 | var filter = {}; 66 | filter[par("filter_by")] = filter_val; 67 | 68 | 69 | select("person") 70 | .find(filter) 71 | .sort(par("filter_by")) 72 | .sort(par("sort_by"), par("sort_dir")) 73 | .each(function(id) { 74 | var person = mustache.to_html(render("skins/person.html"), { 75 | id: id, 76 | name: this["name"], 77 | age: this["age"], 78 | gender: this["gender"] 79 | }); 80 | print(response).html(person); 81 | }); 82 | } 83 | }, 84 | "/person/([a-zA-Z0-9_]+)" : { 85 | get: function(request, response, q, matches) { 86 | var id = parseInt(matches[1], 10); 87 | select("person") 88 | .find(id) 89 | .each(function(id) { 90 | var person = mustache.to_html(render("skins/person.html"), { 91 | id: id, 92 | name: this["name"], 93 | age: this["age"], 94 | gender: this["gender"] 95 | }); 96 | print(response).html(person); 97 | }); 98 | } 99 | }, 100 | "/edit/([0-9]+)" : { 101 | get: function(request, response, q, matches) { 102 | var id = parseInt(matches[1], 10); 103 | select("person") 104 | .find() 105 | .attr({name: "Fuck"}); 106 | response.sendRedirect("/"); 107 | } 108 | }, 109 | "/delete/([0-9]+)" : { 110 | get: function(request, response, q, matches) { 111 | var id = parseInt(matches[1], 10); 112 | select("person") 113 | .find(id) 114 | .del(); 115 | response.sendRedirect("/"); 116 | } 117 | } 118 | }; 119 | 120 | 121 | // simple syntax sugar 122 | function print(response) { 123 | return { 124 | html: function(str) { 125 | if(str) { 126 | response.setContentType('text/html'); 127 | response.getWriter().println(''+str); 128 | } 129 | }, 130 | json: function(j) { 131 | if(j) { 132 | var jsonString = JSON.stringify(j); 133 | response.setContentType("application/json"); 134 | response.getWriter().println(jsonString); 135 | } 136 | } 137 | }; 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A tiny JavaScript web framework targeted for [Google App Engine](http://code.google.com/appengine) 2 | 3 | Prototyping applications is as easy as writing some JSON: 4 | 5 | var apejs = require("apejs.js"); 6 | apejs.urls = { 7 | "/": { 8 | get: function(request, response) { 9 | response.getWriter().println("Hello World!"); 10 | } 11 | }, 12 | "/foo": { 13 | get: function(request, response) { 14 | request.getSession(true); 15 | } 16 | }, 17 | "/recipes/([a-zA-Z0-9_]+)" : { 18 | get: function(request, response, matches) { 19 | response.getWriter().println(matches[1]); 20 | }, 21 | post: function(request, response) { 22 | // do something during POST request 23 | } 24 | } 25 | } 26 | 27 | Reaching `http://yoursite.com/` will output `Hello World!`, accessing 28 | `http://yoursite.com/foo` will start a new session and going to 29 | `http://yoursite.com/recipes/my-fantastic-recipe` will show you 30 | `my-fantastic-recipe`. 31 | 32 | You can leverage the entire Java API (Rhino) directly through *JavaScript*, avoiding re-compilation times in favor of 33 | raw scripting power! 34 | 35 | ## Idea behind ApeJS 36 | 37 | The idea behind ApeJS is to provide a tiny framework for developing Google App 38 | Engine websites using the JavaScript language. There are other JS 39 | frameworks out there that you could use with App Engine - [RingoJS](http://ringojs.org) and 40 | [AppEngineJS](http://www.appenginejs.org/) are extremely well made - 41 | but I needed something more simple, something that didn't come with tons of 42 | libraries and that could let me prototype applications really quickly (think of 43 | [web.py](http://webpy.org)) and so I created ApeJS. 44 | 45 | ## How to start 46 | 47 | 1. Clone ApeJS by simply typing `git clone git@github.com:lmatteis/apejs.git` in a terminal. 48 | 49 | 2. Download the latest **App Engine Java SDK** from the App Engine website. Unzip, 50 | open the folder and navigate to the `lib/user/` directory where you will find an 51 | `appengine-api-1.0-sdk-x.x.x.jar` file. Copy this *jar* file to the your 52 | `war/WEB-INF/lib` directory from within the ApeJS dir we cloned in the earlier 53 | step. 54 | 55 | 3. You're pretty much set. All you have to do is run 56 | `dev_appserver.sh` (or .cmd, found in the App Engine Java SDK) against the `war` 57 | directory of your ApeJS project. 58 | 59 | 4. The main file you need to worry about is `main.js`. This is where all the 60 | handlers for specific urls are called. The examples already in there should get 61 | you started. 62 | 63 | 5. An important folder you want to keep your eyes on is the `/public/` directory. 64 | All your static content should go in here, *stylesheets*, *images* etc. So when 65 | you access `http://yoursite.com/image.jpg` the server will look inside 66 | `/public/image.jpg` for it. 67 | 68 | ## Importing external JS files 69 | 70 | Importing external JS files is quite easy thanks to `require()`. 71 | 72 | To include ApeJS modules (located under `WEB-INF/modules/`) you can do: 73 | 74 | var googlestore = require("googlestore.js"); 75 | 76 | Otherwise you can include things from within your app directory by simply adding 77 | a `./` in front of the filename: 78 | 79 | var myfile = require("./myfile.js"); 80 | 81 | `require()` will simply evaluate the contents of the JavaScript file and only expose 82 | whatever you assign `exports` with. This is how CommonJS implements so you could simply 83 | require CommonJS modules and they should work. 84 | 85 | ## Some templating 86 | 87 | *will add some examples using mustache.js* 88 | 89 | ## Google Datastore 90 | 91 | I'm trying to implement a really basic abstraction around the low-level Google 92 | datastore API. You can read the code under `WEB-INF/modules/googlestore.js`. 93 | In order to work with the datastore, first you need to include it in your file. 94 | 95 | var googlestore = require("googlestore.js"); 96 | 97 | To create an *entity* and store it in the datastore you do: 98 | 99 | var e = googlestore.entity("person", { 100 | "name": "Luca", 101 | "age": 25, 102 | "gender": "female", 103 | "nationality: "Italian" 104 | }); 105 | 106 | // save the entity to the datastore 107 | var key = googlestore.put(e); 108 | 109 | You get an *entity* from the datastore by using a key: 110 | 111 | // creating a key by ID 112 | var key = googlestore.createKey("person", 15); 113 | 114 | // get the entity from the datastore 115 | var person = googlestore.get(key); 116 | 117 | Listing more *entities* is done by using a query: 118 | 119 | // selecting youngest 5 adult males as an array 120 | var people = googlestore.query("person") 121 | .filter("gender", "=", "male") 122 | .filter("age", ">=", 18) 123 | .sort("age", "ASC") 124 | .fetch(5); 125 | 126 | To get properties of an *entity* use the following method: 127 | 128 | person.getProperty("gender"); 129 | 130 | Finally, there are a couple of **key points** to keep in mind when using the *Datastore API*: 131 | 132 | - Filtering Or Sorting On a Property Requires That the Property Exists 133 | - Inequality Filters Are Allowed on One Property Only 134 | - Properties in Inequality Filters Must Be Sorted before Other Sort Orders 135 | 136 | http://code.google.com/appengine/docs/java/datastore/queries.html 137 | 138 | 139 | *More to come ...* 140 | -------------------------------------------------------------------------------- /src/apejs/ApeServlet.java: -------------------------------------------------------------------------------- 1 | package apejs; 2 | 3 | import java.io.*; 4 | import java.util.logging.Logger; 5 | import java.util.*; 6 | 7 | import javax.script.Invocable; 8 | import javax.script.ScriptEngine; 9 | import javax.script.ScriptEngineManager; 10 | import javax.script.ScriptException; 11 | import javax.servlet.ServletConfig; 12 | import javax.servlet.ServletException; 13 | import javax.servlet.ServletRequest; 14 | import javax.servlet.ServletResponse; 15 | import javax.servlet.http.HttpServlet; 16 | import javax.servlet.http.HttpServletRequest; 17 | import javax.servlet.http.HttpServletResponse; 18 | import org.mozilla.javascript.*; 19 | 20 | public class ApeServlet extends HttpServlet { 21 | public static String APP_PATH; 22 | public static ServletConfig CONFIG; 23 | 24 | private ScriptableObject global; 25 | 26 | public void init(ServletConfig config) throws ServletException { 27 | super.init(config); 28 | APP_PATH = config.getServletContext().getRealPath("."); 29 | CONFIG = config; 30 | 31 | Context context = Context.enter(); 32 | try { 33 | global = initGlobalContext(context); 34 | } catch (IOException e) { 35 | throw new ServletException(e); 36 | } finally { 37 | Context.exit(); 38 | } 39 | } 40 | 41 | public void service(ServletRequest request, ServletResponse response) 42 | throws ServletException, IOException { 43 | Context context = Context.enter(); 44 | try { 45 | HttpServletRequest req = (HttpServletRequest) request; 46 | HttpServletResponse res = (HttpServletResponse) response; 47 | res.setContentType("text/html"); 48 | 49 | ScriptableObject global = this.global; 50 | 51 | // if we're in development mode, recompile the JavaScript everytime 52 | if("true".equals(getServletConfig().getInitParameter("development"))) { 53 | global = initGlobalContext(context); 54 | } 55 | 56 | // get the "run" function from the apejs scope and run it 57 | ScriptableObject apejsScope = (ScriptableObject)global.get("apejs", global); 58 | Function fct = (Function)apejsScope.get("run", apejsScope); 59 | Object result = fct.call(context, global, apejsScope, new Object[] {req, res}); 60 | } finally { 61 | Context.exit(); 62 | } 63 | } 64 | 65 | /** 66 | * Evaluates main.js, starts the global scope and defines somes global JS functions 67 | */ 68 | private ScriptableObject initGlobalContext(Context context) throws IOException, ServletException { 69 | String mainFileName = "main.js"; 70 | File mainFile = new File(APP_PATH + "/" + mainFileName); 71 | 72 | // overwrite the global so each requests knows which context we're in 73 | ScriptableObject global = new ImporterTopLevel(context); 74 | 75 | // add the global "names" like require 76 | String[] names = new String[] { 77 | "require", 78 | "render", 79 | "getServletConfig" 80 | }; 81 | global.defineFunctionProperties(names, ApeServlet.class, ScriptableObject.DONTENUM); 82 | 83 | // compile main.js 84 | Script script = context.compileString(getContents(mainFile), mainFileName, 1, null); 85 | script.exec(context, global); 86 | //context.evaluateReader(global, new InputStreamReader(new FileInputStream(mainFile), "ISO-8859-1"), mainFileName, 1, null); 87 | return global; 88 | } 89 | 90 | public static ServletConfig getServletConfig(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws ServletException { 91 | return ApeServlet.CONFIG; 92 | } 93 | 94 | public static ScriptableObject require(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws ServletException { 95 | Scriptable obj; 96 | try { 97 | // if the args[0] starts with a dot look inside the current APP_PATH 98 | String filename = (String)args[0]; 99 | File f; 100 | if(filename.startsWith("./")) { 101 | filename = filename.replace("./", ""); 102 | f = new File(ApeServlet.APP_PATH + "/" + filename); 103 | } else { 104 | // otherwise just look in modules 105 | f = new File(ApeServlet.APP_PATH + "/WEB-INF/modules/" + filename); 106 | } 107 | // create a new "exports" scope and pass it in 108 | obj = cx.newObject(thisObj); 109 | obj.setParentScope(thisObj); // not sure what this does (i think it's for importPackage to work) 110 | ScriptableObject.putProperty(obj, "exports", cx.newObject(obj)); 111 | 112 | Script script = cx.compileString(getContents(f), filename, 1, null); 113 | script.exec(cx, obj); 114 | //cx.evaluateReader(obj, new InputStreamReader(new FileInputStream(f), "ISO-8859-1"), filename, 1, null); 115 | 116 | } catch (IOException e) { 117 | throw new ServletException(e); 118 | } 119 | // now that we evaluated the file, get the "exports" variable and return it 120 | return (ScriptableObject)ScriptableObject.getProperty(obj, "exports"); 121 | } 122 | 123 | public static String render(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws ServletException { 124 | String fileContents = ""; 125 | try { 126 | String filename = (String)args[0]; 127 | if(filename.startsWith("./")) 128 | filename = filename.replace("./", ""); 129 | 130 | if(filename.startsWith("/")) 131 | filename = filename.replace("/", ""); 132 | 133 | // only look in app folder 134 | File f = new File(ApeServlet.APP_PATH + "/" + filename); 135 | 136 | // convert the file to simple string 137 | fileContents = getContents(f); 138 | } catch (IOException e) { 139 | throw new ServletException(e); 140 | } 141 | 142 | return fileContents; 143 | } 144 | 145 | static public String getContents(File aFile) throws IOException, ServletException { 146 | StringBuilder contents = new StringBuilder(); 147 | try { 148 | BufferedReader input = new BufferedReader( 149 | new InputStreamReader(new FileInputStream(aFile), "UTF8") 150 | ); 151 | try { 152 | String line = null; 153 | while (( line = input.readLine()) != null){ 154 | contents.append(line); 155 | contents.append(System.getProperty("line.separator")); 156 | } 157 | } finally { 158 | input.close(); 159 | } 160 | } catch (IOException e){ 161 | throw new ServletException(e); 162 | } 163 | return contents.toString(); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /war/WEB-INF/modules/googlestore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Just a small wrapper around Google App Engine 3 | * low-level datastore api 4 | */ 5 | importPackage(com.google.appengine.api.datastore); 6 | 7 | var memcache = require("memcache.js"); 8 | 9 | var googlestore = (function(){ 10 | 11 | // syntax sugar 12 | var filterOperators = { 13 | '<' : 'LESS_THAN', 14 | '<=': 'LESS_THAN_OR_EQUAL', 15 | '=' : 'EQUAL', 16 | '>' : 'GREATER_THAN', 17 | '>=': 'GREATER_THAN_OR_EQUAL', 18 | '!=': 'NOT_EQUAL' 19 | }; 20 | 21 | var sortDirections = { 22 | 'ASC' : 'ASCENDING', 23 | 'DESC': 'DESCENDING' 24 | }; 25 | function isObject( obj ) { 26 | return toString.call(obj) === "[object Object]"; 27 | } 28 | 29 | return { 30 | datastore: DatastoreServiceFactory.getDatastoreService(), 31 | 32 | // creates a new entity 33 | entity: function() { 34 | if(arguments.length === 2) { 35 | var kind = arguments[0], 36 | data = arguments[1], 37 | entity = new Entity(kind); 38 | } else { 39 | var kind = arguments[0], 40 | keyName = arguments[1], 41 | data = arguments[2], 42 | entity = new Entity(kind, keyName); 43 | } 44 | 45 | for(var i in data) { 46 | // google's datastore doesn't like native arrays. 47 | // it needs a Collection for properties with 48 | // multiple values 49 | if(data[i] instanceof Array) 50 | data[i] = java.util.Arrays.asList(data[i]); 51 | 52 | if(isObject(data[i])) { 53 | // let's stringify it fuck 54 | data[i] = JSON.stringify(data[i]); 55 | } 56 | entity.setProperty(i, data[i]); 57 | } 58 | return entity; 59 | }, 60 | set: function(entity, data) { 61 | for(var i in data) { 62 | if(data[i] instanceof Array) 63 | data[i] = java.util.Arrays.asList(data[i]); 64 | 65 | if(isObject(data[i])) { 66 | data[i] = JSON.stringify(data[i]); 67 | } 68 | entity.setProperty(i, data[i]); 69 | } 70 | }, 71 | put: function(entity) { 72 | return this.datastore.put(entity); 73 | }, 74 | // mimics JDO functionality 75 | get: function(key) { 76 | if(!key) 77 | return null; 78 | var entity = this.datastore.get(key); 79 | return entity; 80 | }, 81 | del: function(key) { 82 | this.datastore["delete"](key); 83 | }, 84 | query: function(kind) { 85 | var q = new Query(kind); 86 | var options = FetchOptions.Builder.withDefaults(); 87 | var cacheKey = null; 88 | var expireSecs = null; 89 | var self; 90 | function filter(propertyName, operator, value) { 91 | operator = filterOperators[operator] || operator; 92 | q.addFilter(propertyName, Query.FilterOperator[operator], value); 93 | return self; 94 | } 95 | function sort(propertyName, direction) { 96 | direction = sortDirections[direction||"ASC"] || direction; 97 | q.addSort(propertyName, Query.SortDirection[direction]); 98 | return self; 99 | } 100 | function setKeysOnly() { 101 | q.setKeysOnly(); 102 | return self; 103 | } 104 | function limit(limit) { 105 | options = options.limit(limit); 106 | return self; 107 | } 108 | function offset(offset) { 109 | options = options.offset(offset); 110 | return self; 111 | } 112 | function setCacheKey(key, secs) { 113 | cacheKey = key; 114 | if(secs) expireSecs = secs; 115 | return self; 116 | } 117 | function fetch(num) { 118 | if(cacheKey) { 119 | var data = memcache.get(cacheKey); 120 | if(data) { 121 | //log("getting it from cache"); 122 | return data; 123 | } 124 | } 125 | //log("getting it from datastore"); 126 | if (num) limit(num); 127 | var preparedQuery = googlestore.datastore.prepare(q); 128 | var ret = preparedQuery.asList(options).toArray(); 129 | if(cacheKey) { 130 | // expire after expireSecs if it exists 131 | if(expireSecs) 132 | memcache.put(cacheKey, ret, expireSecs); 133 | else 134 | memcache.put(cacheKey, ret); 135 | } 136 | return ret; 137 | } 138 | function fetchAsIterable(num) { 139 | if (num) limit(num); 140 | var preparedQuery = googlestore.datastore.prepare(q); 141 | return preparedQuery.asIterable(options); 142 | } 143 | function count() { 144 | var preparedQuery = googlestore.datastore.prepare(q); 145 | return preparedQuery.countEntities(options); 146 | } 147 | return self = { 148 | filter : filter, 149 | sort : sort, 150 | setKeysOnly: setKeysOnly, 151 | limit : limit, 152 | offset : offset, 153 | setCacheKey: setCacheKey, 154 | fetch : fetch, 155 | fetchAsIterable : fetchAsIterable, 156 | count : count 157 | }; 158 | }, 159 | // abstracting everything as possible 160 | createKey: function(kind, id) { 161 | return KeyFactory.createKey(kind, id); 162 | }, 163 | 164 | /** 165 | * transforms an entity into a nice 166 | * JavaScript object ready to be stringified 167 | * so we don't have to call getProperty() all the time. 168 | * this should be more generic. only supports values 169 | * that are directly convertable into strings 170 | * otherwise JSON won't show them 171 | */ 172 | toJS: function(entity) { 173 | var properties = entity.getProperties(), 174 | entries = properties.entrySet().iterator(); 175 | 176 | var ret = {}; 177 | while(entries.hasNext()) { 178 | var entry = entries.next(), 179 | key = entry.getKey(), 180 | value = entry.getValue(); 181 | 182 | if(value instanceof BlobKey) { 183 | // get metadata 184 | var blobInfo = new BlobInfoFactory().loadBlobInfo(value), 185 | contentType = blobInfo.getContentType(); 186 | // based on the mime type we need to figure out which image to show 187 | if(!contentType.startsWith("image")) { // default to plain text 188 | value = ""+blobInfo.getFilename()+""; 189 | } else { 190 | value = ""; 191 | } 192 | } else if(value instanceof Text) { 193 | value = value.getValue(); 194 | } 195 | 196 | // putting an empty string in front of it 197 | // casts it to a JavaScript string even if it's 198 | // more of a complicated type 199 | ret[key] = ""+value; 200 | 201 | // always try to parse this string to see if it's valid JSON 202 | try { 203 | ret[key] = JSON.parse(value); 204 | } catch(e) { 205 | // not valid JSON - don't do anything 206 | } 207 | 208 | } 209 | 210 | return ret; 211 | } 212 | 213 | }; 214 | })(); 215 | 216 | exports = googlestore; 217 | -------------------------------------------------------------------------------- /war/WEB-INF/modules/select.js: -------------------------------------------------------------------------------- 1 | importPackage(com.google.appengine.api.datastore); 2 | 3 | /** 4 | * The main `select()` function. Use `select("kind")` to start the chain. 5 | * 6 | * @param {String} the string kind of the datastore entity we're interacting with 7 | */ 8 | var select = function(kind) { 9 | return select.fn.init(kind); 10 | }; 11 | select.fn = { 12 | init: function(kind) { 13 | this.datastore = DatastoreServiceFactory.getDatastoreService(); 14 | this.kind = kind; 15 | this.query = false; 16 | this.options = false; 17 | this.fetchOptions = FetchOptions.Builder.withDefaults(); 18 | 19 | this.filterOperators = { 20 | '<' : 'LESS_THAN', 21 | '<=': 'LESS_THAN_OR_EQUAL', 22 | '=' : 'EQUAL', 23 | '>' : 'GREATER_THAN', 24 | '>=': 'GREATER_THAN_OR_EQUAL', 25 | '!=': 'NOT_EQUAL' 26 | }; 27 | 28 | this.sortDirections = { 29 | 'ASC' : 'ASCENDING', 30 | 'DESC': 'DESCENDING' 31 | }; 32 | return this; 33 | }, 34 | /** 35 | * Add a new record to the database. 36 | * 37 | * select('users'). 38 | * add({ name: 'Bill Adama' }); 39 | * 40 | * @param {Object} record An object containing values for a new record in the collection. 41 | */ 42 | add: function(record) { 43 | var entity = new Entity(this.kind); 44 | for(var key in record) { 45 | this.setProperty(entity, key, record[key]); 46 | } 47 | this.datastore.put(entity); 48 | }, 49 | /** 50 | * Modify attributes. 51 | * 52 | * select('users'). 53 | * find(1). 54 | * attr({ name: 'William Adama' }); 55 | * 56 | * @param {Object} record An object containing values for a new record in the collection. 57 | * @returns {Object} The current `select` object 58 | */ 59 | attr: function(record) { 60 | var result = this.getResult(); 61 | for(var i=0; i"+blobInfo.getFilename()+""; 241 | } else { 242 | value = ""; 243 | } 244 | } else if(value instanceof Text) { 245 | value = value.getValue(); 246 | } 247 | 248 | if(value instanceof java.util.List) { 249 | // this is how we convert Java Lists into JavaScript native arrays 250 | ret[key] = org.mozilla.javascript.NativeArray(value.toArray()); 251 | } else { 252 | // putting an empty string in front of it 253 | // casts it to a JavaScript string even if it's 254 | // more of a complicated type 255 | ret[key] = ""+value; 256 | } 257 | 258 | // always try to parse this string to see if it's valid JSON 259 | /* for now we don't want to parse everytime 260 | * parsing should be a decision of the user 261 | try { 262 | if(typeof ret[key] === "string") 263 | ret[key] = JSON.parse(value); 264 | } catch(e) { 265 | // not valid JSON - don't do anything 266 | } 267 | */ 268 | } 269 | 270 | return ret; 271 | }, 272 | setProperty: function(entity, key, value) { 273 | // google's datastore doesn't like native arrays. 274 | // it needs a Collection for properties with 275 | // multiple values 276 | if(this.isArray(value)) { 277 | value = java.util.Arrays.asList(value); 278 | } else if(this.isObject(value)) { 279 | value = JSON.stringify(value); 280 | } 281 | /* 282 | if(value instanceof java.lang.String || typeof value === "string") { 283 | value = new Text(value); 284 | } 285 | */ 286 | entity.setProperty(key, value); 287 | }, 288 | isArray: function( obj ) { 289 | return toString.call(obj) === "[object Array]"; 290 | }, 291 | isObject: function( obj ) { 292 | return toString.call(obj) === "[object Object]"; 293 | } 294 | }; 295 | exports = select; 296 | -------------------------------------------------------------------------------- /war/common/mustache.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mustache.js - Logic-less {{mustache}} templates with JavaScript 3 | * http://github.com/janl/mustache.js 4 | */ 5 | var Mustache = exports; 6 | 7 | (function (exports) { 8 | 9 | exports.name = "mustache.js"; 10 | exports.version = "0.5.1-dev"; 11 | exports.tags = ["{{", "}}"]; 12 | 13 | exports.parse = parse; 14 | exports.clearCache = clearCache; 15 | exports.compile = compile; 16 | exports.compilePartial = compilePartial; 17 | exports.render = render; 18 | 19 | exports.Scanner = Scanner; 20 | exports.Context = Context; 21 | exports.Renderer = Renderer; 22 | 23 | // This is here for backwards compatibility with 0.4.x. 24 | exports.to_html = function (template, view, partials, send) { 25 | var result = render(template, view, partials); 26 | 27 | if (typeof send === "function") { 28 | send(result); 29 | } else { 30 | return result; 31 | } 32 | }; 33 | 34 | var whiteRe = /\s*/; 35 | var spaceRe = /\s+/; 36 | var nonSpaceRe = /\S/; 37 | var eqRe = /\s*=/; 38 | var curlyRe = /\s*\}/; 39 | var tagRe = /#|\^|\/|>|\{|&|=|!/; 40 | 41 | // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 42 | // See https://github.com/janl/mustache.js/issues/189 43 | function testRe(re, string) { 44 | return RegExp.prototype.test.call(re, string); 45 | } 46 | 47 | function isWhitespace(string) { 48 | return !testRe(nonSpaceRe, string); 49 | } 50 | 51 | var isArray = Array.isArray || function (obj) { 52 | return Object.prototype.toString.call(obj) === "[object Array]"; 53 | }; 54 | 55 | // OSWASP Guidlines: escape all non alphanumeric characters in ASCII space. 56 | var jsCharsRe = /[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\xFF\u2028\u2029]/gm; 57 | 58 | function quote(text) { 59 | var escaped = text.replace(jsCharsRe, function (c) { 60 | return "\\u" + ('0000' + c.charCodeAt(0).toString(16)).slice(-4); 61 | }); 62 | 63 | return '"' + escaped + '"'; 64 | } 65 | 66 | function escapeRe(string) { 67 | return string.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 68 | } 69 | 70 | var entityMap = { 71 | "&": "&", 72 | "<": "<", 73 | ">": ">", 74 | '"': '"', 75 | "'": ''', 76 | "/": '/' 77 | }; 78 | 79 | function escapeHtml(string) { 80 | return String(string).replace(/[&<>"'\/]/g, function (s) { 81 | return entityMap[s]; 82 | }); 83 | } 84 | 85 | // Export these utility functions. 86 | exports.isWhitespace = isWhitespace; 87 | exports.isArray = isArray; 88 | exports.quote = quote; 89 | exports.escapeRe = escapeRe; 90 | exports.escapeHtml = escapeHtml; 91 | 92 | function Scanner(string) { 93 | this.string = string; 94 | this.tail = string; 95 | this.pos = 0; 96 | } 97 | 98 | /** 99 | * Returns `true` if the tail is empty (end of string). 100 | */ 101 | Scanner.prototype.eos = function () { 102 | return this.tail === ""; 103 | }; 104 | 105 | /** 106 | * Tries to match the given regular expression at the current position. 107 | * Returns the matched text if it can match, `null` otherwise. 108 | */ 109 | Scanner.prototype.scan = function (re) { 110 | var match = this.tail.match(re); 111 | 112 | if (match && match.index === 0) { 113 | this.tail = this.tail.substring(match[0].length); 114 | this.pos += match[0].length; 115 | return match[0]; 116 | } 117 | 118 | return null; 119 | }; 120 | 121 | /** 122 | * Skips all text until the given regular expression can be matched. Returns 123 | * the skipped string, which is the entire tail of this scanner if no match 124 | * can be made. 125 | */ 126 | Scanner.prototype.scanUntil = function (re) { 127 | var match, pos = this.tail.search(re); 128 | 129 | switch (pos) { 130 | case -1: 131 | match = this.tail; 132 | this.pos += this.tail.length; 133 | this.tail = ""; 134 | break; 135 | case 0: 136 | match = null; 137 | break; 138 | default: 139 | match = this.tail.substring(0, pos); 140 | this.tail = this.tail.substring(pos); 141 | this.pos += pos; 142 | } 143 | 144 | return match; 145 | }; 146 | 147 | function Context(view, parent) { 148 | this.view = view; 149 | this.parent = parent; 150 | this.clearCache(); 151 | } 152 | 153 | Context.make = function (view) { 154 | return (view instanceof Context) ? view : new Context(view); 155 | }; 156 | 157 | Context.prototype.clearCache = function () { 158 | this._cache = {}; 159 | }; 160 | 161 | Context.prototype.push = function (view) { 162 | return new Context(view, this); 163 | }; 164 | 165 | Context.prototype.lookup = function (name) { 166 | var value = this._cache[name]; 167 | 168 | if (!value) { 169 | if (name === ".") { 170 | value = this.view; 171 | } else { 172 | var context = this; 173 | 174 | while (context) { 175 | if (name.indexOf(".") > 0) { 176 | var names = name.split("."), i = 0; 177 | 178 | value = context.view; 179 | 180 | while (value && i < names.length) { 181 | value = value[names[i++]]; 182 | } 183 | } else { 184 | value = context.view[name]; 185 | } 186 | 187 | if (value != null) { 188 | break; 189 | } 190 | 191 | context = context.parent; 192 | } 193 | } 194 | 195 | this._cache[name] = value; 196 | } 197 | 198 | if (typeof value === "function") { 199 | value = value.call(this.view); 200 | } 201 | 202 | return value; 203 | }; 204 | 205 | function Renderer() { 206 | this.clearCache(); 207 | } 208 | 209 | Renderer.prototype.clearCache = function () { 210 | this._cache = {}; 211 | this._partialCache = {}; 212 | }; 213 | 214 | Renderer.prototype.compile = function (tokens, tags) { 215 | var fn = compileTokens(tokens), 216 | self = this; 217 | 218 | return function (view) { 219 | return fn(Context.make(view), self); 220 | }; 221 | }; 222 | 223 | Renderer.prototype.compilePartial = function (name, tokens, tags) { 224 | this._partialCache[name] = this.compile(tokens, tags); 225 | return this._partialCache[name]; 226 | }; 227 | 228 | Renderer.prototype.render = function (template, view) { 229 | var fn = this._cache[template]; 230 | 231 | if (!fn) { 232 | fn = this.compile(template); 233 | this._cache[template] = fn; 234 | } 235 | 236 | return fn(view); 237 | }; 238 | 239 | Renderer.prototype._section = function (name, context, callback) { 240 | var value = context.lookup(name); 241 | 242 | switch (typeof value) { 243 | case "object": 244 | if (isArray(value)) { 245 | var buffer = ""; 246 | for (var i = 0, len = value.length; i < len; ++i) { 247 | buffer += callback(context.push(value[i]), this); 248 | } 249 | return buffer; 250 | } else { 251 | return callback(context.push(value), this); 252 | } 253 | break; 254 | case "function": 255 | var sectionText = callback(context, this), self = this; 256 | var scopedRender = function (template) { 257 | return self.render(template, context); 258 | }; 259 | return value.call(context.view, sectionText, scopedRender) || ""; 260 | break; 261 | default: 262 | if (value) { 263 | return callback(context, this); 264 | } 265 | } 266 | 267 | return ""; 268 | }; 269 | 270 | Renderer.prototype._inverted = function (name, context, callback) { 271 | var value = context.lookup(name); 272 | 273 | // From the spec: inverted sections may render text once based on the 274 | // inverse value of the key. That is, they will be rendered if the key 275 | // doesn't exist, is false, or is an empty list. 276 | if (value == null || value === false || (isArray(value) && value.length === 0)) { 277 | return callback(context, this); 278 | } 279 | 280 | return ""; 281 | }; 282 | 283 | Renderer.prototype._partial = function (name, context) { 284 | var fn = this._partialCache[name]; 285 | 286 | if (fn) { 287 | return fn(context, this); 288 | } 289 | 290 | return ""; 291 | }; 292 | 293 | Renderer.prototype._name = function (name, context, escape) { 294 | var value = context.lookup(name); 295 | 296 | if (typeof value === "function") { 297 | value = value.call(context.view); 298 | } 299 | 300 | var string = (value == null) ? "" : String(value); 301 | 302 | if (escape) { 303 | return escapeHtml(string); 304 | } 305 | 306 | return string; 307 | }; 308 | 309 | /** 310 | * Low-level function that compiles the given `tokens` into a 311 | * function that accepts two arguments: a Context and a 312 | * Renderer. Returns the body of the function as a string if 313 | * `returnBody` is true. 314 | */ 315 | function compileTokens(tokens, returnBody) { 316 | if (typeof tokens === "string") { 317 | tokens = parse(tokens); 318 | } 319 | 320 | var body = ['""']; 321 | var token, method, escape; 322 | 323 | for (var i = 0, len = tokens.length; i < len; ++i) { 324 | token = tokens[i]; 325 | 326 | switch (token.type) { 327 | case "#": 328 | case "^": 329 | method = (token.type === "#") ? "_section" : "_inverted"; 330 | body.push("r." + method + "(" + quote(token.value) + ", c, function (c, r) {\n" + 331 | " " + compileTokens(token.tokens, true) + "\n" + 332 | "})"); 333 | break; 334 | case "{": 335 | case "&": 336 | case "name": 337 | escape = token.type === "name" ? "true" : "false"; 338 | body.push("r._name(" + quote(token.value) + ", c, " + escape + ")"); 339 | break; 340 | case ">": 341 | body.push("r._partial(" + quote(token.value) + ", c)"); 342 | break; 343 | case "text": 344 | body.push(quote(token.value)); 345 | break; 346 | } 347 | } 348 | 349 | // Convert to a string body. 350 | body = "return " + body.join(" + ") + ";"; 351 | 352 | // Good for debugging. 353 | // console.log(body); 354 | 355 | if (returnBody) { 356 | return body; 357 | } 358 | 359 | // For great evil! 360 | return new Function("c, r", body); 361 | } 362 | 363 | function escapeTags(tags) { 364 | if (tags.length === 2) { 365 | return [ 366 | new RegExp(escapeRe(tags[0]) + "\\s*"), 367 | new RegExp("\\s*" + escapeRe(tags[1])) 368 | ]; 369 | } 370 | 371 | throw new Error("Invalid tags: " + tags.join(" ")); 372 | } 373 | 374 | /** 375 | * Forms the given linear array of `tokens` into a nested tree structure 376 | * where tokens that represent a section have a "tokens" array property 377 | * that contains all tokens that are in that section. 378 | */ 379 | function nestTokens(tokens) { 380 | var tree = []; 381 | var collector = tree; 382 | var sections = []; 383 | var token, section; 384 | 385 | for (var i = 0; i < tokens.length; ++i) { 386 | token = tokens[i]; 387 | 388 | switch (token.type) { 389 | case "#": 390 | case "^": 391 | token.tokens = []; 392 | sections.push(token); 393 | collector.push(token); 394 | collector = token.tokens; 395 | break; 396 | case "/": 397 | if (sections.length === 0) { 398 | throw new Error("Unopened section: " + token.value); 399 | } 400 | 401 | section = sections.pop(); 402 | 403 | if (section.value !== token.value) { 404 | throw new Error("Unclosed section: " + section.value); 405 | } 406 | 407 | if (sections.length > 0) { 408 | collector = sections[sections.length - 1].tokens; 409 | } else { 410 | collector = tree; 411 | } 412 | break; 413 | default: 414 | collector.push(token); 415 | } 416 | } 417 | 418 | // Make sure there were no open sections when we're done. 419 | section = sections.pop(); 420 | 421 | if (section) { 422 | throw new Error("Unclosed section: " + section.value); 423 | } 424 | 425 | return tree; 426 | } 427 | 428 | /** 429 | * Combines the values of consecutive text tokens in the given `tokens` array 430 | * to a single token. 431 | */ 432 | function squashTokens(tokens) { 433 | var lastToken; 434 | 435 | for (var i = 0; i < tokens.length; ++i) { 436 | var token = tokens[i]; 437 | 438 | if (lastToken && lastToken.type === "text" && token.type === "text") { 439 | lastToken.value += token.value; 440 | tokens.splice(i--, 1); // Remove this token from the array. 441 | } else { 442 | lastToken = token; 443 | } 444 | } 445 | } 446 | 447 | /** 448 | * Breaks up the given `template` string into a tree of token objects. If 449 | * `tags` is given here it must be an array with two string values: the 450 | * opening and closing tags used in the template (e.g. ["<%", "%>"]). Of 451 | * course, the default is to use mustaches (i.e. Mustache.tags). 452 | */ 453 | function parse(template, tags) { 454 | tags = tags || exports.tags; 455 | var tagRes = escapeTags(tags); 456 | 457 | var scanner = new Scanner(template); 458 | 459 | var tokens = [], // Buffer to hold the tokens 460 | spaces = [], // Indices of whitespace tokens on the current line 461 | hasTag = false, // Is there a {{tag}} on the current line? 462 | nonSpace = false; // Is there a non-space char on the current line? 463 | 464 | // Strips all whitespace tokens array for the current line 465 | // if there was a {{#tag}} on it and otherwise only space. 466 | var stripSpace = function () { 467 | if (hasTag && !nonSpace) { 468 | while (spaces.length) { 469 | tokens.splice(spaces.pop(), 1); 470 | } 471 | } else { 472 | spaces = []; 473 | } 474 | 475 | hasTag = false; 476 | nonSpace = false; 477 | }; 478 | 479 | var type, value, chr; 480 | 481 | while (!scanner.eos()) { 482 | value = scanner.scanUntil(tagRes[0]); 483 | 484 | if (value) { 485 | for (var i = 0, len = value.length; i < len; ++i) { 486 | chr = value[i]; 487 | 488 | if (isWhitespace(chr)) { 489 | spaces.push(tokens.length); 490 | } else { 491 | nonSpace = true; 492 | } 493 | 494 | tokens.push({type: "text", value: chr}); 495 | 496 | if (chr === "\n") { 497 | stripSpace(); // Check for whitespace on the current line. 498 | } 499 | } 500 | } 501 | 502 | // Match the opening tag. 503 | if (!scanner.scan(tagRes[0])) { 504 | break; 505 | } 506 | 507 | hasTag = true; 508 | type = scanner.scan(tagRe) || "name"; 509 | 510 | // Skip any whitespace between tag and value. 511 | scanner.scan(whiteRe); 512 | 513 | // Extract the tag value. 514 | if (type === "=") { 515 | value = scanner.scanUntil(eqRe); 516 | scanner.scan(eqRe); 517 | scanner.scanUntil(tagRes[1]); 518 | } else if (type === "{") { 519 | var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1])); 520 | value = scanner.scanUntil(closeRe); 521 | scanner.scan(curlyRe); 522 | scanner.scanUntil(tagRes[1]); 523 | } else { 524 | value = scanner.scanUntil(tagRes[1]); 525 | } 526 | 527 | // Match the closing tag. 528 | if (!scanner.scan(tagRes[1])) { 529 | throw new Error("Unclosed tag at " + scanner.pos); 530 | } 531 | 532 | tokens.push({type: type, value: value}); 533 | 534 | if (type === "name" || type === "{" || type === "&") { 535 | nonSpace = true; 536 | } 537 | 538 | // Set the tags for the next time around. 539 | if (type === "=") { 540 | tags = value.split(spaceRe); 541 | tagRes = escapeTags(tags); 542 | } 543 | } 544 | 545 | squashTokens(tokens); 546 | 547 | return nestTokens(tokens); 548 | } 549 | 550 | // The high-level clearCache, compile, compilePartial, and render functions 551 | // use this default renderer. 552 | var _renderer = new Renderer; 553 | 554 | /** 555 | * Clears all cached templates and partials. 556 | */ 557 | function clearCache() { 558 | _renderer.clearCache(); 559 | } 560 | 561 | /** 562 | * High-level API for compiling the given `tokens` down to a reusable 563 | * function. If `tokens` is a string it will be parsed using the given `tags` 564 | * before it is compiled. 565 | */ 566 | function compile(tokens, tags) { 567 | return _renderer.compile(tokens, tags); 568 | } 569 | 570 | /** 571 | * High-level API for compiling the `tokens` for the partial with the given 572 | * `name` down to a reusable function. If `tokens` is a string it will be 573 | * parsed using the given `tags` before it is compiled. 574 | */ 575 | function compilePartial(name, tokens, tags) { 576 | return _renderer.compilePartial(name, tokens, tags); 577 | } 578 | 579 | /** 580 | * High-level API for rendering the `template` using the given `view`. The 581 | * optional `partials` object may be given here for convenience, but note that 582 | * it will cause all partials to be re-compiled, thus hurting performance. Of 583 | * course, this only matters if you're going to render the same template more 584 | * than once. If so, it is best to call `compilePartial` before calling this 585 | * function and to leave the `partials` argument blank. 586 | */ 587 | function render(template, view, partials) { 588 | if (partials) { 589 | for (var name in partials) { 590 | compilePartial(name, partials[name]); 591 | } 592 | } 593 | 594 | return _renderer.render(template, view); 595 | } 596 | 597 | })(Mustache); 598 | --------------------------------------------------------------------------------