Google App Engine specific shortcuts
In addition to providing direct access to the App Engine services, Gaelyk also adds some syntax sugar on top of these APIs. Let's review some of these improvements.
Improvements to the low-level datastore API
Although it's possible to use JDO and JPA in Google App Engine,
Gaelyk also lets you use the low-level raw API for accessing the datastore,
and makes the Entity class from that API a bit more Groovy-friendly.
Using Entitys as maps or POJOs/POGOs
Note: POGO stands for Plain Old Groovy Object.
When you use the Entity class from Java, you have to use methods like setProperty() or getProperty()
to access the properties of your Entity, making the code more verbose than it needs to be (at least in Java).
Ultimately, you would like to be able to use this class (and its instances) as if they were just like a simple map, or as a normal Java Bean.
That's what Gaelyk proposes by letting you use the subscript operator just like on maps, or a normal property notation.
The following example shows how you can access Entitys:
import com.google.appengine.api.datastore.Entity
Entity entity = new Entity("person")
// subscript notation, like when accessing a map
entity['name'] = "Guillaume Laforge"
println entity['name']
// normal property access notation
entity.age = 32
println entity.age
Note: For string properties, Google App Engine usually distinguishes between strings longer or shorter than 500 characters. Short strings are just mere Java strings, while longer strings (>500 chars) should be wrapped in aTextinstance. Gaelyk shields you from taking care of the difference, and instead, when using the two notations above, you just have to deal with mere Java strings, and don't need to use theTextclass at all.
Some properties of your entities can be unindexed, meaning that they can't be used for your search criteria.
This may be the case for long text properties, for example a bio of a person, etc.
The property notation or subscript notation use normal indexed properties.
If you want to set unindexed properties, you can use the unindexed shorcut:
import com.google.appengine.api.datastore.Entity
Entity entity = new Entity("person")
entity.name = "Guillaume Laforge"
entity.unindexed.bio = "Groovy Project Manager..."
entity.unindexed['address'] = "Very long address..."
A handy mechanism exists to assign several properties at once, on your entities, using the << (left shift) operator.
This is particularly useful when you have properties coming from the request, in the params map variable.
You can the do the following to assign all the key/values in the map as properties on your entity:
// the request parameters contain a firstname, lastname and age key/values:
// params = [firstname: 'Guillaume', lastname: 'Laforge', title: 'Groovy Project Manager']
Entity entity = new Entity("person")
entity << params
assert entity.lastname == 'Laforge'
assert entity.firstname == 'Guillaume'
assert entity.title == 'Groovy Project Manager'
// you can also select only the key/value pairs you'd like to set on the entity
// thanks to Groovy's subMap() method, which will create a new map with just the keys you want to keep
entity << params.subMap(['firstname', 'lastname'])
Note: Gaelyk adds a few converter methods to ease the creation of instances of some GAE SDK types that can be used as properties of entities, using theasoperator:"foobar@gmail.com" as Email "foobar@gmail.com" as JID "http://www.google.com" as Link new URL("http://gaelyk.appspot.com") as Link "+33612345678" as PhoneNumber "50 avenue de la Madeleine, Paris" as PostalAddress "groovy" as Category 32 as Rating "32" as Rating "long text" as Text "some byte".getBytes() as Blob "some byte".getBytes() as ShortBlob "foobar" as BlobKey [45.32, 54.54] as GeoPt
Converting beans to entities and back
The mechanism explained above with type conversions (actually called "coercion") is also available
and can be handy for converting between a concrete bean and an entity.
Any POJO or POGO can thus be converted into an Entity,
and you can also convert an Entity to a POJO or POGO.
// given a POJO
class Person {
String name
int age
}
def e1 = new Entity("Person")
e1.name = "Guillaume"
e1.age = 33
// coerce an entity into a POJO
def p1 = e1 as Person
assert e1.name == p1.name
assert e1.age == p1.age
def p2 = new Person(name: "Guillaume", age: 33)
// coerce a POJO into an entity
def e2 = p2 as Entity
assert p2.name == e2.name
assert p2.age == e2.age
Note: The POJO/POGO classsimpleNameproperty is used as the entity kind. So for example, if thePersonclass was in a packagecom.foo, the entity kind used would bePerson, not the fully-qualified name. This is the same default strategy that Objectify is using.
Further customization of the coercion can be achieved by using annotations on your classes:
@Entityto add CRUD methods to the POGO class and also to set all fields unindexed by default. Following methods are added to the POGO instances:save()to save the object to the datastoredelete()to remove the object from the datastore
get(nameOrId)to retrieve the object from the datastore by its name or id or returnnullif entity not founddelete(nameOrId)to remove the object represented by its name or id from the datastorecount()to count all the object of given POGO class stored in the datastorecount{...query...}to count the objects which satisfies given queryfind{...query...}to find single object which satisfies given queryfindAll()to find all the object of given POGO class stored in the datastorefindAll{...query...}to find the objects which satisfies given queryiterate()to iterate over all the object of given POGO class stored in the datastoreiterate{...query...}to iterate over the objects which satisfies given query
@Keyannotation it also adds@Key long idproperty to the POGO class.
You can set default behavior from unindexed to indexed settingunidexedproperty of the annotations tofalse.@Keyto specify that a particular property or getter method should be used as the key for the entity (should be aStringor along)@Versionto specify that a particular property or getter method should be used as the unique autoincrement version for the entity (should be typelong)@Indexedfor properties or getter methods that should be indexed (ie. can be used in queries)@Unindexedfor properties or getter methods that should be set as unindexed (ie. on which no queries can be done)@Ignorefor properties or getter methods that should be ignored and not persisted@Parentto specify entity's parent entity
Here's an example of a Person bean using @Entity annotation,
whose key is a string login, whose biography should be unindexed,
and whose full name can be ignored since it's a computed property:
import groovyx.gaelyk.datastore.Entity
import groovyx.gaelyk.datastore.Key
import groovyx.gaelyk.datastore.Unindexed
import groovyx.gaelyk.datastore.Ignore
@Entity(unindexed=false)
class Person {
@Key String login
String firstName
String lastName
@Unindexed String bio
@Ignore String getFullName() { "$firstName $lastName" }
}
Thanks to @Entity annotation,
you get basic CRUD operations for free:
assert Person.count() == 0
def glaforge = new Person(
login: 'glaforge',
firstName: 'Guillaume',
lastName: 'Laforge',
bio: 'Groovy Project Manager'
).save()
assert Person.count() == 1
assert glaforge == Person.get('glaforge')
assert Person.findAll { where firstName == 'Guillaume' } == 1
glaforge.delete()
assert Person.count() == 0
Note: In turn, with this feature, you have a lightweight object/entity mapper. However, remember it's a simplistic solution for doing object/entity mapping, and this solution doesn't take into accounts relationships and such. If you're really interested in a fully featured mapper, you should have a look at Objectify or Twig.
List to Key conversion
Another coercion mechanism that you can take advantage of, is to use a list to Key conversion,
instead of using the more verbose KeyFactory.createKey() methods:
[parentKey, 'address', 333] as Key
[parentKey, 'address', 'name'] as Key
['address', 444] as Key
['address', 'name'] as Key
Added save() and delete() methods on Entity
In the previous sub-section, we've created an Entity, but we need to store it in Google App Engine's datastore.
We may also wish to delete an Entity we would have retrieved from that datastore.
For doing so, in a classical way, you'd need to call the save() and put() methods
from the DataService instance.
However, Gaelyk dynamically adds a save() and delete() method on Entity:
def entity = new Entity("person")
entity.name = "Guillaume Laforge"
entity.age = 32
entity.save()
Afterwards, if you need to delete the Entity you're working on, you can simply call:
entity.delete()
Added delete() and get() method on Key
Sometimes, you are dealing with keys, rather than dealing with entities directly — the main reaons being often for performance sake, as you don't have to load the full entity. If you want to delete an element in the datastore, when you just have the key, you can do so as follows:
someEntityKey.delete()
Given a Key, you can get the associated entity with the get() method:
Entity e = someEntityKey.get()
And if you have a list of entities, you can get them all at once:
def map = [key1, key2].get()
// and then access the returned entity from the map:
map[key1]
Converting Key to an encoded String and vice-versa
When you want to store a Key as a string or pass it as a URL parameter,
you can use the KeyFactory methods to encode / decode keys and their string representations.
Gaelyk provides two convenient coercion mechanisms to get the encoded string representation of a key:
def key = ['addresses', 1234] as Key
def encodedKey = key as String
And to retrieve the key from its encoded string representation:
def encodedKey = params.personKey // the encoded string representation of the key
def key = encodedKey as Key
Added withTransaction() method on the datastore service
Last but not least, if you want to work with transactions, instead of using the beginTransaction()
method of DataService, then the commit() and rollback() methods on that Transaction,
and doing the proper transaction handling yourself, you can use the withTransaction() method
that Gaelyk adds on DataService and which takes care of that boring task for you:
datastore.withTransaction {
// do stuff with your entities within the transaction
}
// enable cross group transactions
datastore.withTransaction(true) {
// do stuff with more than one entity group
new Entity('foo').save()
new Entity('bar').save()
}
The withTransaction() method takes a closure as the two parameters.
First one is optional boolean value. If set to true
cross group transactions are enabled. The second parameter is closure
and within that closure, upon its execution by Gaelyk,
your code will be in the context of a transaction.
Added get() methods on the datastore service
To retrieve entities from the datastore, you can use the datastore.get(someKey) method,
and pass it a Key you'd have created with KeyFactory.createKey(...):
this is a bit verbose, and Gaelyk proposes additional get() methods on the datastore service,
which do the key creation for you:
Key pk = ... // some parent key
datastore.get(pk, 'address', 'home') // by parent key, kind and name
datastore.get(pk, 'address', 1234) // by parent key, kind and id
datastore.get('animal', 'Felix') // by kind and name
datastore.get('animal', 2345) // by kind and id
This mechanism also works with the asynchronous datastore, as Gaelyk wraps the Future<Entity>
transparently, so you don't have to call get() on the future:
Key pk = ... // some parent key
datastore.async.get(pk, 'address', 'home') // by parent key, kind and name
datastore.async.get(pk, 'address', 1234) // by parent key, kind and id
datastore.async.get('animal', 'Felix') // by kind and name
datastore.async.get('animal', 2345) // by kind and id
Note: When you have aFuture<Entity> f, when you callf.someProperty, Gaelyk will actually lazily callf.get().someProperty, making the usage of the future transparent. However, note it only works for properties, it doesn't work for method call on futures, where you will have to callget()first. This transparent handling of future properties is working for allFutures, not justFuture<Entity>.
Querying
With the datastore API, to query the datastore, the usual approach is to create a Query,
prepare a PreparedQuery, and retrieve the results as a list or iterator.
Below you will see an example of queries used in the Groovy Web Console
to retrieve scripts written by a given author, sorted by descending date of creation:
import com.google.appengine.api.datastore.*
import static com.google.appengine.api.datastore.FetchOptions.Builder.*
// query the scripts stored in the datastore
// "savedscript" corresponds to the entity table containing the scripts' text
def query = new Query("savedscript")
// sort results by descending order of the creation date
query.addSort("dateCreated", Query.SortDirection.DESCENDING)
// filters the entities so as to return only scripts by a certain author
query.addFilter("author", Query.FilterOperator.EQUAL, params.author)
PreparedQuery preparedQuery = datastore.prepare(query)
// return only the first 10 results
def entities = preparedQuery.asList( withLimit(10) )
Fortunately, Gaelyk provides a query DSL for simplifying the way you can query the datastore. Here's what it looks like with the query DSL:
def entities = datastore.execute {
select all from savedscript
sort desc by dateCreated
where author == params.author
limit 10
}
Let's have a closer look at the syntax supported by the DSL.
There are four methods added dynamically to the datastore:
query{},iterate{}, execute{} and build{}.
query{} allow you to create a Query that you can use then to prepare a PreparedQuery.
iterate{} and execute{} is going further as it executes the query to return a single entity, a list, a count, etc.
build{} allow you to create a QueryBuilder you can modify later and call iterate{} and execute{}
on it.
Creating queries
You can create a Query with the datastore.query{} method.
The closure argument passed to this method supports the verbs select, from, where/and and sort.
Here are the various options of those verbs:
// select the full entity with all its properties
select all
// return just the keys of the entities matched by the query
select keys
// return just a few properties (must be indexed)
select name: String, age: Integer
// return just a few properties (must be indexed) as RawValue
select name, age
// specify the entity kind to search into
from entityKind
// specify that entities searched should be child of another entity
// represented by its key
ancestor entityKey
// add a filter operation
// operators allowed are: <, <=, ==, !=, >, >=, in
where propertyName < value
where propertyName <= value
where propertyName == value
where propertyName != value
where propertyName >= value
where propertyName > value
where propertyName in listOfValues
// you can use "and" instead of "where" to add more where clauses
// ascending sorting
sort asc by propertyName
// descending sorting
sort desc by propertyName
Notes:
- The entity kind of the
fromverb and the property name of thewhereverb andsort/byverbrs are actually mere strings, but you don't need to quote them.- Also, for the
whereclause, be sure to put the property name on the left-hand-side of the comparison, and the compared value on the right-hand-side of the operator.- When you need more than one
whereclause, you can useandwhich is a synonym ofwhere.- You can omit the
selectpart of the query if you wish: by default, it will be equivalent toselect all.- It is possible to put all the verbs of the DSL on a single line (thanks to Groovy 1.8 command chains notation), or split across several lines as you see fit for readability or compactness.
Executing queries
You can use the datastore.execute{} call to execute the queries,
or the datastore.iterate{} call if you want to get the results in the form of an iterator.
The select verb also provides additional values.
The from verb allows to specify a class to coerce the results to a POGO.
In addition, you can specify the FetchOptions with additional verbs like:
limit, offset, range, chunkSize, fetchSize
startAt, endAt. If your are facing queries that expires, use restart automatically.
This option only works with iterator methdod
to for flawless iteration.
// select the full entity with all its properties
select all
// return just the keys of the entities matched by the query
select keys
// return one single entity if the query really returns one single result
select single
// return the count of entities matched by the query
select count
// return just a few properties (must be indexed)
select name: String, age: Integer
// return just a few properties (must be indexed) as RawValue
select name, age
// from an entity kind
from entityKind
// specify the entity kind as well as a type to coerce the results to
from entityKind as SomeClass
// specify that entities searched should be child of another entity
// represented by its key
ancestor entityKey
where propertyName < value
where propertyName <= value
where propertyName == value
where propertyName != value
where propertyName >= value
where propertyName > value
where propertyName in listOfValues
// you can use "and" instead of "where" to add more where clauses
// ascending sorting
sort asc by propertyName
// descending sorting
sort desc by propertyName
// limit to only 10 results
limit 10
// return the results starting from a certain offset
offset 100
// range combines offset and limit together
range 100..109
// fetch and chunk sizes
fetchSize 100
chunkSize 100
// cursor handling
startAt cursorVariable
startAt cursorWebSafeStringRepresentation
endAt cursorVariable
endAt cursorWebSafeStringRepresentation
// automatically restart query when expired
// usefull for long running queries
restart automatically
Notes: If you use thefrom addresses as Addressclause, specifying a class to coerce the results into, if yourwhereandandclauses use properties that are not present in the target class, aQuerySyntaxExceptionwill be thrown.
For select all or select keys queries using iterate or execute the methods return instance
of QueryResultIterator
or QueryResultList from which cursor
and indexList properites could be read.
Asynchronous datastore
In addition to the "synchronous" datastore service, the App Engine SDK also provides an
AsynchrnousDatastoreService.
You can retrieve the asynchronous service with the datastore.async shortcut.
Gaelyk adds a few methods on entities and keys that leverage the asynchronous service:
entity.asyncSave()returns aFuture<Key>entity.asyncDelete()returns aFuture<Void>key.asyncDelete()returns aFuture<Void>
Datastore metadata querying
The datastore contains some special entities representing useful metadata, like the available kinds, namespaces and properties. Gaelyk provides shortcuts to interrogate the datastore for such entity metadata.
Namespace querying
// retrieve the list of namespaces (as a List<Entity>)
def namespaces = datastore.namespaces
// access the string names of the namespaces
def namespaceNames = namespaces.key.name
// if you want only the first two
datastore.getNamespaces(FetchOptions.Builder.withLimit(2))
// if you want to apply further filtering on the underlying datastore query
datastore.getNamespaces(FetchOptions.Builder.withLimit(2)) { Query query ->
// apply further filtering on the query parameter
}
Kind querying
// retrieve the list of entity kinds (as a List<Entity>)
def kinds = datastore.kinds
// get only the string names
def kindNames = kinds.key.name
// get the first kind
datastore.getKinds(FetchOptions.Builder.withLimit(10))
// futher query filtering:
datastore.getKinds(FetchOptions.Builder.withLimit(10)) { Query query ->
// apply further filtering on the query parameter
}
Properties querying
// retrieve the list of entity properties (as a List<Entity>)
def props = datastore.properties
// as for namespaces and kinds, you can add further filtering
datastore.getProperties(FetchOptions.Builder.withLimit(10)) { Query query ->
// apply further filtering on the query parameter
}
// if you want to retrive the list of properties for a given entity kind,
// for an entity Person, with two properties name and age:
def entityKindProps = datastore.getProperties('Person')
// lists of entity names
assert entityKindProps.key.parent.name == ['Person', 'Person']
// list of entity properties
assert entityKindProps.key.name == ['name', 'age']
The task queue API shortcuts
Google App Engine SDK provides support for "task queues".
An application has a default queue, but other queues can be added through the configuration of a
queue.xml file in /WEB-INF.
Note: You can learn more about queues and task queues, and how to configure them on the online documentation.
In your Groovlets and templates, you can access the default queue directly, as it is passed into the binding:
// access the default queue
defaultQueue
You can access the queues either using a subscript notation or the property access notation:
// access a configured queue named "dailyEmailQueue" using the subscript notation
queues['dailyEmailQueue']
// or using the property access notation
queues.dailyEmailQueue
// you can also access the default queue with:
queues.default
To get the name of a queue, you can call the provided getQueueName() method,
but Gaelyk provides also a getName() method on
Queue
so that you can write queue.name, instead of the more verbose queue.getQueueName() or
queue.queueName, thus avoid repetition of queue.
For creating tasks and submitting them on a queue, with the SDK you have to use the
TaskOptions.Builder.
In addition to this builder approach, Gaelyk provides a shortcut notation for adding tasks to the queue using named arguments:
// add a task to the queue synchronously
TaskHandle handle = queue.add countdownMillis: 1000, url: "/task/dailyEmail",
taskName: "dailyNewsletter",
method: 'PUT', params: [date: '20101214'],
payload: content, retryOptions: RetryOptions.Builder.withDefaults()
// add a task to the queue asynchronously
Future<TaskHandle> future = queue.addAsync countdownMillis: 1000, url: "/task/dailyEmail",
taskName: "dailyNewsletter",
method: 'PUT', params: [date: '20101214'],
payload: content, retryOptions: RetryOptions.Builder.withDefaults()
There is also a variant with an overloaded << operator for the second one:
// add a task to the queue
queue << [
countdownMillis: 1000, url: "/task/dailyEmail",
taskName: "dailyNewsletter",
method: 'PUT', params: [date: '20101214'],
payload: content,
retryOptions: [
taskRetryLimit: 10,
taskAgeLimitSeconds: 100,
minBackoffSeconds: 40,
maxBackoffSeconds: 50,
maxDoublings: 15
]
]
Email support
New send() method for the mail service
Gaelyk adds a new send() method to the
mail service,
which takes named arguments. That way, you don't have to manually build a new message yourself.
In your Groovlet, for sending a message, you can do this:
mail.send from: "app-admin-email@gmail.com",
to: "recipient@somecompany.com",
subject: "Hello",
textBody: "Hello, how are you doing? -- MrG",
attachment: [data: "Chapter 1, Chapter 2".bytes, fileName: "outline.txt"]
Similarily, a sendToAdmins() method was added to, for sending emails to the administrators of the application.
Note: There is asenderalias for thefromattribute. And instead of atextBodyattribute, you can send HTML content with thehtmlBodyattribute.
Note: There are two attachment attributes:attachmentandattachments.
attachmentis used for when you want to send just one attachment. You can pass a map with adataand afileNamekeys. Or you can use an instance ofMailMessage.Attachment.attachmentslets you define a list of attachments. Again, either the elements of that list are maps ofdata/fileNamepairs, or instances ofMailMessage.Attachment.
Incoming email messages
Your applications can also receive incoming email messages,
in a similar vein as the incoming XMPP messaging support.
To enable incoming email support, you first need to update your appengine-web.xml file as follows:
<inbound-services>
<service>mail</service>
</inbound-services>
In your web.xml file, you can eventually add a security constraint on the web handler
that will take care of treating the incoming emails:
...
<!-- Only allow the SDK and administrators to have access to the incoming email endpoint -->
<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>
...
You need to define a Groovlet handler for receiving the incoming emails
with a special route definition,
in your /WEB-INF/routes.groovy configuration file:
email to: "/receiveEmail.groovy"
Remark: You are obviously free to change the name and path of the Groovlet.
All the incoming emails will be sent as MIME messages through the request of your Groovlet.
To parse the MIME message, you'll be able to use the parseMessage(request) method
on the mail service injected in the binding of your Groovlet, which returns a
javax.mail.MimeMessage
instance:
def msg = mail.parseMessage(request)
log.info "Subject ${msg.subject}, to ${msg.allRecipients.join(', ')}, from ${msg.from[0]}"
XMPP/Jabber support
Your application can send and receive instant messaging through XMPP/Jabber.
Note: You can learn more about XMPP support on the online documentation.
Sending messages
Gaelyk provides a few additional methods to take care of sending instant messages, get the presence of users,
or to send invitations to other users.
Applications usually have a corresponding Jabber ID named after your application ID, such as yourappid@appspot.com.
To be able to send messages to other users, your application will have to invite other users, or be invited to chat.
So make sure you do so for being able to send messages.
Let's see what it would look like in a Groovlet for sending messages to a user:
String recipient = "someone@gmail.com"
// check if the user is online
if (xmpp.getPresence(recipient).isAvailable()) {
// send the message
def status = xmpp.send(to: recipient, body: "Hello, how are you?")
// checks the message was successfully delivered to all the recipients
assert status.isSuccessful()
}
Gaelyk once again decorates the various XMPP-related classes in the App Engine SDK with new methods:
- on
XMPPService's instanceSendResponse send(Map msgAttr): more details on this method belowvoid sendInvitation(String jabberId): send an invitation to a usersendInvitation(String jabberIdTo, String jabberIdFrom): send an invitation to a user from a different Jabber IDPresence getPresence(String jabberId): get the presence of this particular userPresence getPresence(String jabberIdTo, String jabberIdFrom): same as above but using a different Jabber ID for the request
- on
MessageinstancesString getFrom(): get the Jabber ID of the sender of this messageGPathResult xml(): get the XmlSlurper parsed document of the XML payloadList<String> getRecipients(): get a list of Strings representing the Jabber IDs of the recipients
- on
SendResponseinstancesboolean isSuccessful(): checks that all recipients received the message
To give you a little more details on the various attributes you can use to create messages to be sent,
you can pass the following attributes to the send() method of XMPPService:
- body : the raw text content of your message
- xml : a closure representing the XML payload you want to send
- to : contains the recipients of the message (either a String or a List of String)
- from : a String representing the Jabber ID of the sender
- type : either an instance of the
MessageTypeenum or a String ('CHAT','ERROR','GROUPCHAT','HEADLINE','NORMAL')
Note:bodyandxmlare exclusive, you can't specify both at the same time.
We mentioned the ability to send XML payloads, instead of normal chat messages:
this functionality is particularly interesting if you want to use XMPP/Jabber as a communication transport
between services, computers, etc. (ie. not just real human beings in front of their computer).
We've shown an example of sending raw text messages, here's how you could use closures in the xml
to send XML fragments to a remote service:
String recipient = "service@gmail.com"
// check if the service is online
if (xmpp.getPresence(recipient).isAvailable()) {
// send the message
def status = xmpp.send(to: recipient, xml: {
customers {
customer(id: 1) {
name 'Google'
}
}
})
// checks the message was successfully delivered to the service
assert status.isSuccessful()
}
Implementation detail: the closure associated with thexmlattribute is actually passed to an instance ofStreamingMarkupBuilderwhich creates an XML stanza.
Receiving messages
It is also possible to receive messages from users. For that purpose, Gaelyk lets you define a Groovlet handler that will be receiving the incoming messages. To enable the reception of messages, you'll have to do two things:
- add a new configuration fragment in
/WEB-INF/appengine-web.xml - add a route for the Groovlet handler in
/WEB-INF/routes.groovy
As a first step, let's configure appengine-web.xml by adding this new element:
<inbound-services>
<service>xmpp_message</service>
</inbound-services>
Similarily to the incoming email support, you can define security constraints:
...
<!-- Only allow the SDK and administrators to have access to the incoming jabber endpoint -->
<security-constraint>
<web-resource-collection>
<url-pattern>/_ah/xmpp/message/chat/</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
...
Then let's add the route definition in routes.groovy:
jabber to: "/receiveJabber.groovy"
Alternatively, you can use the longer version:
jabber chat, to: "/receiveJabber.groovy"
Remark: You are obviously free to change the name and path of the Groovlet.
All the incoming Jabber/XMPP messages will be sent through the request of your Groovlet.
Thanks to the parseMessage(request) method on the xmpp service
injected in the binding of your Groovlet, you'll be able to access the details of a
Message
instance, as shown below:
def message = xmpp.parseMessage(request)
log.info "Received from ${message.from} with body ${message.body}"
// if the message is an XML document instead of a raw string message
if (message.isXml()) {
// get the raw XML string
message.stanza
// or get a document parsed with XmlSlurper
message.xml()
}
XMPP presence handling
To be notified of users' presence, you should first configure appengine-web.xml
to specify you want to activate the incoming presence service:
<inbound-services>
<service>xmpp_presence</service>
</inbound-services>
Then, add a special route definition in routes.groovy:
jabber presence, to: "/presence.groovy"
Remark: You are obviously free to change the name and path of the Groovlet handling the presence requests.
Now, in your presence.groovy Groovlet, you can call the overriden XMPPService#parsePresence method:
// parse the incoming presence from the request
def presence = xmpp.parsePresence(request)
log.info "${presence.fromJid.id} is ${presence.available ? '' : 'not'} available"
XMPP subscription handling
To be notified of subscriptions, you should first configure appengine-web.xml
to specify you want to activate the incoming subscription service:
<inbound-services>
<service>xmpp_subscribe</service>
</inbound-services>
Then, add a special route definition in routes.groovy:
jabber subscription, to: "/subscription.groovy"
Remark: You are obviously free to change the name and path of the Groovlet handling the subscription requests.
Now, in your subscription.groovy Groovlet, you can call the overriden XMPPService#parseSubscription method:
// parse the incoming subscription from the request
def subscription = xmpp.parseSubscription(request)
log.info "Subscription from ${subscription.fromJid.id}: ${subscription.subscriptionType}}"
Enhancements to the Memcache service
Gaelyk provides a few additional methods to the
Memcache service,
to get and put values in the cache using Groovy's natural subscript notation,
as well as for using the in keyword to check when a key is present in the cache or not.
class Country implements Serializable { String name }
def countryFr = new Country(name: 'France')
// use the subscript notation to put a country object in the cache, identified by a string
// (you can also use non-string keys)
memcache['FR'] = countryFr
// check that a key is present in the cache
if ('FR' in memcache) {
// use the subscript notation to get an entry from the cache using a key
def countryFromCache = memcache['FR']
}
Note: Make sure the objects you put in the cache are serializable. Also, be careful with the last example above as the'FR'entry in the cache may have disappeared between the time you do theif (... in ...)check and the time you actually retrieve the value associated with the key from memcache.
Asynchronous Memcache service
The Memcache service is synchronous, but App Engine also proposes an asynchronous Memcache service
that you can access by calling the async property on the Memcache service instance:
memcache.async.put(key, value)
Note: Additionally, the usual property notation and subscript access notation are also available.
Closure memoization
As Wikipedia puts it, memoization is an optimization technique used primarily to speed up computer programs by having function calls avoid repeating the calculation of results for previously-processed inputs. Gaelyk provides such a mechanism for closures, storing invocation information (a closure call with its arguments values) in memcache.
An example, if you want to avoid computing expansive operations (like repeatedly fetching results from the datastore) in a complex algorithm:
Closure countEntities = memcache.memoize { String kind ->
datastore.prepare( new Query(kind) ).countEntities()
}
// the first time, the expensive datastore operation will be performed and cached
def totalPics = countEntities('photo')
/* add new pictures to the datastore */
// the second invocation, the result of the call will be the same as before, coming from the cache
def totalPics2 = countEntities('photo')
Note: Invocations are stored in memcache only for up to the 60 seconds request time limit of App Engine.
Enhancements related to the Blobstore and File services
Gaelyk provides several enhancements around the usage of the blobstore service.
Getting blob information
Given a blob key, you can retrieve various details about the blob when it was uploaded:
BlobKey blob = ...
// retrieve an instance of BlobInfo
BlobInfo info = blob.info
// directly access the BlobInfo details from the key itself
String filename = blob.filename
String contentType = blob.contentType
Date creation = blob.creation
long size = blob.size
Serving blobs
With the blobstore service, you can stream the content of blobs back to the browser, directly on the response object:
BlobKey blob = ...
// serve the whole blob
blob.serve response
// serve a fragment of the blob
def range = new ByteRange(1000) // starting from 1000
blob.serve response, range
// serve a fragment of the blob using an int range
blob.serve response, 1000..2000
Reading the content of a Blob
Beyond the ability to serve blobs directly to the response output stream with
blobstoreService.serve(blobKey, response) from your groovlet,
there is the possibility of
obtaining an InputStream
to read the content of the blob.
Gaelyk adds three convenient methods on BlobKey
to easily deal with a raw input stream or with a reader, leveraging Groovy's own input stream and reader methods.
The stream and reader are handled properly with regards to cleanly opening and closing those resources
so that you don't have to take care of that aspect yourself.
BlobKey blobKey = ...
blobKey.withStream { InputStream stream ->
// do something with the stream
}
// defaults to using UTF-8 as encoding for reading from the underlying stream
blobKey.withReader { Reader reader ->
// do something with the reader
}
// specifying the encoding of your choice
blobKey.withReader("UTF-8") { Reader reader ->
// do something with the reader
}
You can also fetch byte arrays for a given range:
BlobKey blob = ...
byte[] bytes
// using longs
bytes = blob.fetchData 1000, 2000
// using a Groovy int range
bytes = blob.fetchData 1000..2000
// using a ByteRange
def range = new ByteRange(1000, 2000) // or 1000..2000 as ByteRange
bytes = blob.fetchData range
Deleting a blob
Given a blob key, you can easily delete it thanks to the delete() method:
BlobKey blob = ...
blob.delete()
Iterating over and collecting BlobInfos
The blobstore service stores blobs that are identified by BlobKeys,
and whose metadata are represented by BlobInfo.
If you want to iterate over all the blobs from the blobstore,
you can use the BlobInfoFactory and its queryBlobInfos() method,
but Gaelyk simplifies that job with an each{} and a collect{} method
right from the blobstore service:
blobstore.each { BlobInfo info -> out << info.filename }
def fileNames = blobstore.collect { BlobInfo info -> info.filename }
Example Blobstore service usage
In this section, we'll show you a full-blown example.
First of all, let's create a form to submit a file to the blobstore,
in a template named upload.gtpl at the root of your war:
<html>
<body>
<h1>Please upload a text file</h1>
<form action="${blobstore.createUploadUrl('/uploadBlob.groovy')}"
method="post" enctype="multipart/form-data">
<input type="file" name="myTextFile">
<input type="submit" value="Submit">
</form>
</body>
</html>
The form will be posted to a URL created by the blobstore service,
that will then forward back to the URL you've provided when calling
blobstore.createUploadUrl('/uploadBlob.groovy')
Warning: The URL to he groovlet to which the blobstore service will forward the uploaded blob details
should be a direct path to the groovlet like /uploadBlob.groovy.
For an unknown reason, you cannot use a URL defined through the URL routing system.
This is not necessarily critical, in the sense that this URL is never deployed in the browser anyway.
Now, create a groovlet named uploadBlob.groovy stored in /WEB-INF/groovy
with the following content:
def blobs = blobstore.getUploadedBlobs(request)
def blob = blobs["myTextFile"]
response.status = 302
if (blob) {
redirect "/success?key=${blob.keyString}"
} else {
redirect "/failure"
}
In the groovlet, you retrieve all the blobs uploaded in the upload.gtpl page,
and more particularly, the blob coming from the myTextFile input file element.
Warning: Google App Engine mandates that you explicitly specify a redirection status code (301, 302 or 303), and that you do redirect the user somewhere else, otherwise you'll get some runtime errors.
We define some friendly URLs in the URL routing definitions for the upload form template, the success and failure pages:
get "/upload", forward: "/upload.gtpl"
get "/success", forward: "/success.gtpl"
get "/failure", forward: "/failure.gtpl"
You then create a failure.gtpl page at the root of your war directory:
<html>
<body>
<h1>Failure</h1>
<h2>Impossible to store or access the uploaded blob</h2>
</body>
</html>
And a success.gtpl page at the root of your war directory,
showing the blob details, and outputing the content of the blob (a text file in our case):
<% import com.google.appengine.api.blobstore.BlobKey %>
<html>
<body>
<h1>Success</h1>
<% def blob = new BlobKey(params.key) %>
<div>
File name: ${blob.filename} <br/>
Content type: ${blob.contentType}<br/>
Creation date: ${blob.creation}<br/>
Size: ${blob.size}
</div>
<h2>Content of the blob</h2>
<div>
<% blob.withReader { out << it.text } %>
</div>
</body>
</html>
Now that you're all set up, you can access http://localhost:8080/upload,
submit a text file to upload, and click on the button.
Google App Engine will store the blob and forward the blob information to your uploadBlob.groovy groovlet
that will then redirect to the success page (or failure page in case something goes wrong).
File service
The File service API provides a convenient solution for accessing the blobstore,
and particularly for programmatically adding blobs
without having to go through the blobstore form-based upload facilities.
Gaelyk adds a files variable in the binding of Groovlets and templates,
which corresponds to the FileService instance.
Writing text content
Inspired by Groovy's own withWriter{} method, a new method is available on
AppEngineFile
that can be used as follows, to write text content through a writer:
// let's first create a new blob file through the regular FileService method
def file = files.createNewBlobFile("text/plain", "hello.txt")
file.withWriter { writer ->
writer << "some content"
}
You can also specify three options to the withWriter{} method, in the form of named arguments:
- encoding: a string ("UTF-8" by default) defining the text encoding
- locked: a boolean (true by default) telling if we want an exclusive access to the file
- finalize: a boolean (true by default) to indicate if we want to finalize the file to prevent further appending
file.withWriter(encoding: "US-ASCII", locked: false, finalize: false) { writer ->
writer << "some content"
}
Writing binary content
In a similar fashion, you can write to an output stream your binary content:
// let's first create a new blob file through the regular FileService method
def file = files.createNewBlobFile("text/plain", "hello.txt")
file.withOutputStream { stream ->
stream << "Hello World".bytes
}
You can also specify two options to the withOutputStream{} method, in the form of named arguments:
- locked: a boolean (true by default) telling if we want an exclusive access to the file
- finalize: a boolean (true by default) to indicate if we want to finalize the file to prevent further appending
file.withOutputStream(locked: false, finalize: false) { writer ->
writer << "Hello World".bytes
}
Note: To finalize a file in the blobstore, App Engine mandates the file needs to be locked. That's why by defaultlockedandfinalizeare set to true by default. When you want to later be able to append again to the file, make sure to setfinalizeto false. And if you want to avoid others from concurrently writing to your file, it's better to setlockedto true.
Reading binary content
Gaelyk already provides reading capabilities from the blobstore support, as we've already seen,
but the File service also supports reading from AppEngineFiles.
To read from an AppEngineFile instance, you can use the withInputStream{} method,
which takes an optional map of options, and a closure whose argument is a BufferedInputStream:
file.withInputStream { BufferedInputStream stream ->
// read from the stream
}
You can also specify an option for locking the file (the file is locked by default):
file.withInputStream(locked: false) { BufferedInputStream stream ->
// read from the stream
}
Reading text content
Similarily to reading from an input stream, you can also read from a BufferedReader,
with the withReader{} method:
file.withReader { BufferedReader reader ->
log.info reader.text
}
You can also specify an option for locking the file (the file is locked by default):
file.withReader(locked: false) { BufferedReader reader ->
log.info reader.text
}
Miscelanous improvements
If you store a file path in the form of a string (for instance for storing its reference in the datastore),
you need to get back an AppEngineFile from its string representation:
def path = someEntity.filePath
def file = files.fromPath(path)
If you have a BlobKey, you can retrieve the associated AppEngineFile:
def key = ... // some BlobKey
def file = key.file
You can retrieve the blob key associated with your file (for example when you want to access an Image instance:
def key = file.blobKey
def image = key.image
And if you want to delete a file without going through the blobstore service, you can do:
file.delete()
Namespace support
Google App Engine SDK allows you to create "multitenant"-aware applications, through the concept of namespace, that you can handle through the NamespaceManager class.
Gaelyk adds the variable namespace into the binding of your groovlets and templates.
This namespace variable is simply the NamespaceManager class.
Gaelyk adds a handy method for automating the pattern of setting a temporary namespace and restoring it to its previous value,
thanks to the added of() method, taking a namespace name in the form of a string,
and a closure to be executed when that namespace is active.
This method can be used as follows:
// temporarily set a new namespace
namespace.of("customerA") {
// use whatever service leveraging the namespace support
// like the datastore or memcache
}
// once the closure is executed, the old namespace is restored
Images service enhancements
The images service and service factory wrapper
The Google App Engine SDK is providing two classes for handling images:
-
ImageServiceFactoryis used to retrieve the Images service, to create images (from blobs, byte arrays), and to make transformation operations. -
ImageServiceis used for applying transforms to images, create composite images, serve images, etc.
Very quickly, as you use the images handling capabilities of the API,
you quickly end up jumping between the factory and the service class all the time.
But thanks to Gaelyk, both ImagesServiceFactory and ImagesService are combined into one.
So you can call any method on either of them on the same images instance available in your groovlets and templates.
// retrieve an image stored in the blobstore
def image = images.makeImageFromBlob(blob)
// apply a resize transform on the image to create a thumbnail
def thumbnail = images.applyTransform(images.makeResize(260, 260), image)
// serve the binary data of the image to the servlet output stream
sout << thumbnail.imageData
On the first line above, we created the image out of the blobstore using the images service, but there is also a more rapid shortcut for retrieving an image when given a blob key:
def blobKey = ...
def image = blobKey.image
Note: blobKey.image creates an image object with only the blob key set.
It's not retrieving the actual image right away, nor its properties like its dimensions.
See this Gaelyk issue for more information,
or this Google App Engine issue.
In case you have a file or a byte array representing your image, you can also easily instanciate an Image with:
// from a byte array
byte[] byteArray = ...
def image = byteArray.image
// from a file directly
image = new File('/images/myimg.png').image
An image manipulation language
The images service permits the manipulation of images by applying various transforms, like resize, crop, flip (vertically or horizontally), rotate, and even an "I'm feeling lucky" transform! The Gaelyk image manipulation DSL allows to simplify the combination of such operations=
blobKey.image.transform {
resize 100, 100
crop 0.1, 0.1, 0.9, 0.9
horizontal flip
vertical flip
rotate 90
feeling lucky
}
The benefit of this approach is that transforms are combined within a single composite transform,
which will be applied in one row to the original image, thus saving on CPU computation.
But if you just need to make one transform, you can also call new methods on Image as follows:
def image = ...
def thumbnail = image.resize(100, 100)
def cropped = image.crop(0.1, 0.1, 0.9, 0.9)
def hmirror = image.horizontalFlip()
def vmirror = image.verticalFlip()
def rotated = image.rotate(90)
def lucky = image.imFeelingLucky()
Capabilities service support
Occasionally, Google App Engine will experience some reliability issues with its various services,
or certain services will be down for scheduled maintenance.
The Google App Engine SDK provides a service, the
CapabilitiesService,
to query the current status of the services.
Gaelyk adds support for this service, by injecting it in the binding of your groovlets and templates,
and by adding some syntax sugar to simplify its use.
import static com.google.appengine.api.capabilities.Capability.*
import static com.google.appengine.api.capabilities.CapabilityStatus.*
if (capabilities[DATASTORE] == ENABLED && capabilities[DATASTORE_WRITE] == ENABLED) {
// write something into the datastore
} else {
// redirect the user to a page with a nice maintenance message
}
Note: Make sure to have a look at the capability-aware URL routing configuration.
The services that can be queried are defined as static constants on
Capability
and currently are:
- BLOBSTORE
- DATASTORE
- DATASTORE_WRITE
- IMAGES
- MEMCACHE
- TASKQUEUE
- URL_FETCH
- XMPP
The different possible statuses are defined in the
CapabilityStatus
enum:
- ENABLED
- DISABLED
- SCHEDULED_MAINTENANCE
- UNKNOWN
Tip: Make sure to static importCapabilityandCapabilityStatusin order to keep your code as concise and readable as possible, like in the previous example, with:import static com.google.appengine.api.capabilities.Capability.* import static com.google.appengine.api.capabilities.CapabilityStatus.*
Additionally, instead of comparing explicitely against a specific CapabilityStatus,
Gaelyk provides a coercion of the status to a boolean (also called "Groovy Truth").
This allows you to write simpler conditionals:
import static com.google.appengine.api.capabilities.Capability.*
import static com.google.appengine.api.capabilities.CapabilityStatus.*
if (capabilities[DATASTORE] && capabilities[DATASTORE_WRITE]) {
// write something into the datastore
} else {
// redirect the user to a page with a nice maintenance message
}
Note: Only theENABLEDandSCHEDULED_MAINTENACEstatuses are considered to betrue, whereas all the other statuses are considered to befalse.
URLFetch Service improvements
Google App Engine offers the URLFetch Service to interact with remote servers,
to post to or to fetch content out of external websites.
Often, using the URL directly with Groovy's getBytes() or getText() methods is enough,
and transparently uses the URLFetch Service under the hood.
But sometimes, you need a bit more control of the requests you're making to remote servers,
for example for setting specific headers, for posting custom payloads, making asynchronous requests, etc.
Gaelyk 0.5 provides a convenient integration of the service with a groovier flavor.
Note: You may also want to have a look at HTTPBuilder's HttpURLClient for a richer HTTP client library that is compatible with Google App Engine.
Gaelyk decorates the URL class with 5 new methods, for the 5 HTTP methods GET, POST, PUT, DELETE, HEAD which can take an optional map for customizing the call:
url.get()url.post()url.put()url.delete()url.head()
Those methods return an
HTTPResponse
or a Future<HTTPResponse>
if the async option is set to true.
Let's start with a simple example, say, you want to get the Gaelyk home page content:
URL url = new URL('http://gaelyk.appspot.com')
def response = url.get()
assert response.responseCode == 200
assert response.text.contains('Gaelyk')
As you can see above,
Gaelyk adds a getText() and getText(String encoding) method to HTTPResponse,
so that it is easier to get textual content from remote servers —
HTTPResponse only provided a getContent() method that returns a byte array.
If you wanted to make an asynchronous call, you could do:
def future = url.get(async: true)
def response = future.get()
Allowed options
Several options are allowed as arguments of the 5 methods.
- allowTruncate: a boolean (false by default), to explicit if we want an exception to be thrown if the reponse exceeds the 1MB quota limit
- followRedirects: a boolean (true by default), to specify if we want to allow the request to follow redirects
- deadline: a double (default to 10), the number of seconds to wait for a request to succeed
- headers: a map of headers
- payload: a byte array for the binary payload you want to post or put
- params: a map of query parameters
- async: a boolean (false by defauly), to specify you want to do an asynchronous call or not
To finish on the URLFetch Service support, we can have a look at another example using some of the options above:
URL googleSearch = "http://www.google.com/search".toURL()
HTTPResponse response = googleSearch.get(params: [q: 'Gaelyk'],
headers: ['User-Agent': 'Mozilla/5.0 (Linux; X11)'])
assert response.statusCode == 200
assert response.text.contains('http://gaelyk.appspot.com')
assert response.headersMap.'Content-Type' == 'text/html; charset=utf-8'
Note:response.statusCodeis a synonym ofresponse.responseCode. And notice the convenientresponse.headersMapshortcut which returns a convenientMap<String, String>of headers instead of SDK'sresponse.headers'sList<HTTPHeader>.
Channel Service improvements
For your Comet-style applications, Google App Engine provides its
Channel service.
The API being very small, beyond adding the channel binding variable,
Gaelyk only adds an additional shortcut method for sending message
with the same send name as Jabber and Email support (for consistency),
but without the need of creating an instance of ChannelMessage:
def clientId = "1234"
channel.createChannel(clientId)
channel.send clientId, "hello"
Backend service support
The backend service support is quite minimal, from a Gaelyk perspective,
as only a backends (corresponding to a BackendService instance)
and lifecycle (the LifecycleManager) variables
have been added to the binding of Groovlets and templates.
In addition, a method for shutdown hooks was added that allows you to use a closure
instead of a ShutdownHook instance:
lifecycle.shutdownHook { /* shutting down logic */ }
You can also run code in separate background thread
instead of a ThreadManager instance:
Warning: If code that's not running in a backend attempts to start a background thread, it raises an exception.
backends.run { /* your background code */ }
Search service support
The full-text search functionality can be accessed with the search variable in the binding
of Groovlets and templates. You can also specify a special namespace to restrict the searches to that namespace.
// access the search service
search
// access the search service for a specific namespace
search['myNamespace']
You access a particular search index with the index() method,
where you specify the index name and consistency mode by passing their values as parameters:
def index = search.index("books")
To add documents to an index, you call the put() method on the index,
which takes a closure that accepts document(map) {} method calls.
You can specify several documents in a single put() call,
by simply making several document() calls inside the closure passed to put().
def index = search.index("books")
def response = index.put {
document(id: "1234", locale: US, rank: 3) {
title text: "Big bad wolf", locale: ENGLISH
published date: new Date()
numberOfCopies number: 35
summary html: "super story
", locale: ENGLISH
description text: "a book for children"
category atom: "children"
category atom: "book"
keyword text: ["wolf", "red hook"]
location geoPoint: [15,50]
}
// other documents with other document(...) {} calls
}
The named parameters passed to the document() methods can be id, locale
and rank.
Inside the closure, you can have as many field definitions of the form:
fieldName type: value or with an optional locale:
fieldName type: value, locale: someLocale.
Fields can be repeated in order to have multi-valued document fields or you
can specify map values as list: fieldName type: [one, two].
Empty lists and null values are ignored completely. Such fields
are not added to the document.
Once you have added documents to an index, you can search for them, and iterate over all the results:
// search the index
def results = index.search("wolf")
// iterate over all the resuts
results.each { ScoredDocument doc ->
assert doc.id == "1234"
assert doc.title == "Big bad wolf"
assert doc.numberOfCopies == 35
assert doc.summary.contains("story")
assert doc.keyword.size() == 2
assert "wolf" in doc.keyword
assert "red hook" in doc.keyword
}
As you can see, you can access a document field with the Groovy property notation.
When a field is multivalued, the doc.field property access actually returns a list of values.
Because search API is sometimes too much faulty, you can specify number of retries used by index.searchAsync.
Following code will attempt to search books related to wolfs three times before failing
def results = index.searchAsync("wolf", 3).get()
Note: You can make any closure returning Future retrying by usingnumberOfRetries * { future }notation. For example3 * { index.searchAsync("wolf") }will behave the same way as described above. Keep the code inside the closure reasonable small because the closure is called at the beginning of each attempt.
Advanced Full Text Search
App Engine Search API is very unfriendly. It overuses builders in any possible way. Fortunately, Gaelyk provides a search DSL for simplifying the way you running full text queries. The DSL is very close to the datastore query DSL:
def documents = search.search {
select all from books
sort desc by published, SearchApiLimits.MINIMUM_DATE_VALUE
where title =~ params.title
and keyword = params.keyword
limit 10
}
The query DSL could be used with two methods on search service object: search and searchAsync.
Let's have a closer look at the syntax supported by the DSL:
// select the full document with all its fields
select all
// return just the ids of the documents matched by the query
select ids
// return just a few document's fields
select name, age
// return just a few expresions, see https://developers.google.com/appengine/docs/java/search/overview#Expressions
// methods like distance or geopoint are also supported
select numberOfCopies: numberOfCopies - 10, body: snippet(params.body, body), rating: max(rating, 10)
// specify the index to search into
from books
// add a filter operation
// operators allowed are: <, <=, ==, !=, >, >=, =~, ~
// date values are properly handled for you
where propertyName < expression
where propertyName <= expression
where propertyName == expression
where propertyName != expression
where propertyName >= expression
where propertyName > expression
// instead of query operator ':' use Groovy matches operator "=~""
where propertyName =~ value
// to search for singular and plural form of the word you can use "~" operator
where propertyName == ~value
// you can use "and" instead of "where" to add more where clauses
// to use logical disjunction ("or"), you can use "||" logical operator
// you can use "&&" as well for "and"
where propertyName == value || propertyName == other
// you can also use built-in methods such as distance, geopoint, max, min, count
where distance(geopoint(10, 50), locality) < 10
// ascending sorting, the default value is mandatory
sort asc by propertyName, defaultValue
// descending sorting, the default value is mandatory
sort desc by propertyName, defaultValue
// limit to only 10 results
limit 10
// return the results starting from a certain offset
offset 100
// cursor handling
startAt cursorVariable
startAt cursorWebSafeStringRepresentation
// limits sorting
limit sort to 1000
// sets number found accuracy to 150
number found accuracy 150
Notes:
- The expressions are actually mere strings, but you don't need to quote them.
- Also, for the
whereclause, be sure to put the property name on the left-hand-side of the comparison, and the compared value on the right-hand-side of the operator.- When you need more than one
whereclause, you can useandwhich is a synonym ofwhere.- You can omit the
selectpart of the query if you wish: by default, it will be equivalent toselect all.- It is possible to put all the verbs of the DSL on a single line (thanks to Groovy 1.8 command chains notation), or split across several lines as you see fit for readability or compactness.
