├── 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 |
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 |
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 |
--------------------------------------------------------------------------------