Giter Club home page Giter Club logo

appengine-magic's Introduction

The appengine-magic library attempts to abstract away the infrastructural nuts and bolts of writing a Clojure application for the Google App Engine platform.

The development environment of Google App Engine for Java expects pre-compiled classes, and generally does not fit well with Clojure's interactive development model. appengine-magic attempts to make REPL-based development of App Engine applications as natural as any other Clojure program.

  1. Programs using appengine-magic just need to include appengine-magic as a Leiningen dev-dependency.
  2. appengine-magic takes a Ring handler and makes it available as a servlet for App Engine deployment.
  3. appengine-magic is also a Leiningen plugin, and adds several tasks which simplify preparing for App Engine deployment.

Using appengine-magic still requires familiarity with Google App Engine. This README file tries to describe everything you need to know to use App Engine with Clojure, but does not explain the details of App Engine semantics. Please refer to Google's official documentation for details.

Please read the project's HISTORY file to learn what changed in recent releases.

Current Status

The code on this branch adds experimental support for App Engine SDK 1.7.4 and Leiningen 2.0. A stable older version is available at the v0.5.0 tag.

TODO for a stable 0.5.1 release

  • fix the strange startup error related to the blob store
  • fix the _ah/admin console — some links do not work
  • retest all supported App Engine services — server push (at least) is broken

Dependencies

  • Clojure 1.4.0
  • Leiningen 2.0
  • Google App Engine SDK 1.7.4

Overview

To use appengine-magic effectively, you need the following:

  1. The appengine-magic jar available on the project classpath.
  2. A Ring handler for your main application. You may use any Ring-compatible framework to make it. If your application does not yet have a core.clj file, then the lein appengine-new task creates one for you with a simple "hello world" Ring handler.
  3. A var defined by passing the Ring handler to the appengine-magic.core/def-appengine-app macro. This makes the application available both to interactive REPL development, and to App Engine itself.
  4. An entry point servlet. REPL development does not use it, but the standard App Engine SDK dev_appserver.sh mode and production deployment both do. This servlet must be AOT-compiled into a class file. This servlet defaults to the name app_servlet.clj, and the lein appengine-new task creates one for your project. The servlet must refer to the var defined by def-appengine-app.
  5. Web application resources. This primarily includes web application descriptors. lein appengine-new generates those and places them in the war/WEB-INF/ directory. You should also place all static files that your application uses in war/.

Here is a sample core.clj, using Compojure (other Ring-compatible frameworks, such as Moustache, also work):

(ns simple-example.core
  (:use compojure.core)
  (:require [appengine-magic.core :as ae]))

(defroutes simple-example-app-handler
  (GET "/" req
       {:status 200
        :headers {"Content-Type" "text/plain"}
        :body "Hello, world!"})
  (GET "/hello/:name" [name]
       {:status 200
        :headers {"Content-Type" "text/plain"}
        :body (format "Hello, %s!" name)})
  (ANY "*" _
       {:status 200
        :headers {"Content-Type" "text/plain"}
        :body "not found"}))

(ae/def-appengine-app simple-example-app #'simple-example-app-handler)

If you wish to emit HTML or XML from your application, you should use a specialized Clojure server-side templating library, e.g., Enlive or Hiccup. None of the appengine-magic examples rely on these libraries.

Getting Started

Project setup

You need a copy of the Google App Engine SDK installed somewhere. appengine-magic cannot replace its dev_appserver.sh and appcfg.sh functionality.

  1. lein new
  2. Optional: rm src/<project-name>/core.clj to clean out the default core.clj file created by Leiningen. You need to do this so that appengine-magic can create a default file which correctly invokes the def-appengine-app macro.
  3. Edit project.clj: add [appengine-magic "0.5.1-SNAPSHOT"] to both your :dependencies and :plugins.
  4. lein deps. This fetches appengine-magic, and makes its Leiningen plugin tasks available. If you already have the App Engine SDK installed locally, and do not wish to wait for Maven to download it again as a dependency, you may optionally run the provided install-artifacts.sh script first.
  5. lein appengine-new. This sets up four files for your project: core.clj (which has a sample Ring handler and uses the def-appengine-app macro), app_servlet.clj (the entry point for the application), war/WEB-INF/web.xml (a servlet descriptor), and war/WEB-INF/appengine-web.xml (an App Engine application descriptor). These files should contain reasonable starting defaults for your application.

With regard to AOT-compilation, if your project needs it, then you must include <project>.app_servlet in Leiningen's :aot directive. Otherwise, omit the :aot directive altogether. The lein appengine-prepare task will take care of AOT-compiling the entry point servlet and cleaning up afterwards.

The default .gitignore file produced by Leiningen works well with the resulting project, but do take a careful look at it. In particular, you should avoid checking in war/WEB-INF/lib/ or war/WEB-INF/classes/: let Leiningen take care of managing those directories.

Development process

Launch lein swank or lein repl, whichever you normally use. Once you have a working REPL, compile your application's core.clj (or whatever other entry point file you use).

The key construct provided by appengine-magic is the appengine-magic.core/def-appengine-app macro. It takes a Ring handler and defines a new <project-name>-app var. If you want to rename this var, remember to update app_servlet.clj. That's it: you may now write your application using any framework which produces a Ring-compatible handler. Then, just pass the resulting Ring handler to def-appengine-app.

To test your work interactively, you can control a Jetty instance from the REPL using appengine-magic.core/start and appengine-magic.core/stop. In addition, a convenience function, appengine-magic.core/serve, will either start or restart a running instance. Examples (assuming you are in your application's core namespace and your application is named foo):

(require '[appengine-magic.core :as ae])

;; recommended: use this to start or restart an app
(ae/serve foo-app)

;; or use these lower-level functions
(ae/start foo-app)
(ae/stop)
(ae/start foo-app :port 8095)
(ae/stop)

Recompiling the functions which make up your Ring handler should produce instantaneous results.

If you use SLIME, then the swank.core/break function works even inside a Ring handler.

Testing with dev_appserver.sh

  1. lein appengine-prepare. This AOT-compiles the entry point servlet, makes a jar of your application, and copies it, along with all your library dependencies, to your application's war/WEB-INF/lib/ directories.
  2. Run dev_appserver.sh with a path to your application's war/ directory.

Static files

Just put all static files into your application's war/ directory. If you put a file called index.html there, it will become a default welcome file.

Classpath resources

Put all classpath resources you expect to need at runtime in resources/. You can then access them using the appengine-magic.core/open-resource-stream, which returns a java.io.BufferedInputStream instance.

You may also use appengine-magic.core/resource-url to find a classpath resource's internal URL. This URL will not be externally visible (it will not be an HTTP URL), but you may use it to refer to classpath resources from within the application's code.

Do not use direct methods like java.io.File or ClassLoader/getSystemClassLoader to access classpath resources; they do not work consistently across all App Engine environments.

Deployment to App Engine

  1. First of all, be careful. You must manually maintain the version field in appengine-web.xml and you should understand its implications. Refer to Google App Engine documentation for more information.
  2. lein appengine-prepare prepares the war/ directory with the latest classes and libraries for deployment.
  3. When you are ready to deploy, just run appcfg.sh update with a path to your application's war/ directory.

Checking the runtime environment

  • appengine-magic.core/appengine-environment-type: returns a keyword corresponding to the current environment: :production, :dev-appserver, and :interactive. Useful if you want to, e.g., return more detailed error messages and stack traces in non-production mode.
  • appengine-magic.core/appengine-app-id: returns the ID of the running application.
  • appengine-magic.core/appengine-app-version: returns the current deployed version string.
  • appengine-magic.core/appengine-base-url: returns a string with the base hostname of the current application, e.g., http://my-app.appspot.com. In production, this always points to the appspot.com domain. In interactive mode, this always points to localhost, but also includes the correct port. The :https? keyword determines if the schema in the URL should be https://, but is ignored in interactive mode. This function does not work in dev_appserver.sh at all (it is difficult from within the application environment to determine the correct port).

Automatic testing code

The clojure.test system works well for testing appengine-magic applications, but all tests must bootstrap App Engine services in order to run. The appengine-magic.testing namespace provides several functions usable as clojure.test fixtures to help you do so. The easiest way to get started is:

(use 'clojure.test)
(require '[appengine-magic.testing :as ae-testing])

(use-fixtures :each (ae-testing/local-services :all))

Then, write deftest forms normally; you can use App Engine services just as you would in application code.

File uploads and multipart forms

A Ring application requires the use of middleware to convert the request body into something useful in the request map. Ring comes with ring.middleware.multipart-params/wrap-multipart-params which does this; unfortunately, this middleware uses classes restricted in App Engine. To deal with this, appengine-magic has its own middleware.

appengine-magic.multipart-params/wrap-multipart-params works just like the Ring equivalent, except file upload parameters become maps with a :bytes key (instead of :tempfile). This key contains a byte array with the upload data.

A full Compojure example (includes features from the Datastore service):

(use 'compojure.core
     '[appengine-magic.multipart-params :only [wrap-multipart-params]])

(require '[appengine-magic.core :as ae]
         '[appengine-magic.services.datastore :as ds])

(ds/defentity Image [^{:tag :key} name, content-type, data])

(defroutes upload-images-demo-app-handler
  ;; HTML upload form
  (GET "/upload" _
       {:status 200
        :headers {"Content-Type" "text/html"}
        :body (str "<html><body>"
                   "<form action=\"/done\" "
                   "method=\"post\" enctype=\"multipart/form-data\">"
                   "<input type=\"file\" name=\"file-upload\">"
                   "<input type=\"submit\" value=\"Submit\">"
                   "</form>"
                   "</body></html>")})
  ;; handles the uploaded data
  (POST "/done" _
        (wrap-multipart-params
         (fn [req]
           (let [img (get (:params req) "file-upload")
                 img-entity (Image. (:filename img)
                                    (:content-type img)
                                    (ds/as-blob (:bytes img)))]
             (ds/save! img-entity)
             {:status 200
              :headers {"Content-Type" "text/plain"}
              :body (with-out-str
                      (println (:params req)))}))))
  ;; hit this route to retrieve an uploaded file
  (GET ["/img/:name", :name #".*"] [name]
       (let [img (ds/retrieve Image name)]
         (if (nil? img)
             {:status 404}
             {:status 200
              :headers {"Content-Type" (:content-type img)}
              :body (.getBytes (:data img))}))))

(ae/def-appengine-app upload-images-demo-app #'upload-images-demo-app-handler)

Please note that you do not need to use this middleware with the Blobstore service. App Engine takes care decoding the upload in its internal handlers, and the upload callbacks do not contain multipart data.

Managing multiple environments

Most web applications use several environments internally: production, plus various staging and development installations. App Engine supports multiple versions in its appengine-web.xml file, but does nothing to help deal with installing to different full environments. Since different versions of App Engine applications share the same blobstore and datastore, distinguishing between production and staging using only versions is dangerous.

appengine-magic has a mechanism to help deal with multiple environments. The Leiningen appengine-update task replaces the use of appcfg.sh update, and a new entry in project.clj manages applications and versions.

  1. Rename your WEB-INF/application-web.xml file to WEB-INF/application-web.xml.tmpl. For safety reasons, appengine-update will not run if a normal application-web.xml exists. For clarity, you should blank out the contents of the <application> and <version> tags of the template file (but leave the tags in place).

  2. Add a new entry to project.clj: :appengine-app-versions. This entry is a map from application name to application version. Example:

     :appengine-app-versions {"myapp-production" "2010-11-25 11:15"
                              "myapp-staging"    "2010-11-27 22:05"
                              "myapp-dev1"       "2830"
                              "myapp-dev2"       "2893"}

    The myapp- key strings correspond to App Engine applications, registered and managed through the App Engine console. The value strings are the versions appengine-update will install if invoked on that application.

  3. Add a new entry to project.clj: :appengine-sdk. The App Engine SDK location is necessary to execute the actual production deployment. This value can be just a string, representing a path. Alternatively, for teams whose members keep the App Engine SDK in different locations, this value can be a map from username to path string. Examples:

     :appengine-sdk "/opt/appengine-java-sdk"
     :appengine-sdk {"alice"   "/opt/appengine-java-sdk"
                     "bob"     "/Users/bob/lib/appengine-java-sdk"
                     "charlie" "/home/charlie/appengine/sdk/current"}

    If the APPENGINE_HOME environment variable is set, its value will be used if no :appengine-sdk entry is found in the project.clj file.

  4. Run lein appengine-update <application>, where the argument is an application name from the :appengine-app-versions map.

If you use this mechanism, be aware that dev_appserver.sh will no longer work (since your project no longer defines a simple appengine-web.xml file). To run that process, use lein appengine-dev-appserver <application>.

You may also force a specific version string if you pass it as an optional argument to appengine-update: lein appengine-update <application> <version>.

App Engine Services

appengine-magic provides convenience wrappers for using App Engine services from Clojure. Most of these API calls will work when invoked from the REPL, but only if an application is running — that is, it was launched using appengine-magic.core/start.

User service

The appengine-magic.services.user namespace provides the following functions for handling users.

  • current-user: returns the com.google.appengine.api.users.User for the currently logged-in user.
  • user-logged-in?
  • user-admin?
  • login-url (optional keyword: :destination): returns the Google authentication servlet URL, and forwards the user to the optional destination.
  • logout-url (optional keyword: :destination): performs logout, and forwards the user to the optional destination.

Memcache service

The appengine-magic.services.memcache namespace provides the following functions for the App Engine memcache. See App Engine documentation for detailed explanations of the underlying Java API.

  • statistics: returns the current memcache statistics.
  • clear-all!: wipes the entire cache for all namespaces.
  • contains? <key> (optional keyword: :namespace): checks if the given key exists in the cache.
  • delete! <key> (optional keywords: :namespace, :millis-no-readd): removes the key from the cache, optionally refraining from adding it for the given number of milliseconds. If the key argument is sequential, deletes all the named keys.
  • get <key> (optional keyword: :namespace): returns the value for the given key, but if the key argument is sequential, returns a map of key-value pairs for each supplied key.
  • put! <key> <value> (optional keywords: :namespace, :expiration, :policy): saves the given value under the given key; expiration is an instance of com.google.appengine.api.memcache.Expiration; policy is one of :always (the default), :add-if-not-present, or :replace-only.
  • put-map! <key-value-map> (optional keywords: :namespace, :expiration, :policy): writes the key-value-map into the cache. Other keywords same as for put.
  • increment! <key> <delta> (optional keywords: :namespace, :initial): atomically increments long integer values in the cache; if key is sequential, it increments all keys by the given delta.
  • increment-map! <key-delta-map> (optional keywords: :namespace, :initial): atomically increments long integer values by deltas given in the argument map.

Datastore

The appengine-magic.services.datastore namespace provides a fairly complete interface for the App Engine datastore.

A few simple examples:

(require '[appengine-magic.services.datastore :as ds])

(ds/defentity Author [^{:tag :key} name, birthday])
(ds/defentity Book [^{:tag :key} isbn, title, author])

;; Writes three authors to the datastore.
(let [will (Author. "Shakespeare, William" nil)
      geoff (Author. "Chaucer, Geoffrey" "1343")
      oscar (Author. "Wilde, Oscar" "1854-10-16")]
  ;; First, just write Will, without a birthday.
  (ds/save! will)
  ;; Now overwrite Will with an entity containing a birthday, and also
  ;; write the other two authors.
  (ds/save! [(assoc will :birthday "1564"), geoff, oscar]))

;; Retrieves two authors and writes book entites.
(let [will (first (ds/query :kind Author :filter (= :name "Shakespeare, William")))
      geoff (first (ds/query :kind Author :filter [(= :name "Chaucer, Geoffrey")
                                                   (= :birthday "1343")]))]
  (ds/save! (Book. "0393925870" "The Canterbury Tales" geoff))
  (ds/save! (Book. "143851557X" "Troilus and Criseyde" geoff))
  (ds/save! (Book. "0393039854" "The First Folio" will)))

;; Retrieves all Chaucer books in the datastore, sorting by descending title and
;; then by ISBN.
(let [geoff (ds/retrieve Author "Chaucer, Geoffrey")]
  (ds/query :kind Book
            :filter (= :author geoff)
            :sort [[title :dsc] :isbn]))

;; Deletes all books by Chaucer.
(let [geoff (ds/retrieve Author "Chaucer, Geoffrey")]
  (ds/delete! (ds/query :kind Book :filter (= :author geoff))))

The next example (which uses Compojure) demonstrates the use of entity groups and transactions.

(use '[clojure.pprint :only [pprint]]
     'compojure.core)
(require '[appengine-magic.core :as ae]
         '[appengine-magic.services.datastore :as ds])

(ds/defentity Parent [^{:tag :key} name, children])
(ds/defentity Child [^{:tag :key} name])

(defroutes entity-group-example-app-handler
  (GET  "/" [] {:headers {"Content-Type" "text/plain"} :body "started"})
  (POST "/new/:parent-name/:child-name" [parent-name child-name]
        (let [parent (or (ds/retrieve Parent parent-name)
                         ;; Note the use of ds/save! here. Unless an entity has
                         ;; been saved to the datastore, children cannot join
                         ;; the entity group.
                         (ds/save! (Parent. parent-name [])))
              ;; Note the use of ds/new* here: it is required so that a :parent
              ;; entity may be specified in the instantiation of a child entity.
              child (ds/new* Child [child-name] :parent parent)]
          ;; Updating the parent and the child together occurs in a transaction.
          (ds/with-transaction
            (ds/save! (assoc parent
                        :members (conj (:children parent) child-name)))
            (ds/save! child))
          {:headers {"Content-Type" "text/plain"}
           :body "done"}))
  (GET  "/parents" []
        (let [parents (ds/query :kind Parent)]
          {:headers {"Content-Type" "text/plain"}
           :body (str (with-out-str (pprint parents))
                      "\n"
                      (with-out-str (pprint (map ds/get-key-object parents))))}))
  (GET  "/children" []
        (let [children (ds/query :kind Child)]
          {:headers {"Content-Type" "text/plain"}
           :body (str (with-out-str (pprint children))
                      "\n"
                      (with-out-str (pprint (map ds/get-key-object children))))}))
  (ANY  "*" [] {:status 404 :body "not found" :headers {"Content-Type" "text/plain"}}))
  • defentity (optional keywords: :kind, :before-save, :after-load): defines an entity record type suitable for storing in the App Engine datastore. These entities work just like Clojure records. Internally, they implement an additional protocol, EntityProtocol, which provides the save! method. When defining an entity, you may specify ^{:tag :key} metadata on any one field of the record, and the datastore will use this as the primary key. Omitting the key will make the datastore assign an automatic primary key to the entity. Specifying the optional :kind keyword (a string value) causes App Engine to save the entity under the given "kind" name — like a datastore table. This allows kinds to remain disjoint from entity record types. The :before-save and :after-load keywords allow specifying a hook for transforming the entity on its way into the datastore before it is saved, or on its way out of the datastore after it is read.
  • new*: instantiates a datastore entity record. You may also use standard Clojure conventions to instantiate entity records, but creating entities destined for entity groups requires using new*. To put the new entity into a group, use the :parent keyword with the parent entity. Instantiating an entity does not automatically write it to the datastore. new* accepts either a vector of slot values or a map of slots.
  • get-key-object: this returns the primary Key object of the given entity. For a newly-instantiated entity lacking an explicit primary key, this method returns nil. Entities properly brought under entity groups using new* will have hierarchical keys. You should rarely need to use this explicitly.
  • key-str: this utility function returns the string representation of a Key object. The Key object may be given directly, or as the encoded result of call to com.google.appengine.api.datastore.KeyFactory/keyToString. In addition, the Key may be constructed by passing key-str the type (or kind string) of the object and its ID. This function is probably most useful for generating human-readable keys for storing entities in maps or memcache.
  • key-id: this utility function returns the numeric identifier of the numeric key of the given entity.
  • key-name: this utility function returns the string identifier of the string key of the given entity.
  • key-kind: this utility function returns the kind, as a string, of the given entity.
  • save!: calling this method on an entity writes it to the datastore, using the primary key returned by calling get-key-object on the entity. May be called on a sequence of entities.
  • delete!: removes an entity. May be called on a sequence of entities.
  • retrieve <entity-record-type> <primary-key> (optional keywords: :parent, :kind): this is a low-level entity retrieval function. It returns a record of the given type with the given primary key value. If the target entity belongs to an entity group, specify the parent using the optional keyword. If the target entity was stored with a different kind from the entity record type, specify the actual kind using the optional keyword. This function returns nil if the given key of the given kind does not exist.
  • exists? <entity-record-type> <primary-key> (optional keywords the same as for retrieve): used exactly like retrieve, but returns true if the given entity exists and false otherwise.
  • query (optional keywords: :kind, :ancestor, :filter, :sort, :keys-only?, :count-only?, :in-transaction?, :limit, :offset, :prefetch-size, :chunk-size, :entity-record-type): runs a query with the given parameters.
    • :kind: primarily identifies the App Engine entity kind. If given as an entity record type (recommended), the query returns a sequence of entity records of that type. If given as a string, it then checks to see if :entity-record-type is given, and uses that type if so; otherwise, the query returns generic EntityBase records.
    • :filter: one filter clause, or a list of clauses. Each consists of a symbol specifying the filter operation, a property name, and a target property value. See example.
    • :sort: one sort criterion, or a list of criteria. Each specified criterion defaults to ascending sort order, but may also sort in descending order.
  • with-transaction <body>: wraps the body in a transaction. Can be nested. (Keep the limitations of App Engine's transaction system in mind when using this.)
  • init-datastore-service: not normally needed. Only use this method if you want to modify the the read consistency and implicit transaction policies of the datastore service.
  • Type conversion functions: these help cast your data into a Java type which receives special treatment from App Engine.
    • as-blob: casts a byte array to com.google.appengine.api.datastore.Blob.
    • as-short-blob: casts a byte array to com.google.appengine.api.datastore.ShortBlob.
    • as-blob-key: casts a string to com.google.appengine.api.blobstore.BlobKey.
    • as-text: casts a string to com.google.appengine.api.datastore.Text.
    • as-link: casts a string to com.google.appengine.api.datastore.Link.

The Clojure interface to the Datastore has an additional feature: any entity field may be marked with the ^:clj metadata tag:

(ds/defentity TestEntity [^{:tag :key} test-id, ^:clj some-table])

The values of fields marked with the ^:clj tag will go into the datastore as strings produced by Clojure's prn-str function, and they will be retrieved as Clojure objects read by read-string. In other words, ^:clj fields will be serialized and retrieved using Clojure's reader. This is quite helpful for dealing with types which the datastore does not support: specifically maps (not even java.util.HashMap works) and sets (not even java.util.HashSet works). Keep in mind, however, that these fields are stored as instances of com.google.appengine.api.datastore.Text, which the datastore does not index.

Blobstore

The appengine-magic.services.blobstore namespace helps with the App Engine Blobstore service, designed for hosting large files. Note that the production App Engine only enables the Blobstore service for applications with billing enabled.

Using the Blobstore generally requires three components: an upload session, an HTTP multipart/form-data file upload (usually initiated through an HTML form), and an upload callback.

  1. Your application must first initiate an upload session; this gives it a URL to use for the corresponding HTTP POST request.
  2. Your application must provide a proper upload form, with the action pointing to the URL of the upload session, the method set to post, and enctype set to multipart/form-data; each uploaded file must have a name attribute.
  3. Your application must provide an upload callback URL. App Engine will make an HTTP POST request to that URL once the file upload completes. This callback's request will contain information about the uploaded files. The callback should save this data in some way that makes sense for the application. The callback implementation must end with an invocation of the callback-complete function. Do not attempt to return a Ring response map from an upload handler.
  4. A Ring handler which serves up a blob must end with an invocation of the serve function. Do not attempt to return a Ring response map from a blob-serving handler.

NB: In the REPL environment and in dev_appserver.sh, using the Blobstore writes entities into the datastore: __BlobInfo__ and __BlobUploadSession__. This does not happen in the production environment.

  • upload-url <success-path>: initializes an upload session and returns its URL. success-path is the URL of the upload callback.
  • delete! <blob-keys>: deletes the given blobs by their keys.
  • serve <ring-request-map> <blob-key>: modifies the given Ring request map to serve up the given blob.
  • callback-complete <ring-request-map> <destination>: redirects the uploading HTTP client to the given destination.
  • uploaded-blobs <ring-request-map>: returns a map of form upload name fields to blob keys.

This is confusing, but a Compojure example will help.

(use 'compojure.core)

(require '[appengine-magic.core :as ae]
         '[appengine-magic.services.datastore :as ds]
         '[appengine-magic.services.blobstore :as blobs])

(ds/defentity UploadedFile [^{:tag :key} blob-key])

(defroutes upload-demo-app-handler
  ;; HTML upload form; note the upload-url call
  (GET "/upload" _
       {:status 200
        :headers {"Content-Type" "text/html"}
        :body (str "<html><body>"
                   "<form action=\""
                   (blobs/upload-url "/done")
                   "\" method=\"post\" enctype=\"multipart/form-data\">"
                   "<input type=\"file\" name=\"file1\">"
                   "<input type=\"file\" name=\"file2\">"
                   "<input type=\"file\" name=\"file3\">"
                   "<input type=\"submit\" value=\"Submit\">"
                   "</form>"
                   "</body></html>")})
  ;; success callback
  (POST "/done" req
       (let [blob-map (blobs/uploaded-blobs req)]
         (ds/save! [(UploadedFile. (.getKeyString (blob-map "file1")))
                    (UploadedFile. (.getKeyString (blob-map "file2")))
                    (UploadedFile. (.getKeyString (blob-map "file3")))])
         (blobs/callback-complete req "/list")))
  ;; a list of all uploaded files with links
  (GET "/list" _
       {:status 200
        :headers {"Content-Type" "text/html"}
        :body (apply str `["<html><body>"
                           ~@(map #(format " <a href=\"/serve/%s\">file</a>"
                                           (:blob-key %))
                                  (ds/query :kind UploadedFile))
                           "</body></html>"])})
  ;; serves the given blob by key
  (GET "/serve/:blob-key" {{:strs [blob-key]} :params :as req}
       (blobs/serve req blob-key)))

(ae/def-appengine-app upload-demo-app #'upload-demo-app-handler)

Note that the Blobstore API primarily allows for browser-driven file uploads. appengine-magic includes a hack which allows an application to upload a blob without a browser.

  • upload-hack <contents> <success-path>: upload contents into the blobstore. When the upload completes, App Engine will make a request to the <success-path> URL, just like in a regular blobstore upload. This callback should record the blob key of the uploaded data. <contents> is either a single map, or a vector of maps, each with the following keys:
    • :field: the name of the imitation form field; used as keys in the result of uploaded-blobs.
    • :filename
    • :bytes: byte array of the uploaded data.

Mail service

The appengine-magic.services.mail namespace provides helper functions for sending and receiving mail in an App Engine application.

To send an mail message, construct it using make-message and make-attachment functions, and send it using the send function.

To receive incoming mail, first read and understand the relevant section in (Google's official documentation)[http://code.google.com/appengine/docs/java/mail/receiving.html]. You need to modify your application's appengine-web.xml, and you should add a security constraint for /_ah/mail/* URLs in your web.xml. In your application add a Ring handler for POST methods for URLs which begin with /_ah/mail.

  • make-attachment <filename> <bytes>: constructs an attachment object for a file with the given filename and consisting of the given bytes.
  • make-message: this function has many keyword parameters, and constructs a message object. The parameters are self-explanatory: :from, :to (takes a string or a vector), :subject, :cc (takes a string or a vector), :bcc (takes a string or a vector), :reply-to (takes a string or a vector), :text-body, :html-body, and :attachments (takes a vector).
  • send <msg>: sends the given message.
  • parse-message <ring-request-map>: returns a Clojure record of type appengine-magic.services.mail.MailMessage. Call this function inside the POST handler for /_ah/mail/*, and it will return the message sent in the given HTTP request.

NB: With Compojure, the only route which seems to work in the production App Engine for handling mail is /_ah/mail/*.

(use 'compojure.core)

(require '[appengine-magic.core :as ae]
         '[appengine-magic.services.mail :as mail])

(defroutes mail-demo-app-handler
  ;; sending
  (GET "/mail" _
       (let [att1 (mail/make-attachment "hello.txt" (.getBytes "hello world"))
             att2 (mail/make-attachment "jk.txt" (.getBytes "just kidding"))
             msg (mail/make-message :from "[email protected]"
                                    :to "[email protected]"
                                    :cc ["[email protected]" "[email protected]"]
                                    :subject "Test message."
                                    :text-body "Sent from appengine-magic."
                                    :attachments [att1 att2])]
         (mail/send msg)
         {:status 200
          :headers {"Content-Type" "text/plain"}
          :body "sent"}))
  ;; receiving
  (POST "/_ah/mail/*" req
       (let [msg (mail/parse-message req)]
         ;; use the resulting MailMessage object
         {:status 200})))

(ae/def-appengine-app mail-demo-app #'mail-demo-app-handler)

Task Queues service

The appengine-magic.services.task-queues namespace has helper functions for using task queues. As always, read Google's documentation on task queues, in particular the sections on configuring queue.xml, and on securing task URLs in web.xml. In addition, the section on scheduled tasks (cron.xml) is useful.

Use the add! function to add a new task to a queue, and provide a callback URL which implements the actual work performed by the task.

  • add! :url <callback-url> (optional keywords: :queue, :task-name, :join-current-transaction?, :params, :headers, :payload, :method, :countdown-ms, :eta-ms, :eta). The :url keyword is required. This function returns a task handle object.
    • :queue: name of the queue to use; if omitted, uses the system default queue. If provided, the queue must be defined in queue.xml.
    • :task-name: an optional name for the task.
    • :join-current-transaction?: defaults to false. If true, and if this occurs inside a datastore transaction context, then only adds this task to the queue if the transaction commits successfully.
    • :params: a map of form parameter key-value pairs for the callback. Do not combine with the :payload keyword.
    • :headers: a map of extra HTTP headers sent to the callback.
    • :payload: provides data for the callback. Can be a string, a vector of the form [<string> <charset>], or a vector of the form [<byte-array> <content-type>].
    • :method: supports :post, :delete, :get, :head, and :put. Default is :post.
    • :countdown-ms, :eta-ms, and :eta: scheduling parameters. Only one of these may be used at a time. :countdown-ms schedules a task for the given number of milliseconds from the time the add! function ran. :eta-ms schedules a task for the given number of milliseconds from the beginning of the epoch. :eta schedules execution for the time given by the a java.util.Date object.
  • purge! (optional keyword: :queue). Removes all tasks from the given queue.
    • :queue: name of the queue to use; if omitted, uses the system default queue.
  • delete! <task> (optional keyword: :queue). Deletes the given task from the given queue. The task may be specified by its name or by its handle object.
    • :queue: name of the queue to use; if omitted, uses the system default queue.

URL Fetch service

appengine-magic.services.url-fetch lets App Engine applications send arbitrary HTTP requests to external services.

  • fetch <url> (optional keywords: :method, :headers, :payload, :allow-truncate, :follow-redirects, :deadline).
    • :method: :get (default), :post, :delete, :head, or :put.
    • :headers: a map from header name (string) to value (string).
    • :payload: a Java byte array.
    • :allow-truncate: if true, allow App Engine to truncate a large response without an error; if false, throws an exception instead.
    • :follow-redirects: if true (default), follows request redirects.
    • :deadline: deadline for the requst, in seconds, expressed as a double.
    • :async?: if true, returns a future-like object. May block when derefed if it has not yet finished loading.

Images service

With appengine-magic.services.images, an application can (1) apply simple transformations to images, either in the blobstore or saved in byte arrays, and (2) access blobstore images through a CDN, with limited resizing capability.

  • get-image <image-arg>: if image-arg is a string or a blob key, returns an image reference to this blob; if image-arg is a byte array, returns an image corresponding to this byte array.
  • serving-url <blob-key>: returns a URL pointing directly at a blob image in a Google content delivery network.
    • :size: some resized versions of the given blob are available.
    • :crop?: some sizes can be cropped instead of resized.
  • transform <image-arg> <transforms>: applies one or more transformations to an image and returns the result as an instance of com.google.appengine.api.images.Image. Image/getImageData returns an array of bytes, useful as a response body. The image-arg argument can be an instance of Image, or a string blob key reference, or a byte array. The transforms argument is a vector of transformation objects, created using the transformation functions below.Keyword arguments:
    • :async?: if true, makes the transform function return a future-like object.
    • :quality: a value from 1 to 100.
    • :format: the output format, either :jpeg (alternatively :jpg) or :png.
  • Transformation functions:
    • crop* <left-x> <top-y> <right-x> <bottom-y>: crops an image, each argument is a fractional value from 0.0 to 1.0.
    • im-feeling-lucky*: tries to automatically correct color and contrast; does nothing in the development environment.
    • resize* <width> <height>
    • rotate* <degrees-clockwise>
    • horizontal-flip*
    • vertical-flip*

Channel service

App Engine has an implementation of server push through its Channel service (appengine-magic.services.channel). Using it requires a combination of client-side JavaScript event callbacks, and channel management on the server.

Conceptually, the server maintains one or more channels associated with a client ID (this is a small number; it is probably safest to assume only one channel per ID). The server opens a channel, which generates a channel token. This token must be passed to the connecting client; the client then uses the token to receive messages from the server.

  • create-channel <client-id>: creates a new channel and returns a token; JavaScript code will use this token to connect to the server.
  • make-message <client-id> <message-string>: makes a message object destined for all channels associated with the given client ID.
  • send <message-object>: sends the given message object.
  • send <client-id> <message-string>: sends the given string to the given client.
  • parse-presence <ring-request-map>: returns a ClientStatus record, containing two fields: :status and :id. If the client just connected, the :status is :connected; otherwise :disconnected.

NB: The current version of the Channel service does not help with channel bookkeeping. It probably cleans up idle channels internally, but does not inform the application of this. The application is responsible for keeping track of active channels.

The client needs to load the JavaScript code at /_ah/channel/jsapi:

<script src="/_ah/channel/jsapi" type="text/javascript"></script>

Once this library loads, the client must initiate a request in which the server can return the channel ID. Once this is done, the rest of the client API looks like this:

// read this from a normal server response
var channel_token = ...;

// open a "socket" to the server
var channel = new goog.appengine.Channel(channel_token);
var socket = channel.open();

// implement these callbacks to take action when an event occurs
socket.onopen = function(evt) { var data = evt.data; ... };
socket.onmessage = function(evt) { var data = evt.data; ... };
socket.onerror = function(evt) { var data = evt.data; ... };
socket.onclose = function(evt) { var data = evt.data; ... };

NB: The development implementations of the Channel service just poll the server for updates, and merely emulate server push. If you watch a browser request console, you'll see the polling requests.

Limitations

Incomplete features

The following Google services are not yet tested in the REPL environment:

  • Anything added in App Engine SDK 1.6.0
  • Anything added in App Engine SDK 1.7.0
  • Asynchronous Memcache API requests (from App Engine SDK 1.6.0)
  • Pull queues (from App Engine SDK 1.5.0)
  • Deferred API (from App Engine SDK 1.4.3)
  • Remote API (from App Engine SDK 1.4.3)
  • Files API (from App Engine SDK 1.4.3)
  • Adding and removing multiple tasks from queues in a single request
  • Datastore async queries
  • Datastore cursors
  • Compositing in the Images API
  • Multitenancy (namespaces)
  • Metadata queries (in the datastore API)
  • Capabilities
  • OAuth
  • XMPP

They may still work, but appengine-magic does not provide convenient Clojure interfaces for them, and may lack mappings for any necessary supporting URLs.

Warning

Google App Engine maintains a whitelist of permitted classes in Java's standard library. Other classes will cause your application to fail to deploy. Examples include threads and sockets. If you use those in your application, it will not work. This means that you cannot use Clojure's agents or futures. In addition, if one of your dependencies uses those, your application will also not work. For example, clojure.java.io (and its fore-runner, duck-streams from clojure-contrib), uses java.net.Socket, a forbidden class.

Whenever you add a new dependency, no matter how innocuous, you should make sure your app still works. dev_appserver.sh is a good place to start, but you must also test in the main App Engine. The two do not always load classes the same way.

Contributors

Many thanks to:

  • Brian Gruber
  • Marko Kocić
  • Conrad Barski
  • Yuri Niyazov
  • Alex Bolodurin
  • Stefan Kamphausen
  • Masashi Iizuka
  • Dave Lambert
  • Brian Rowe
  • Tobias Raeder
  • Mark Rathwell
  • Jieren Chen
  • Sridhar Ratnakumar
  • Justin Barton
  • Shawn Lewis
  • Christoph Mewes
  • Avishai Ish-Shalom

License

appengine-magic is distributed under the MIT license.

appengine-magic's People

Contributors

bgruber avatar christophmewes avatar davelambert avatar gcv avatar jurrchen avatar liquidz avatar nukemberg avatar rathwell avatar shawnlewis avatar yn avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

appengine-magic's Issues

Any ETA yet on App Engine 1.5.0 support?

I tried running appengine-magic with the new App Engine 1.5.0 and ran into some problems... I assume this is expected, since appengine-magic still has 1.4.3 as a dependency.

Just curious if there is any timeline for 1.5.0 support yet... I may have some time available around a month from now and will look into submitting some patches if 1.5.0 support is still an open issue at that time. For my current projects I am fine with 1.4.3 for now.

Thanks again for all the great work on appengine-magic!

-Conrad Barski

get the interactive mode running

Hey

Trying to use appengine-magic - looks really interesting

tried to fallow the setup steps closely to set up a development project

at one point in the tutorial they mention that you should compile core.clj, well i am not sure what to do here.
just tried in repl
user=> (compile 'testproj.core)
nil

after that it should be possible to run the webapp but it does not work.

i executed
user=> (require '[appengine-magic.core :as ae])
user=> (ae/serve testproj-app)

and this call did not succede.
(ae/serve testproj-app)
java.lang.Exception: Unable to resolve symbol: testproj-app in this context (NO_SOURCE_FILE:7)

at this point i do not now how to proceed. does anyone have any suggestion how to resolve this.

thanks for your help

best michael

Lacking documentation / obvious way to do multiple file upload

Implementing an upload form for single files is straightforward. Sadly things become more complicated for multiple file upload. An example or info on potential caveats in the development environment would be nice.

Easy to add the multiple attribute to a file input. My current understanding is that you need a new generated upload-url for every single file, which means Javascript is required. My best attempt so far, with my nearly non-existent understanding of JS, parts taken from several sources:

...
<input type="file" name="files" multiple />

<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
  google.load("jquery", "1.4");
</script>

<script type="text/javascript">
  input = $('[name="files"]')[0]

  input.addEventListener("change", function () {
   traverseFiles(this.files);
  }, false);

  function traverseFiles (files) {
    for (var i=0, l=files.length; i<l; i++) {
      prepareUpload(files[i]);
    };
  };

  function prepareUpload (file) {
    $.get('/admin/generate_upload_url', function (url) {
      uploadFile(file, url);
    });
  };

  function uploadFile (file, url) {
    xhr = new XMLHttpRequest();
    xhr.open("post", url, true);

    var boundary = "AJAX--------------" + (new Date).getTime();
    var contentType = "multipart/form-data; boundary=" + boundary;
    xhr.setRequestHeader("Content-Type", contentType);
    xhr.setRequestHeader("X-File-Name", file.fileName);
    xhr.setRequestHeader("X-File-Size", file.fileSize);
    xhr.setRequestHeader("X-File-Type", file.type);

    xhr.send(file);
    // also tried: xhr.sendAsBinary(file);
};
</script>
...

Clojure routing:

    (ns tlog.core
    (:require [appengine-magic.core :as ae]
          [net.cgrand.enlive-html :as en]
          [ring.util.response :as rsp]
          [tlog.models :as models]
          [tlog.views :as views]
          [appengine-magic.services.blobstore :as blobs])
    (:use ring.middleware.params
      net.cgrand.moustache))

; Validators for serving handler left out

(defn note-blobs
  [request]
  ;(let [b-map (blobs/uploaded-blobs request)]
  ;  (models/save-blobs b-map))
  (println "NOTE-BLOBS")
  (blobs/callback-complete request "/admin/file_done"))

(defn file-done
  [request]
  (println "FILE-DONE")
  (rsp/response  "OK"))

(defn serve-file
  "Take a __BlobInfo__ key and request, serve file from blobstore."
  [key request]
  (blobs/serve request key))

(def tlog-app-handler
     (app
          ["admin" &] {:get (app
             ; File upload form
             ["file"] (views/file-form)

             ["generate_upload_url"]
             (-> (blobs/upload-url "/admin/file_callback") rsp/response constantly)

             :post (app
                ["file_callback"] (-> note-blobs wrap-params)

                ["file_done"] (-> file-done wrap-params)))}

      ; Serve files from the blobstore
      [[name valid-filename-to-blob-key]] {:get (partial serve-file name)}))

(ae/def-appengine-app tlog-app #'tlog-app-handler)

This seems to work until to the point where the success handler should be called. That is, note-blobs is entered, but file-done isn't.

Consistent sessions

There doesn't seem to be a way of working sessions in a consistent manner between the GAE environment and the dev environment.

Appengine 1.6.3 + support

My question is: Do you have plans to rev appengine-magic from 1.5.5 to 1.6.x anytime soon? Full text search is coming
and I will need the 1.6.x versions to support it.

As an experiment, I changed the dependencies and the handler mappings in core_local.clj.
There is one serious issue regarding the serving of blobs from the blobstore.
There are minor issues with the datastore, taskqueue, and capabilities admin viewers.
I am a clojure and GAE novice, so I can't evaluate the severity.
If you have no plans to do it anytime soon, I can keep hacking at it.

P.S. I love your project. It really made getting off the ground in Clojure + GAE easy. ;)

Can't resolve appengine-magic

$ lein deps
Copying 19 files to /home/mvid/development/clojure/checkin/lib
Copying 21 files to /home/mvid/development/clojure/checkin/lib/dev
Exception in thread "main" java.lang.RuntimeException: java.util.zip.ZipException: error in opening zip file (NO_SOURCE_FILE:0)
at clojure.lang.Compiler.eval(Compiler.java:5440)
at clojure.lang.Compiler.eval(Compiler.java:5391)
at clojure.core$eval.invoke(core.clj:2382)
at clojure.main$eval_opt.invoke(main.clj:235)
at clojure.main$initialize.invoke(main.clj:254)
at clojure.main$script_opt.invoke(main.clj:270)
at clojure.main$main.doInvoke(main.clj:354)
at clojure.lang.RestFn.invoke(RestFn.java:457)
at clojure.lang.Var.invoke(Var.java:377)
at clojure.lang.AFn.applyToHelper(AFn.java:172)
at clojure.lang.Var.applyTo(Var.java:482)
at clojure.main.main(main.java:37)
Caused by: java.lang.RuntimeException: java.util.zip.ZipException: error in opening zip file
at clojure.lang.LazySeq.sval(LazySeq.java:47)
at clojure.lang.LazySeq.seq(LazySeq.java:56)
at clojure.lang.Cons.next(Cons.java:39)
at clojure.lang.RT.next(RT.java:560)
at clojure.core$next.invoke(core.clj:61)
at leiningen.deps$extract_native_deps.invoke(deps.clj:176)
at leiningen.deps$deps.doInvoke(deps.clj:198)
at clojure.lang.RestFn.invoke(RestFn.java:410)
at clojure.lang.Var.invoke(Var.java:365)
at clojure.lang.AFn.applyToHelper(AFn.java:161)
at clojure.lang.Var.applyTo(Var.java:482)
at clojure.core$apply.invoke(core.clj:542)
at leiningen.core$apply_task.invoke(core.clj:228)
at leiningen.core$_main.doInvoke(core.clj:294)
at clojure.lang.RestFn.invoke(RestFn.java:410)
at clojure.lang.AFn.applyToHelper(AFn.java:161)
at clojure.lang.RestFn.applyTo(RestFn.java:132)
at clojure.core$apply.invoke(core.clj:542)
at leiningen.core$_main.invoke(core.clj:297)
at user$eval42.invoke(NO_SOURCE_FILE:1)
at clojure.lang.Compiler.eval(Compiler.java:5424)
... 11 more
Caused by: java.util.zip.ZipException: error in opening zip file
at java.util.zip.ZipFile.open(Native Method)
at java.util.zip.ZipFile.(ZipFile.java:127)
at java.util.jar.JarFile.(JarFile.java:135)
at java.util.jar.JarFile.(JarFile.java:99)
at sun.reflect.GeneratedConstructorAccessor6.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at clojure.lang.Reflector.invokeConstructor(Reflector.java:160)
at leiningen.deps$extract_native_deps$fn__2277.invoke(deps.clj:176)
at clojure.core$map$fn__3699.invoke(core.clj:2096)
at clojure.lang.LazySeq.sval(LazySeq.java:42)
... 31 more

What is the zip file that it can't find? Here is my project.clj:
(defproject checkin "1.0.0-SNAPSHOT"
:description "universal checkin api"
:dependencies [[org.clojure/clojure "1.2.1"]
[org.clojure/clojure-contrib "1.2.0"]
[fi.foyt/foursquare-api "1.0.0"]
[clj-yelp "0.1.0-SNAPSHOT"]
[compojure "0.6.4"]]
:dev-dependencies [[appengine-magic "0.4.2"]]
:repositories {"foursquare" "http://foursquare-api-java.googlecode.com/svn/repository"})

Reduce and/or document dependencies

There are a lot of dependencies for appengine-magic. Some are marked as hard dependencies, while might not even be required.

It would be good if next to each dependency it is clearly stated by which services it is needed.

For example, I want to use only datastore api, and don't care about user/blobstore/memcached. What should I remove from project.clj?
Another examples are jstl, jsp, tomcat ... Am I safe to delete them if I don't want JSP support?
What about commons-*, are they here just for convenience or because ae core is using them?

Also, it would be good if all local repl related dependencies are moved to dev-dependencies.
Do I need those com.google.appengine.* dependencies in production, or those could be moved to dev-dependencies?

fails on Appengine Java SDK >= 1.6.4

I followed all the instructions and when I
dev_appserver.sh war/
and point my browser to localhost:8080/ i get the following error: (also fails deployed on the appengine site)

HTTP ERROR 500

Problem accessing /. Reason:

Invalid method Code length 77527 in class file clojure/core__init

Caused by:

java.lang.ClassFormatError: Invalid method Code length 77527 in class file clojure/core__init
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClassCond(ClassLoader.java:631)
at java.lang.ClassLoader.defineClass(ClassLoader.java:615)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:141)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:283)
at java.net.URLClassLoader.access$000(URLClassLoader.java:58)
at java.net.URLClassLoader$1.run(URLClassLoader.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at com.google.appengine.tools.development.IsolatedAppClassLoader.loadClass(IsolatedAppClassLoader.java:176)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:247)
at clojure.lang.RT.loadClassForName(RT.java:2024)
at clojure.lang.RT.load(RT.java:417)
at clojure.lang.RT.load(RT.java:398)
at clojure.lang.RT.doInit(RT.java:434)
at clojure.lang.RT.(RT.java:316)
at clojure.lang.Namespace.(Namespace.java:34)
at clojure.lang.Namespace.findOrCreate(Namespace.java:176)
at clojure.lang.Var.internPrivate(Var.java:149)
at guestbookclj.app_servlet.(Unknown Source)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at java.lang.Class.newInstance0(Class.java:355)
at java.lang.Class.newInstance(Class.java:308)
...

Fails sing Appengine Java SDK 1.6.4 or 1.6.5, works fine using version 1.6.3.1 though.
I have no idea how to fix this.

The task "lein appengine-new"

Am i getting something wrong?
What do i have to do to get leiningen running the appeninge-new task, i can't find a hint in the tutorial what modifies it to do so.

Error on lein appengine-prepare

When I run lein appengine-prepare it output this error:

java.io.FileNotFoundException: Could not locate lancet/core__init.class or lancet/core.clj on classpath: 
    at clojure.lang.RT.load(RT.java:432)
    at clojure.lang.RT.load(RT.java:400)
    at clojure.core$load$fn__4890.invoke(core.clj:5415)
    at clojure.core$load.doInvoke(core.clj:5414)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.core$load_one.invoke(core.clj:5227)
    at clojure.core$load_lib.doInvoke(core.clj:5264)
    at clojure.lang.RestFn.applyTo(RestFn.java:142)
    at clojure.core$apply.invoke(core.clj:603)
    at clojure.core$load_libs.doInvoke(core.clj:5298)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.core$apply.invoke(core.clj:603)
    at clojure.core$require.doInvoke(core.clj:5381)
    at clojure.lang.RestFn.invoke(RestFn.java:482)
    at leiningen.appengine_prepare$eval13$loading__4784__auto____14.invoke(appengine_prepare.clj:1)
    at leiningen.appengine_prepare$eval13.invoke(appengine_prepare.clj:1)
    at clojure.lang.Compiler.eval(Compiler.java:6511)
    at clojure.lang.Compiler.eval(Compiler.java:6501)
    at clojure.lang.Compiler.load(Compiler.java:6952)
    at clojure.lang.RT.loadResourceScript(RT.java:359)
    at clojure.lang.RT.loadResourceScript(RT.java:350)
    at clojure.lang.RT.load(RT.java:429)
    at clojure.lang.RT.load(RT.java:400)
    at clojure.core$load$fn__4890.invoke(core.clj:5415)
    at clojure.core$load.doInvoke(core.clj:5414)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.core$load_one.invoke(core.clj:5227)
    at clojure.core$load_lib.doInvoke(core.clj:5264)
    at clojure.lang.RestFn.applyTo(RestFn.java:142)
    at clojure.core$apply.invoke(core.clj:603)
    at clojure.core$load_libs.doInvoke(core.clj:5298)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.core$apply.invoke(core.clj:603)
    at clojure.core$require.doInvoke(core.clj:5381)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at leiningen.core.utils$require_resolve.invoke(utils.clj:29)
    at leiningen.core.utils$require_resolve.invoke(utils.clj:31)
    at leiningen.core.main$resolve_task.invoke(main.clj:130)
    at leiningen.core.main$resolve_task.invoke(main.clj:135)
    at leiningen.core.main$apply_task.invoke(main.clj:159)
    at leiningen.core.main$_main$fn__1665.invoke(main.clj:237)
    at leiningen.core.main$_main.doInvoke(main.clj:221)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.lang.Var.invoke(Var.java:415)
    at clojure.lang.AFn.applyToHelper(AFn.java:161)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.core$apply.invoke(core.clj:601)
    at clojure.main$main_opt.invoke(main.clj:324)
    at clojure.main$main.doInvoke(main.clj:427)
    at clojure.lang.RestFn.invoke(RestFn.java:436)
    at clojure.lang.Var.invoke(Var.java:423)
    at clojure.lang.AFn.applyToHelper(AFn.java:167)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.main.main(main.java:37)

I am using lein 2.0 and appengine-magic 0.5.1-SNAPSHOT

Add support for Servlets

Hi,

This is a feature request to add support for standard java web applications. I am in the situation where I need to start the appengine environment with Java servlets and web.xml style configuration. As I have seen currently appengine-magic currently only works with ring handlers.

Regards,
Razvan

(ds/retrieve) always returns nil

Hi, I'm trying your Datastore examples from README in swank repl.
I was able to successfully call ds/query, but all calls that depends on ds/retrieve are not working, since ds/retrieve fails to retrieve entity from datastore.

user>(ds/defentity Book [^:key isbn, title, author])
user.Author
user> (def will (first (ds/query :kind Author :filter (= :name "Shakespeare, William"))))
#'user/will
user> will
#:user.Author{:name "Shakespeare, William", :birthday nil}
user> (ds/retrieve Author "Shakespeare, William")
nil
user> (ds/exists? Author "Shakespeare, William")
false

I'm using clojure-1.3.0-alpha4 and latest appengine-magic from git 0.4.0 branch.

query etc do not appear to return "real" maps

I don't know if this is intended behaviour, but I do find it odd that the maps returned by appengine-magic from the datastore return true to map?, but don't appear to actually be maps. I say this because

(appengine-map :key)

Does not appear to work.

It isn't really a problem for me as I do uniformly use (:key {}) style, but I am aware that others don't.

Regards,
Folcon

PS: Please close if this is intended behaviour.

tag rebindable vars with ^:dynamic to prepare for Clojure 1.3

I want to shut up the warnings generated when doing lein appengine-prepare under Clojure 1.3.0-beta1 such as the following:

Warning: user-service not declared dynamic and thus is not dynamically rebindable, but its name suggests otherwise. Please either indicate ^:dynamic user-service or change the name.`

Most of these warnings come from the code that wraps the specific google appengine services.

I can probably take this one on and submit a patch as long as it's not too much more complicated than it seems to be. I'm pretty new to Clojure but can probably take on this kind of grunt work.

Mail Service throws java.lang.ClassNotFoundException: MailHandlerServlet

I've setup the mail service as described.

Adding:

  <inbound-services>
    <service>mail</service>
  </inbound-services>

to appengine-web.xml and

<servlet>
        <servlet-name>mailhandler</servlet-name>
        <servlet-class>MailHandlerServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>mailhandler</servlet-name>
        <url-pattern>/_ah/mail/*</url-pattern>
    </servlet-mapping>
    <security-constraint>
        <web-resource-collection>
            <url-pattern>/_ah/mail/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>

to web.xml.

Here is the log message from GAE.

2011-08-01 12:19:37.571
EXCEPTION 
java.lang.ClassNotFoundException: MailHandlerServlet
    at com.google.appengine.runtime.Request.process-74166ddc42b4a59b(Request.java)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:266)
    at org.mortbay.util.Loader.loadClass(Loader.java:91)
    at org.mortbay.util.Loader.loadClass(Loader.java:71)
    at org.mortbay.jetty.servlet.Holder.doStart(Holder.java:73)
    at org.mortbay.jetty.servlet.ServletHolder.doStart(ServletHolder.java:242)
    at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
    at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:685)
    at org.mortbay.jetty.servlet.Context.startContext(Context.java:140)
    at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1250)
    at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:517)
    at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:467)
    at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
    at com.google.net.rpc.impl.RpcUtil.runRpcInApplication(RpcUtil.java:422)
    at com.google.net.rpc.impl.Server$RpcTask.runInContext(Server.java:579)
    at com.google.tracing.TraceContext$TraceContextRunnable$1.run(TraceContext.java:449)
    at com.google.tracing.TraceContext.runInContext(TraceContext.java:689)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContextNoUnref(TraceContext.java:327)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContext(TraceContext.java:319)
    at com.google.tracing.TraceContext$TraceContextRunnable.run(TraceContext.java:447)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:636)
E 2011-08-01 12:19:37.572
javax.servlet.ServletContext log: unavailable
javax.servlet.UnavailableException: MailHandlerServlet
    at org.mortbay.jetty.servlet.Holder.doStart(Holder.java:79)
    at org.mortbay.jetty.servlet.ServletHolder.doStart(ServletHolder.java:242)
    at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
    at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:685)
    at org.mortbay.jetty.servlet.Context.startContext(Context.java:140)
    at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1250)
    at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:517)
    at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:467)
    at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
    at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.createHandler(AppVersionHandlerMap.java:202)
    at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.getHandler(AppVersionHandlerMap.java:171)
    at com.google.apphosting.runtime.jetty.JettyServletEngineAdapter.serviceRequest(JettyServletEngineAdapter.java:123)
    at com.google.apphosting.runtime.JavaRuntime.handleRequest(JavaRuntime.java:260)
    at com.google.apphosting.base.RuntimePb$EvaluationRuntime$2.handleRequest(RuntimePb.java:9805)
    at com.google.net.rpc.impl.RpcUtil.runRpcInApplication(RpcUtil.java:422)
    at com.google.net.rpc.impl.Server$RpcTask.runInContext(Server.java:579)
    at com.google.tracing.TraceContext$TraceContextRunnable$1.run(TraceContext.java:449)
    at com.google.tracing.TraceContext.runInContext(TraceContext.java:689)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContextNoUnref(TraceContext.java:327)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContext(TraceContext.java:319)
    at com.google.tracing.TraceContext$TraceContextRunnable.run(TraceContext.java:447)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:636)
W 2011-08-01 12:19:37.580
Failed startup of context com.google.apphosting.utils.jetty.RuntimeAppEngineWebAppContext@5bd978{/,/base/data/home/apps/cashewmanager/1.352248060040957903}
java.lang.NullPointerException
    at java.lang.Class.isAssignableFrom(Native Method)
    at org.mortbay.jetty.servlet.ServletHolder.doStart(ServletHolder.java:256)
    at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
    at org.mortbay.jetty.servlet.ServletHandler.initialize(ServletHandler.java:685)
    at org.mortbay.jetty.servlet.Context.startContext(Context.java:140)
    at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1250)
    at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:517)
    at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:467)
    at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:50)
    at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.createHandler(AppVersionHandlerMap.java:202)
    at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.getHandler(AppVersionHandlerMap.java:171)
    at com.google.apphosting.runtime.jetty.JettyServletEngineAdapter.serviceRequest(JettyServletEngineAdapter.java:123)
    at com.google.apphosting.runtime.JavaRuntime.handleRequest(JavaRuntime.java:260)
    at com.google.apphosting.base.RuntimePb$EvaluationRuntime$2.handleRequest(RuntimePb.java:9805)
    at com.google.net.rpc.impl.RpcUtil.runRpcInApplication(RpcUtil.java:422)
    at com.google.net.rpc.impl.Server$RpcTask.runInContext(Server.java:579)
    at com.google.tracing.TraceContext$TraceContextRunnable$1.run(TraceContext.java:449)
    at com.google.tracing.TraceContext.runInContext(TraceContext.java:689)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContextNoUnref(TraceContext.java:327)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContext(TraceContext.java:319)
    at com.google.tracing.TraceContext$TraceContextRunnable.run(TraceContext.java:447)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:636)
C 2011-08-01 12:19:37.583
Uncaught exception from servlet
javax.servlet.UnavailableException: Initialization failed.
    at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.createHandler(AppVersionHandlerMap.java:211)
    at com.google.apphosting.runtime.jetty.AppVersionHandlerMap.getHandler(AppVersionHandlerMap.java:171)
    at com.google.apphosting.runtime.jetty.JettyServletEngineAdapter.serviceRequest(JettyServletEngineAdapter.java:123)
    at com.google.apphosting.runtime.JavaRuntime.handleRequest(JavaRuntime.java:260)
    at com.google.apphosting.base.RuntimePb$EvaluationRuntime$2.handleRequest(RuntimePb.java:9805)
    at com.google.net.rpc.impl.RpcUtil.runRpcInApplication(RpcUtil.java:422)
    at com.google.net.rpc.impl.Server$RpcTask.runInContext(Server.java:579)
    at com.google.tracing.TraceContext$TraceContextRunnable$1.run(TraceContext.java:449)
    at com.google.tracing.TraceContext.runInContext(TraceContext.java:689)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContextNoUnref(TraceContext.java:327)
    at com.google.tracing.TraceContext$AbstractTraceContextCallback.runInInheritedContext(TraceContext.java:319)
    at com.google.tracing.TraceContext$TraceContextRunnable.run(TraceContext.java:447)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:636)
I 2011-08-01 12:19:37.586
This request caused a new process to be started for your application, and thus caused your application code to be loaded for the first time. This request may thus take longer and use more CPU than a typical request for your application.

Hope you can help.

Regards,
Folcon

"not found" error in lein appengine-prepare getDirectoryScanner for lib/dev

I think I'm following the directions at https://github.com/gcv/appengine-magic README.md correctly, but appengine-prepare hits an error trying to set up the "dev" directory. If I missed a step, thanks in advance for any advice.

To reproduce:

$ pwd
/tmp
$ lein --version
Leiningen 1.6.0 on Java 1.6.0_26 Java HotSpot(TM) 64-Bit Server VM
$ lein new simple-test
Created new project in: /tmp/simple-test
$ cd simple-test
$ rm src/simple_test/core.clj

edit project.clj to add :dev-dependencies [[appengine-magic "0.4.2"]]

$ cat project.clj
(defproject simple-test "1.0.0-SNAPSHOT"
:description "FIXME: write description"
:dependencies [[org.clojure/clojure "1.2.1"]]
:dev-dependencies [[appengine-magic "0.4.2"]])
$ lein deps
Copying 1 file to /private/tmp/simple-test/lib
Copying 21 files to /private/tmp/simple-test/lib/dev
$ lein appengine-new
making a skeleton for a Google App Engine application
created war directory war
created WEB-INF directory war/WEB-INF
created base entry point servlet /private/tmp/simple-test/src/simple_test/app_servlet.clj
created core namespace file /private/tmp/simple-test/src/simple_test/core.clj
web.xml written to war/WEB-INF/web.xml
appengine-web.xml written to war/WEB-INF/appengine-web.xml
$ lein appengine-prepare

Unexpected results:

preparing App Engine application simple-test for deployment
Compiling simple-test.app_servlet
Compilation succeeded.
Created dir: /private/tmp/simple-test/war/WEB-INF/lib
Copying 1 file to /private/tmp/simple-test/lib
Created /private/tmp/simple-test/simple-test-1.0.0-SNAPSHOT.jar
Moving 1 file to /private/tmp/simple-test/war/WEB-INF/lib
Copying 1 file to /private/tmp/simple-test/war/WEB-INF/lib
Exception in thread "main" /private/tmp/simple-test/lib/dev not found. (NO_SOURCE_FILE:0)
at clojure.lang.Compiler.eval(Compiler.java:5440)
at clojure.lang.Compiler.eval(Compiler.java:5391)
at clojure.core$eval.invoke(core.clj:2382)
at clojure.main$eval_opt.invoke(main.clj:235)
at clojure.main$initialize.invoke(main.clj:254)
at clojure.main$script_opt.invoke(main.clj:270)
at clojure.main$main.doInvoke(main.clj:354)
at clojure.lang.RestFn.invoke(RestFn.java:457)
at clojure.lang.Var.invoke(Var.java:377)
at clojure.lang.AFn.applyToHelper(AFn.java:172)
at clojure.lang.Var.applyTo(Var.java:482)
at clojure.main.main(main.java:37)
Caused by: /private/tmp/simple-test/lib/dev not found.
at org.apache.tools.ant.types.AbstractFileSet.getDirectoryScanner(AbstractFileSet.java:349)
at org.apache.tools.ant.taskdefs.Copy.execute(Copy.java:404)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at clojure.lang.Reflector.invokeMatchingMethod(Reflector.java:90)
at clojure.lang.Reflector.invokeNoArgInstanceMember(Reflector.java:265)
at lancet.core$copy.doInvoke(core.clj:153)
at clojure.lang.RestFn.invoke(RestFn.java:421)
at leiningen.appengine_prepare$appengine_prepare.invoke(appengine_prepare.clj:46)
at clojure.lang.Var.invoke(Var.java:365)
at clojure.lang.AFn.applyToHelper(AFn.java:161)
at clojure.lang.Var.applyTo(Var.java:482)
at clojure.core$apply.invoke(core.clj:542)
at leiningen.core$apply_task.invoke(core.clj:228)
at leiningen.core$_main.doInvoke(core.clj:294)
at clojure.lang.RestFn.invoke(RestFn.java:410)
at clojure.lang.AFn.applyToHelper(AFn.java:161)
at clojure.lang.RestFn.applyTo(RestFn.java:132)
at clojure.core$apply.invoke(core.clj:542)
at leiningen.core$_main.invoke(core.clj:297)
at user$eval42.invoke(NO_SOURCE_FILE:1)
at clojure.lang.Compiler.eval(Compiler.java:5424)
... 11 more

At this point here's the state of my project directory: there is indeed no lib/dev directory.

$ ls -R
README classes lib project.clj src test war

./classes:
appengine_magic simple_test

./classes/appengine_magic:
core$appengine_app_id.class servlet$servlet$fn__134.class
core$appengine_app_version.class servlet$servlet.class
core$appengine_base_url.class servlet$set_response_body.class
core$appengine_environment_type.class servlet$set_response_headers.class
core$def_appengine_app.class servlet__init.class
core$in_appengine_interactive_mode_QMARK_$fn__43.class utils$dash.class
core$in_appengine_interactive_mode_QMARK
.class utils$copy_stream.class
core$loading__4414__auto__.class utils$dash_.class
core$open_resource_stream.class utils$derefify_future$fn__67.class
core__init.class utils$derefify_future$reify__69.class
core_google__init.class utils$derefify_future.class
servlet utils$loading__4414__auto__.class
servlet$adapt_servlet_response.class utils$os_type.class
servlet$get_headers$fn__95.class utils$record.class
servlet$get_headers.class utils$xpath_replace_all.class
servlet$loading__4414__auto__.class utils$xpath_value$fn__86.class
servlet$make_request_map.class utils$xpath_value.class
servlet$make_servlet_service_method$fn__131.class utils__init.class
servlet$make_servlet_service_method.class

./classes/appengine_magic/servlet:
proxy$javax

./classes/appengine_magic/servlet/proxy$javax:
servlet

./classes/appengine_magic/servlet/proxy$javax/servlet:
http

./classes/appengine_magic/servlet/proxy$javax/servlet/http:
HttpServlet$0.class

./classes/simple_test:
app_servlet$service.class core$loading__4414__auto_.class
app_servlet$loading__4414__auto__.class core$simple_test_app_handler.class
app_servlet.class core__init.class
app_servlet__init.class

./lib:
clojure-1.2.1.jar

./src:
simple_test

./src/simple_test:
app_servlet.clj core.clj

./test:
simple_test

./test/simple_test:
test

./test/simple_test/test:
core.clj

./war:
WEB-INF

./war/WEB-INF:
appengine-web.xml lib web.xml

./war/WEB-INF/lib:
clojure-1.2.1.jar simple-test-1.0.0-SNAPSHOT.jar

However, if I clean the project and re-run deps, I do see a dev directory. It looks as if the appengine-prepare task is deleting lib/dev. Is it supposed to do that?

$ lein clean
Cleaning up.
$ lein deps
Copying 1 file to /private/tmp/simple-test/lib
Copying 21 files to /private/tmp/simple-test/lib/dev
$ ls -R
README classes lib project.clj src test war

./classes:

./lib:
clojure-1.2.1.jar dev

./lib/dev:
appengine-api-1.0-sdk-1.5.1.jar commons-exec-1.1.jar
appengine-api-labs-1.5.1.jar commons-fileupload-1.2.1.jar
appengine-api-stubs-1.5.1.jar commons-io-1.4.jar
appengine-local-runtime-1.5.1.jar commons-logging-1.0.3.jar
appengine-local-runtime-shared-1.5.1.jar geronimo-jsp_2.1_spec-1.0.1.jar
appengine-magic-0.4.2.jar jasper-runtime-5.0.28.jar
appengine-testing-1.5.1.jar jstl-1.1.2.jar
appengine-tools-api-1.5.1.jar ring-core-0.3.8.jar
clojure-1.2.1.jar servlet-api-2.5.jar
commons-codec-1.4.jar standard-1.1.2.jar
commons-el-1.0.jar

./src:
simple_test

./src/simple_test:
app_servlet.clj core.clj

./test:
simple_test

./test/simple_test:
test

./test/simple_test/test:
core.clj

./war:
WEB-INF

./war/WEB-INF:
appengine-web.xml lib web.xml

./war/WEB-INF/lib:
clojure-1.2.1.jar simple-test-1.0.0-SNAPSHOT.jar

How to push HTTP parameters through Blobstore file upload

Hi again,

I'm trying to create a file upload form that uploads files to the blobstore, then redirects to another URL but carries some form parameters with it. These form parameters carry the session context.

I create the form with Compojure and Appengine-magic as follows:

[:form {:action (blobs/upload-url "/add_file") :method "post" :enctype "multipart/form-data"}
        [:input {:type "file" :name "file-upload"}]
        [:input {:type "hidden" :name "user" :value "[email protected]"]
        [:input {:type "submit" :value text}]]

The generated HTML seems to be

<form action="/_ah/upload/agpjbG91ZHNwb29schwLEhVfX0Jsb2JVcGxvYWRTZXNzaW9uX18YngEM" enctype="multipart/form-data" method="post">
    <input name="file-upload" type="file" />
    <input type="submit" value="Add a file" />
    <input name=":user" type="hidden" value="[email protected]" />
</form>

However, the request received by my "/add_file" route is

{:remote-addr "127.0.0.1", 
:scheme :http, 
:response #<ResponseWrapper com.google.appengine.api.blobstore.dev.ServeBlobFilter$ResponseWrapper@60f9ccb3>,
:query-params {}, 
:session {}, 
:form-params {}, 
:multipart-params {}, 
:servlet #<HttpServlet$0 appengine_magic.servlet.proxy$javax.servlet.http.HttpServlet$0@2fee2d82>, 
:request-method :post, 
:query-string nil, 
:route-params {}, 
:content-type "text/plain", 
:request #<Request POST /add_job HTTP/1.1
...
}

that is, the [email protected] that I was hoping for doesn't seem to have made it through the upload. I also can't find it by inspecting the Jetty request in the :request field. I'm using compojure.handler.site, which handles parameters correctly in other contexts. What's the best way to push the parameters through?

Once again, thanks very much for Appengine-magic and sorry if this is not a good question.

Is it OK to ask questions here on the issues page, or would you prefer them somewhere else?

Anand

Eclipse CCW does not correctly detect interactive mode

appengine-magic.core/in-appengine-interactive-mode? does not detect Eclipse CCW because it doesn't seem to have any uniquely identifying stack frames. I tried to move away from stack-based environment detection, but I can't think of any way to distinguish between interactive REPL mode and compilation from lein appengine-prepare.

query :entity-record-type broken

I found no way to actually restrict the query to an entity-record-type within one kind.

Given
(ds/defentity Article [title, body, created, updated] :kind "Post")
(ds/defentity Comment [parent, index, author, link, body, created, updated] :kind "Post")

2 Articles stored and no Comments (and 2 other instances or another entity/kind):

tlog.models=> (ds/query :kind "Post" :count-only? true)
2
tlog.models=> (ds/query :kind "Post" :entity-record-type Comment :count-only? true)
2
tlog.models=> (ds/query :kind "Post" :entity-record-type Article :count-only? true)
2
tlog.models=> (ds/query :entity-record-type Comment :count-only? true)             
4
tlog.models=> (ds/query :entity-record-type Article :count-only? true)
4
tlog.models=> (ds/query :kind Article :entity-record-type Article :count-only? true)
0
tlog.models=> (ds/query :kind "Article" :entity-record-type Article :count-only? true)
0
tlog.models=> (ds/query :kind "Article" :entity-record-type "Article" :count-only? true)
0

I guess the workaround will be filtering for a non-nil property present in one type, but not the other.

countEntites 1000 limit

Hi

datastore counts are lmited to 1000 due to deprecated PreparedQuery countEntities method

in javadoc:
@deprecated int countEntities() Deprecated. Use countEntities(FetchOptions) instead.

In apopengine issues 3671
http://code.google.com/p/googleappengine/issues/detail?id=3671

Just adding the fetch-options-object used for the query works for me
Cheers Justin

From 2f3bc1d8e7655197b29c838d2f4302af0bb4e272 Mon Sep 17 00:00:00 2001
From: justin barton
Date: Sat, 27 Aug 2011 00:30:07 +1200
Subject: [PATCH] use fetchoptions in result count to avoid 1000 limit in counts


src/appengine_magic/services/datastore.clj | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)

diff --git a/src/appengine_magic/services/datastore.clj b/src/appengine_magic/services/datastore.clj
index c66dd33..18d4775 100644
--- a/src/appengine_magic/services/datastore.clj
+++ b/src/appengine_magic/services/datastore.clj
@@ -460,7 +460,7 @@
result-type (if (and (instance? Class kind) (extends? EntityProtocol kind))
kind
entity-record-type)

  •    result-count (.countEntities prepared-query)]
    
  •    result-count (.countEntities prepared-query fetch-options-object)]
    

    (cond count-only? result-count
    (zero? result-count) (list)
    :else (let [results (seq (.asIterable prepared-query fetch-options-object))

    1.7.4

Consider splitting appengine-magic to multiple libraries

while working on 1.8.x sdk support i encountered a classloader error in the dev server:

java.lang.ClassCastException: com.google.appengine.tools.development.ApiProxyLocalImpl cannot be cast to com.google.appengine.tools.development.ApiProxyLocal

This was caused by com.google.appengine/appengine-api-stubs and com.google.appengine/appengine-testing dependencies being copied into the war file. while it's simple enough to exclude these in appengine-prepare lein task, the correct solution would be to separate runtime dependencies from lein plugin and test dependencies.
I suggest splitting appegine-magic to 3 libraries - lein-appengine-magic, appengine-magic and appengine-magic-testing so that users can add dependencies to lein profiles as needed.

Writing (and reading) files to the blobstore throws an error

First off, I hope appengine magic continues to evolve as it fills a big gap and is very helpful. In any case, we just started using the FileService api to read and write files to the blobstore, as referenced here:

https://developers.google.com/appengine/docs/java/blobstore/overview#Writing_Files_to_the_Blobstore

It works perfectly in production, but does not appear to be compatible with appengine magic. For example, .createNewBlobFile works fine, but .openWriteChannel throws the following exception:

java.lang.NullPointerException 
at com.google.appengine.tools.development.RequestEndListenerHelper.register(RequestEndListenerHelper.java:39)
at com.google.appengine.api.files.dev.LocalFileService.open(LocalFileService.java:247)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

running version 0.5.0.

Getting null pointer exceptions

I've been getting null pointer exceptions when trying to run the simple-example.core code in the readme under overview.

The stacktrace is reproduced below:

No message.
  [Thrown class java.lang.NullPointerException]

Restarts:
 0: [QUIT] Quit to the SLIME top level

Backtrace:
  0: java.io.File.<init>(Unknown Source)
  1: sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
  2: sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
  3: sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
  4: java.lang.reflect.Constructor.newInstance(Unknown Source)
  5: clojure.lang.Reflector.invokeConstructor(Reflector.java:160)
  6: appengine_magic.core$start.doInvoke(core_local.clj:85)
  7: clojure.lang.RestFn.invoke(RestFn.java:439)
  8: appengine_magic.core$serve.doInvoke(core_local.clj:139)
  9: clojure.lang.RestFn.invoke(RestFn.java:410)
 10: user$eval2115.invoke(NO_SOURCE_FILE:1)
 11: clojure.lang.Compiler.eval(Compiler.java:5424)
 12: clojure.lang.Compiler.eval(Compiler.java:5391)
 13: clojure.core$eval.invoke(core.clj:2382)
 14: swank.commands.basic$eval_region.invoke(basic.clj:47)
 15: swank.commands.basic$eval_region.invoke(basic.clj:37)
 16: swank.commands.basic$eval807$listener_eval__808.invoke(basic.clj:71)
 17: clojure.lang.Var.invoke(Var.java:365)
 18: user$eval2113.invoke(NO_SOURCE_FILE)
 19: clojure.lang.Compiler.eval(Compiler.java:5424)
 20: clojure.lang.Compiler.eval(Compiler.java:5391)
 21: clojure.core$eval.invoke(core.clj:2382)
 22: swank.core$eval_in_emacs_package.invoke(core.clj:92)
 23: swank.core$eval_for_emacs.invoke(core.clj:239)
 24: clojure.lang.Var.invoke(Var.java:373)
 25: clojure.lang.AFn.applyToHelper(AFn.java:167)
 26: clojure.lang.Var.applyTo(Var.java:482)
 27: clojure.core$apply.invoke(core.clj:540)
 28: swank.core$eval_from_control.invoke(core.clj:99)
 29: swank.core$eval_loop.invoke(core.clj:104)
 30: swank.core$spawn_repl_thread$fn__493$fn__494.invoke(core.clj:309)
 31: clojure.lang.AFn.applyToHelper(AFn.java:159)
 32: clojure.lang.AFn.applyTo(AFn.java:151)
 33: clojure.core$apply.invoke(core.clj:540)
 34: swank.core$spawn_repl_thread$fn__493.doInvoke(core.clj:306)
 35: clojure.lang.RestFn.invoke(RestFn.java:397)
 36: clojure.lang.AFn.run(AFn.java:24)
 37: java.lang.Thread.run(Unknown Source)

save! and as-text weirdess

I already though I would have solved my issues with saving an entity with a Text property, but suddenly I couldn't reproduce my success, as if there are Gremlins in the system.

I started to simplify and experiment:

(ds/defentity Eek [body])

(defn save-article []
  (ds/save! (Eek. (ds/as-text "first")))
  (ds/save! (Eek. "second")))

Results in 2 entities with bodies: <Text: first>, second

  (ds/save! (Eek. (ds/as-text "first")))
  (ds/save! (Eek. (ds/as-text "second"))))

After clearing the datastore, results in no bodies

So what is it that I fail to understand, or what's going wrong?

ae/start unavailable when starting clojure with "lein run"

I helped thorwil in #clojure to debug a problem with starting the web server. His code looked something like this:

(ns my-ns ...)
;; define a handler, etc
(defn -main [](ae/start my-app))

By adding ":main my-ns" to the project.clj file, one would expect to be able to start the server by running "lein run", but clojure complained about the var ae/start being missing. I looked into the source of appengine-magic.core and found that it does something similar to "browser sniffing" in the javascript world, i.e. it conditionally loads code depending on what environment you are in. This seems a bit hackish to me. I guess this is about not including that code in production, but wouldn't it be better if the user could choose himself whether he wants to be able to start a local server? Perhaps this could be as simple as making core_local its own namespace. If the user wants to load it, he can simply require it. Maybe it could even be in its own dev-dependency jar.

Entites can be retrieved by key, but the key slot is nil

When saving new entity, I'm passing nil as a value of the key, and all save! and retrieve works fine, but the key of retrieved entities still contains nil.

(defentity Foo [^:key id, name])
(save! (Foo. nil "Bob"))

(:name (retrieve Foo 1)) ;=> "Bob"
(:id (retrieve Foo 1)) ;=> nil

This behaviour is not local to my system with testing from slime and dev_appserver.sh

Thanks,
Dmitri

new* with select-keys not working

Hi,

When using dataservices new* together with select-keys I get an java.lang.IllegalArgumentException:

(ds/new* Order {:id 100}) #works fine

(ds/new* Order (select-keys {:a 100 :b 200 :id 5000} '(:id))) #throws java.lang.IllegalArgumentException

My apologies if this is a false bug, I'm a greenhorn in clojure matters.

Cheers,
Razvan

Federated Login

Is Google federated login supported by appengine magic? If so then the documentation could really use some examples on how to use it, if not then consider this a feature request!

Add Support for Lein 2

$ lein --version
Leiningen 2.0.0-preview7 on Java 1.7.0_06-icedtea OpenJDK 64-Bit Server VM

$ lein appengine-new
making a skeleton for a Google App Engine application
java.lang.NullPointerException
    at java.io.File.<init>(File.java:389)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:525)
    at clojure.lang.Reflector.invokeConstructor(Reflector.java:180)
    at leiningen.appengine_new$appengine_new.invoke(appengine_new.clj:47)
    at clojure.lang.Var.invoke(Var.java:415)
    at clojure.lang.AFn.applyToHelper(AFn.java:161)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.core$apply.invoke(core.clj:603)
    at leiningen.core.main$resolve_task$fn__823.doInvoke(main.clj:73)
    at clojure.lang.RestFn.invoke(RestFn.java:410)
    at clojure.lang.AFn.applyToHelper(AFn.java:161)
    at clojure.lang.RestFn.applyTo(RestFn.java:132)
    at clojure.lang.AFunction$1.doInvoke(AFunction.java:29)
    at clojure.lang.RestFn.applyTo(RestFn.java:137)
    at clojure.core$apply.invoke(core.clj:603)
    at leiningen.core.main$apply_task.invoke(main.clj:95)
    at leiningen.core.main$_main$fn__862.invoke(main.clj:156)
    at leiningen.core.main$_main.doInvoke(main.clj:155)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.lang.Var.invoke(Var.java:415)
    at clojure.lang.AFn.applyToHelper(AFn.java:161)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.core$apply.invoke(core.clj:601)
    at clojure.main$main_opt.invoke(main.clj:324)
    at clojure.main$main.doInvoke(main.clj:427)
    at clojure.lang.RestFn.invoke(RestFn.java:436)
    at clojure.lang.Var.invoke(Var.java:423)
    at clojure.lang.AFn.applyToHelper(AFn.java:167)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.main.main(main.java:37)

Datastore fetch options :prefetch-size and :chunk-size give an exception

The Datastore fetch options :prefetch-size and :chunk-size give the following error:

java.lang.IllegalArgumentException: No matching field found: prefetchSize for class java.lang.Integer
at clojure.lang.Reflector.getInstanceField(Reflector.java:245)
at clojure.lang.Reflector.invokeNoArgInstanceMember(Reflector.java:267)
at appengine_magic.services.datastore$make_fetch_options_object.invoke(datastore.clj:294)
at appengine_magic.services.datastore$query_helper.invoke(datastore.clj:453)

This seems to be caused by a missing fetch-options-object argument in the setter calls, which causes the .prefetchSize and .chunkSize to be executed on the Integer argument itself instead of on the FetchOptions object. This happens with v0.4.0 but also applies to the master / v0.3.3 branch.

repl mystery

Here's something puzzling about running an ae-magic app in the repl. ae/def-appengine-app creates a map with keys :handler and :war-root. But if you precompile, the map seems to go away, leaving only the handler, which chokes ae/start.

$ lein clean
$ rm -rf war/WEB-INF/classes/ ;; start from scratch
$ lein repl
Compiling...
... ;; at this point everything is precompiled, no?
user=> (require '[appengine-magic.core :as ae])
nil
user=> sibtest.user/sibtest-user ;; my servlet; was compiled by lein repl
prepping sibtest-user servlet ;; this is from a println in servlet code - must be compiling?
CompilerException java.lang.RuntimeException: Unable to find static field: sibtest-user in class sibtest.user, compiling:(NO_SOURCE_PATH:1:606) ;; why not?
user=> (compile 'sibtest.user) ;; try to force it
sibtest.user ;; no recompile - lein compile knows src hasn't changed?
user=> sibtest.user/sibtest-user

'sibtest.user/user-handler ;;; BOINK! should be a map with :handler and :war-root keys

;; note that explicit compile made sibtest-user field available,
;; but what happened to the map created by ae/def-appengine-app?
user=> (ae/start sibtest.user/sibtest-user)
NullPointerException java.io.File. (File.java:389) ;; because no :war-root key

user=> (compile 'sibtest.user) ;; try again
sibtest.user ;; no "prepping" msg - lein compile knows src hasn't changed
user=> sibtest.user/sibtest-user

'sibtest.user/user-handler ;; no effect

;; touch or edit source
user=> (compile 'sibtest.user)
prepping sibtest-user servlet ;; this time it compiles
sibtest.user
user=> sibtest.user/sibtest-user
{:handler #<core$wrap_war_static$fn__1810 appengine_magic.core$wrap_war_static$fn__1810@8b7458e>, :war-root "/Users/gar/lein/sibtest/test/../war"} ;; bingo. this is what ae/start expects
user=> (ae/start sibtest.user/sibtest-user)
...
2013-03-29 06:02:23.609:INFO::Started [email protected]:8080

<Server Server@31689ae1> ;; that's better

What baffles me is what happens to the map that ae/def-appengine-app? It appears that the only way to get it right is to force a recompile while in the repl. But my expectation is that if I precompile everything it should just work. Why does the runtime find only the handler of the app and not the map?

Thanks,

Gregg Reynolds

ae architecture

Hi,

In the course of writing a leiningen plugin for appengine-magic (https://github.com/greynolds/gaem) I learned a lot about ae and have a number of suggestions as to future development. I'm not sure what the appropriate forum is for discussing them, so here I'll just mention one (which qualifies as a bug) and ask how you would prefer to discuss architectural issues if you are interested.

Here's the bug: appengine-magic.core/start hardcodes a single "handler" as the handler for "/". This effectively means that only one monolithic servlet is allowed; it makes web.xml irrelevant. I've messed around with the code a little bit but have not figured out how to remedy this. It requires more knowledge than I care to acquire about Jetty.

The plugin mentioned above (gaem) demonstrates that this is not necessary - the sample project uses two servlets, and it works on dev_appserver. But running it via a repl using ae/start exposes problems. You can only ae/start one servlet at a time, and when you do, that servlet services the entire "/*" tree, despite what web.xml says. There are a couple of other problems which I'll file separately.

Implementing the plugin has lead me to rethink the entire architecture of appengine-magic - not the internals of the api library, but the packaging of what are essentially separate tools and dev functions as part of ae. Such stuff could all be split out into a separate library (most likely a leiningen plugin like gaem) leaving a simplified ae lib that does the gae api only.

Before I go any further with this I'd like to get the developer's feedback (and blessing, I hope). What is your preferred forum for discussing such issues?

Thanks very much for making ae!

-Gregg Reynolds

Namespaces/Multitenancy issue and fix

The local_env_helper ApiProxy proxy creates a new, empty HashMap every time getAttributes is called. Since namespace is kept in here, it will never be set persistently.

(getAttributes )

I just created an environment-attributes atom, return it from getAttributes, clear it in the environment clearing function. Works okay so far.

Form params seem not to work, in dev environment only, after AppEngine 1.5+

Hi everyone- I had posted a couple of months ago about an issue I had after upgrading to AppEngine 1.5+. The issue seems to persist with the latest appengine-magic, though the problem could also be due to a bug in ring, hiccup, or for that matter my java/jetty environment.

The app works fine when pushed into production. I have pushed a minimal app that reproduces the problem to http://formbug.appspot.com/. If you enter a username and submit it, it will echo it back on the followup page, as it should. When I try it in the dev environment however, it doesn't work, even though it worked fine on AppEngine 1.4.x. In that case, I can see the forms param properly submitted via the browser, but never seems to appear in the request object in my defroutes. Therefore, no matter what is typed into the form, the echoed result is always "The username is ".

I should point out that I've done some relatively complex development with AppEngine 1.5+ and appengine-magic and everything is working perfectly for me except for this one isolated issue.

I already tried digging into ring/hiccup/appengine-magic to see if I could find an exact cause and create a patch, but there is enough going on here that I have a hard time pinning down the cause of the issue (but I plan to look into this some more)

Here is the core.clj for my test app:

(ns formbug.core
  (:use [compojure.core :only [defroutes GET POST PUT DELETE ANY]]
        [ring.middleware.params :only [wrap-params]])
  (:require [appengine-magic.core :as ae]))

(defroutes formbug-app-handler
  (GET "/" [req]
       "<form name='input' action='post_url' method='post'>
Username: <input type='text' name='user' />
<input type='submit' value='Submit' />
</form>")
  (POST "/post_url" [user]
        {:status 200
         :headers {"Content-Type" "text/plain"}
         :body (str "the username is " user)}
        ))

(ae/def-appengine-app formbug-app
  (-> #'formbug-app-handler
      wrap-params))    

Here is my project.clj:

(defproject formbug "1.0.0-SNAPSHOT"
  :description "FIXME: write description"
  :dependencies [[org.clojure/clojure "1.2.1"]
                 [org.clojure/clojure-contrib "1.2.0"]
                 [hiccup "0.3.6"]
                 [compojure "0.6.4"]]
  :dev-dependencies [[appengine-magic "0.4.4"]])        

Since this might conceivably be environment-related, here are my dev box specs:

 Ubuntu 11.04
 AppEngine Release: 1.5.3
 Java(TM) SE Runtime Environment (build 1.7.0-b147)
 Java HotSpot(TM) Server VM (build 21.0-b17, mixed mode)

If any of you happen to know why this is failing, please let me know. I realize it may not actually be an appengine-magic issue. I will keep investigating on my end, as well.

appengine-magic.services.user/login-url fails

Hi all,

i have the feeling I'm doing something daft, but (appengine-magic.services.user/login-url) results in

The API package 'user' or call 'CreateLoginURL()' was not found.
  [Thrown class com.google.apphosting.api.ApiProxy$CallNotFoundException]

This is with appengine-magic 0.4.2-SNAPSHOT and App Engine SDK 1.5.0.1. It also happened with appengine-magic 0.4.1 and App Engine SDK 1.4.3 . Confusingly, (appengine-magic.services.user/get-user-service) works.

Thanks very much for producing appengine-magic and such good documentation, and thanks in advance for any fixes or answers.

Anand

default-war-root fails sometimes in IntelliJ

default-war-root should look for "war" locaton using some other point (maybe, project.clj?) as it may fail sometimes. The reason is the following:

When I do development in REPL in IntelliJ La Clojure REPL, the baseLoader is here:
user=> (-> (clojure.lang.RT/baseLoader)
(.getResource "."))

<URL file:/home/jevgeni/projects/sandbox/lib/>

However, if I build/make the project, the REPL will have another baseLoader:
user=> (-> (clojure.lang.RT/baseLoader)
(.getResource "."))

<URL file:/home/jevgeni/projects/sandbox/out/production/sandbox/>

This all will make static resource unavailable, until you remove the "out" manually.

This issue is definitely IntelliJ's specific, but, I guess, the default-war-root should be more robust or give some feedback on this... Otherwise you can spend few hours searching why static resource is found on one machine and isn't on another :)

XMPP support started

Hi,
I just wanted to point out that I've started to add XMPP support on my xmpp branch here:
https://github.com/jolby/appengine-magic

I'm still new to GAE and appengine-magic, so if anyone sees something that is incorrect or that doesn't fit in with this project, let me know. I'll try to finish it up over the next week or so, and hopefully it can be included in this project.

Also, is there an official maillist for this project, or do folks mostly hang out on this issue list?

Thanks,
Joel Boehland

Intall broken

lein appengine-new fails with:

Error
DOMSource cannot be processed: check that saxon9-dom.jar is on the classpath
Exception in thread "main" net.sf.saxon.trans.XPathException: DOMSource cannot be processed: check that saxon9-dom.jar is on the classpath (NO_SOURCE_FILE:0)

^:clj should be ^{:tag :clj}?

Hi,

I think there is an error in the datastore part of the README.md documentation where it says that properties can be marked ^:clj for serialization, but it should be ^{:tag :clj} I think?

Why has this changed btw?

lein appengine-prepare broken with lein 1.7.0

I followed the following steps:

  1. lein new coco
  2. added :dev-dependencies [[appengine-magic "0.4.7"]]) to project.clj
  3. rm src/coco/core.clj
  4. lein deps
  5. lein appengine-new
  6. lein appengine-prepare

preparing App Engine application coco for deployment
Compiling coco.app_servlet
Compilation succeeded.
Created dir: /tmp/coco/war/WEB-INF/lib
Copying 1 file to /tmp/coco/lib
Overriding previous definition of reference to dependency.fileset
Overriding previous definition of reference to coco
Copying 20 files to /tmp/coco/lib/dev
No namespaces to :aot compile listed in project.clj.
Created /tmp/coco/coco-1.0.0-SNAPSHOT.jar
Moving 1 file to /tmp/coco/war/WEB-INF/lib
Copying 1 file to /tmp/coco/war/WEB-INF/lib
Copying 7 files to /tmp/coco/war/WEB-INF/lib

The jar coco-1.0.0-SNAPSHOT.jar does not contain any classes.
jar -tvf war/WEB-INF/lib/coco-1.0.0-SNAPSHOT.jar

70 Thu Mar 15 12:42:36 EDT 2012 META-INF/MANIFEST.MF
1416 Thu Mar 15 12:42:36 EDT 2012 META-INF/maven/coco/coco/pom.xml
93 Thu Mar 15 12:42:36 EDT 2012 META-INF/maven/coco/coco/pom.properties
173 Thu Mar 15 12:40:36 EDT 2012 project.clj

gae/devappserver.sh war:

in java.lang.ClassNotFoundException: coco.app_servlet

Adding :aot [coco.core coco.app_servlet] to project.clj and running lein appengine-prepare will add the classes
to the jar.

However, adding the :aot directive will result in NullPointerException when working in the repl:

lein clean
lein repl
(use 'coco.core)
(require '[appengine-magic.core :as ae])
(ae/serve coco-app)

results in NullPointerException java.io.File. (File.java:389).

If I remove the :aot directive from project.clj, (ae/serve coco-app) starts jetty with no errrors.

I am using lein version 1.7.0.

Am I missing something or is this behavior a bug ?

generating query filter dynamically

Hi,

I'm trying to generate the filter expression dynamically. It does not seem possible with the current implementation of query. Take following function

(defn filterexpand [params]
(map #(list '= (key %) (val %)) params))

Using it like this is not possible:
(defn search-orders [params]
(let [o (ds/query :kind Order (filterexpand params))]
(map-xml-list o)))

I also tried with a macro version of filterexpand:
(defmacro filterexpand [body]
(let [p (eval body)]
(map #(list '= (key %) (val%)) p)))

Can such usage of query be made possible?

Razvan

Add support for pre/post to ds/defentity

Would you consider adding support for a pre and post hook to ds/defentity? So if these are defined then a given function can be called before saving (pre-save) and after loading (post-load)?

Kind Regards,
Folcon

Unable to use com.google.appengine.api.ThreadManager in dev

When trying to access the ThreadManager in development (using appengine-magic.core/serve), the import works but this exception is thrown:

clojure.lang.Compiler$CompilerException: java.lang.RuntimeException: Unable to find static field: createThreadForCurrentRequest in class com.google.appengine.api.ThreadManager, compiling: x

If run the exact same code via the dev_appserver.sh script, it works just fine. I guess that some part of the threading stuff is not getting initialized by appengine-magic?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.