Skip to content

Usage Examples

Chidiebere Okwudire edited this page Jan 24, 2017 · 19 revisions

Introduction

This page illustrates how to use parse4cn1. For consistency, these examples mimic those provided in the REST API documentation whenever possible.

Note that it is possible that some functionality described here is not yet available in the open source Parse Server! See known issues with Parse Server for more info.


Initialization

Call the initialize() method (e.g., in the initVars() method of your state machine class) before making any Parse API calls.

public class StateMachine extends StateMachineBase {
    /**
     * this method should be used to initialize variables instead of
     * the constructor/class scope to avoid race conditions
     */
    protected void initVars(Resources res) {
         Parse.initialize(API_ENDPOINT, APP_ID, CLIENT_KEY);
    }

}

Note that with the migration to Parse server:

  • API_ENDPOINT is the URL of your Parse backend.
  • CLIENT_KEY is optional and can be null.

Objects

Creating Objects

ParseObject gameScore = ParseObject.create("GameScore");
gameScore.put("score", 1337);
gameScore.put("playerName", "Sean Plott");
gameScore.put("cheatMode", false);
gameScore.save();

Retrieving Objects

Objects can be retrieved in two ways: Directly fetching by id (simpler) or using a query. Fields can be accessed using the appropriate getX() method or the generic get() method that returns a Java Object.

Fetching by Id

ParseObject gameScore = ParseObject.fetch("GameScore" /*class name*/, "nsC2NdmCuQ" /*objectId*/);
int score = gameScore.getInt("score");
String playerName = gameScore.getString("playerName");
boolean cheatMode = gameScore.getBoolean("cheatMode");

Retrieving Object Metadata

The three special values have their own accessors:

String objectId = gameScore.getObjectId();
Date updatedAt = gameScore.getUpdatedAt();
Date createdAt = gameScore.getCreatedAt();

Note that when an object is created Parse returns only the objectId and createdAt fields. However, parse4cn1 automatically sets the updatedAt to equal the createdAt time.

Including Child Objects

See the query section for examples

Updating Objects

// Assuming ParseObject 'gameScore' with key 'score'  already exists.
gameScore.put("score", 73453);
gameScore.save();

Counters

// increment by 1
gameScore.increment("score");  // use increment(key, amount) for non-unity increment
gameScore.save();

// decrement
gameScore.increment("score", -1);
gameScore.save();

Arrays

Atomic operations (Add, AddUnique and Remove) as specified in the REST API are not (yet) implemented. However, note that all array operations can be implemented by creating an updating a List (or JSONArray) field as illustrated below

// Add
List<String> skillList= new ArrayList<String>();
skillList.add("flying");
skillList.add("kungfu");
gameScore.put("skills", skillList);
gameScore.save();

// Remove
skillList = gameScore.getList("skills");
skillList.remove("kungfu");
gameScore.put("skills", skillList);
gameScore.save();

Deleting Objects

Deleting an Entire Object

gameScore.delete();

Deleting a Field

gameScore.deleteField("score");

Batch Operations

// Create objects locally
final ParseObject gameScore1 = ParseObject.create(classGameScore);
gameScore1.put("playerName", "Sean Plott");
gameScore1.put("score", 1337);

final ParseObject gameScore2 = ParseObject.create(classGameScore);
gameScore2.put("playerName", "ZeroCool");
gameScore2.put("score", 1338);

// Run a batch to persist the objects to Parse
ParseBatch batch = ParseBatch.create();
batch.addObject(gameScore1, ParseBatch.EBatchOpType.CREATE);
batch.addObject(gameScore2, ParseBatch.EBatchOpType.CREATE);

if (batch.execute()) {
     // Batch operation succeeded
}

// Updating and deleting via batch operations is also possible
gameScore1.put("score", 999999);
batch.addObject(gameScore1, ParseBatch.EBatchOpType.UPDATE);
batch.addObject(gameScore2, ParseBatch.EBatchOpType.DELETE);

batch.execute();

Users

Signing Up



ParseUser user = ParseUser.create("cooldude6", "p_n7!-e8");
user.put("phone", "415-392-0202");
user.signUp();

// Session token is created and can be retrieved
final String sessionToken = user.getSessionToken();

// User-defined fields are accessible as on other Parse objects
final String phoneNumber = user.getString("phone");

Logging In

ParseUser user = ParseUser.create("cooldude6", "p_n7!-e8");
user.login();

// Session token is created and can be retrieved
final String sessionToken = user.getSessionToken();

Logging Out

user.logout(); // Effectively deletes the current session (cf. Deleting Sessions)

Verifying Emails

final String emailVerified = "emailVerified";
user.put("email", "coolguy@iloveapps.com");
user.save();

// If email verification is enabled, verification email is sent and the emailVerified field is automatically created
boolean emailVerified = user.getBoolean("emailVerified");

Requesting a Password Reset

ParseUser.requestPasswordReset("coolguy@iloveapps.com");

Retrieving Users

ParseUser userById = ParseUser.fetch("users" /* end point */, "g7y9tkhB7O" /* object id */);

Validating Session Tokens / Retrieving Current User

ParseUser userBySession = ParseUser.fetchBySession("r:pnktnjyb996sj4p156gjtp4im");

Updating Users

user.put("phone", "415-369-6201");
user.save();

Querying

ParseQuery<ParseUser> query = ParseQuery.getQuery("_User");
List<ParseUser> results = query.find();

Deleting Users

user.delete();

Linking Users

TODO (not yet implemented)

Security (ACLs)

TODO (not yet implemented)


Queries

Basic Queries

ParseQuery<ParseObject> query = ParseQuery.getQuery("className");
List<ParseObject> results = query.find();

Query Constraints

For example, if we wanted to retrieve Sean Plott's scores that were not in cheat mode, we could do:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerName", "Sean Plott").whereEqualTo("cheatMode", false);
List<ParseObject> results = query.find();

For example, to retrieve scores between 1000 and 3000, including the endpoints, we could issue:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereGreaterThanOrEqualTo("score", 1000);
query.whereLessThanOrEqualTo("score", 3000);
List<ParseObject> results = query.find();

To retrieve scores equal to an odd number below 10, we could issue:

List<Integer> allowedScores = Arrays.asList(new Integer[]{1, 3, 5, 7, 9});
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereContainedIn("score", allowedScores);
List<ParseObject> results = query.find();

To retrieve scores not by a given list of players we could issue:

List<String> disallowedPlayers = Arrays.asList(new String[]{"Jonathan Walsh", "Dario Wunsch", "Shawn Simon"});
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereNotContainedIn("playerName", disallowedPlayers );
List<ParseObject> results = query.find()

To retrieve documents with the score set, we could issue:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereExists("score");
List<ParseObject> results = query.find();

To retrieve documents without the score set, we could issue:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereDoesNotExist("score");
List<ParseObject> results = query.find();

If you have a class containing sports teams and you store a user's hometown in the user class, you can issue one query to find the list of users whose hometown teams have winning records. The query would look like:

ParseQuery<ParseObject> subQuery = ParseQuery.getQuery("Team");
subQuery.whereGreaterThan("winPct", 0.5);

ParseQuery<ParseUser> mainQuery = ParseQuery.getQuery("_User");
mainQuery.whereMatchesKeyInQuery("hometown", "city", subQuery);
List<ParseUser> results = mainQuery.find();

You can sort by multiple fields by passing order a comma-separated list. To retrieve documents that are ordered by scores in ascending order and the names in descending order:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.addAscendingOrder("score").addDescendingOrder("name");
List<ParseObject> results = query.find();

You can use the limit and skip parameters for pagination. limit defaults to 100, but anything from 1 to 1000 is a valid limit. Thus, to retrieve 200 objects after skipping the first 400:

ParseQuery<ParseUser> query = ParseQuery.getQuery("GameScore");
query.setLimit(200).setSkip(400);
List<ParseObject> results = query.find();

You can restrict the fields returned by passing keys a comma-separated list. To retrieve documents that contain only the score and playerName fields:

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
Set<String> targetKeys = new HashSet<String>();
targetKeys.add("playerName");
targetKeys.add("score");
query.selectKeys(targetKeys);
List<ParseObject> results = query.find()

Queries on Array Values

For keys with an array type, you can find objects where the key's array value contains 2 by:

ParseQuery<ParseObject> query = ParseQuery.getQuery("RandomObject");
query.whereEqualTo("arrayKey", 2);
List<ParseObject> results = query.find();

You can also use the $all operator to find objects with an array field which contains each of the values 2, 3, and 4 by:

List<Integer> values = Arrays.asList(new Integer[]{2, 3, 4});
ParseQuery<ParseObject> query = ParseQuery.getQuery("RandomObject");
query.whereContainsAll("arrayKey", values);
List<ParseObject> results = query.find();

Relational Queries

For example, if each Comment has a Post object in its post field, you can fetch comments for a particular Post:

ParseQuery<ParseObject> query = ParseQuery.getQuery("Comment");
query.whereEqualTo("post", postObject /* existing ParseObject */);
List<ParseObject> results = query.find();

For example, imagine you have Post class and a Comment class, where each Comment has a pointer to its parent Post. You can find comments on posts with images by doing:

ParseQuery<ParseObject> postWithImageQuery = ParseQuery.getQuery("Post");
postWithImageQuery.whereExists("image");
ParseQuery commentQuery = ParseQuery.getQuery("Comment");
commentQuery.whereMatchesQuery("post", postWithImageQuery);
List<ParseObject> results = commentQuery.find();

You can find comments on posts without images by doing:

ParseQuery<ParseObject> postWithImageQuery = ParseQuery.getQuery("Post");
postWithImageQuery.whereExists("image");
ParseQuery commentQuery = ParseQuery.getQuery("Comment");
commentQuery.whereDoesNotMatchQuery("post", postWithImageQuery);
List<ParseObject> results = commentQuery.find();

If the Users that liked a Post were stored in a Relation on the post under the key "likes", you can find the users that liked a particular post by:

ParseQuery query = post.getRelation("likes").getQuery();
List<ParseObject> results = query.find();

// The relation should have been created earlier, e.g., by
ParseRelation<ParseObject> relation = post.getRelation("likes");
relation.add(user1);
relation.add(user2);
post.save();

You can find also find all posts liked by a particular user by:

ParseQuery query = ParseQuery.getQuery("Post");
query.whereEqualTo("like", user1);
results = query.find();

In some situations, you want to return multiple types of related objects in one query. You can do this by passing the field to include in the include parameter. For example, let's say you are retrieving the last ten comments, and you want to retrieve their related posts at the same time:

ParseQuery query = ParseQuery.getQuery("Comment");
query.setLimit(10).include("post").orderByDescending("createdAt");
List<ParseObject> results = query.find();

You can also do multi level includes using dot notation. If you wanted to include the post for a comment and the post's author as well you can do:

ParseQuery query = ParseQuery.getQuery("Comment");
query.setLimit(10).include("post.author").orderByDescending("createdAt");
List<ParseObject> results = query.find();

Counting Objects

ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
int count = query.count();

Compound Queries

If you want to find objects that match one of several queries, you can use $or operator, with a JSONArray as its value. For instance, if you want to find players with either have a lot of wins or a few wins, you can do:

ParseQuery lessThan5Query = ParseQuery.getQuery(classGameScore);
lessThan5Query.whereLessThan("wins", 5);

ParseQuery greaterThan150Query = ParseQuery.getQuery(classGameScore);
greaterThan150Query.whereGreaterThan("wins", 150);

ParseQuery mainQuery = ParseQuery.getOrQuery(Arrays.asList(new ParseQuery[]{lessThan5Query, greaterThan150Query}));
List<ParseObject> results = mainQuery.find();

Files

Uploading Files

Here's a simple example that'll create a file named hello.txt containing a string.

ParseFile textFile = new ParseFile("hello.txt", "Hello World!".getBytes());
textFile.save();

To upload an image (or any other type of file on the filesystem), the syntax is a little bit different. Here's an example that will upload the image myPicture.jpg from the current directory.

byte[] inputBytes = getBytes("myPicture.jpg"); // getBytes is a utility that read raw bytes from the file
ParseFile file = new ParseFile(fileName, inputBytes, MimeType.getMimeType(getFileExtension(fileName)));
file.save();

Associating with Objects

After files are uploaded, you can associate them with Parse objects:

ParseFile file = new ParseFile("profile.png", bytes);
file.save();

ParseObject player= ParseObject.create("PlayerProfile");
player.put("name", "Andrew");
player.put("picture", file);
player.save();

Deleting Files

Deleting files requires the Master Key which is deliberately not exposed via this library due to security considerations. Deleting files can be implemented via a Cloud Function. Here's a possible such function:

/** 
 * Unconditionally deletes the file with the provided filename. This method should be used with care!!!
 * <p><em>Note:</em> It was previously required by ParseInstallation in parse4cn1 but is no longer necessary thanks to this fix: https://github.com/ParsePlatform/parse-server/issues/1718
 * 
 * @filename: The name of the file to be deleted
 * @param: server The Parse Server URL WITHOUT a trailing space.
 */
Parse.Cloud.define("deleteFile", function(request, response) {
  
  var filename = request.params.filename;
  var server = request.params.server;
  
  if (!filename) {
    response.error("Filename is not defined");
  } else if (!server) {
    response.error("Parse server URL (WITHOUT trailing backslash) is required")
  } else {
	  Parse.Cloud.httpRequest({
		method: 'DELETE',
		url: server + '/files/' + filename,
		headers: {
		  'X-Parse-Application-Id': Parse.applicationId,
          'X-Parse-Master-Key': Parse.masterKey
		},
		success: function(httpResponse) {
		  response.success(httpResponse.text);
		},
		error: function(httpResponse) {
		  response.error("Request failed: " + httpResponse.text);
		}
	  });
  }
});

GeoPoints

GeoPoint

A GeoPoint can be created and associated with a ParseObject as follows:

ParseObject obj = ParseObject.create("PlaceObject");
obj.put("location", new ParseGeoPoint(40, -30));
obj.save();

// Retrieval 
ParseGeoPoint geoPoint = obj.getParseGeoPoint("location");

Geo Queries

Queries based on location can be performed using the whereNear() clause as illustrated below:

final ParseGeoPoint refPoint = new ParseGeoPoint(30, -20);
ParseQuery query = ParseQuery.getQuery("PlaceObject");
query.whereNear("location", refPoint);
        
// Results will be ordered by distance from refPoint
List<ParseObject> results = query.find();

To limit the search to a maximum distance add a whereWithin*() constraint:

// Limit radius
final double maxDistanceInMi = 10;
final ParseGeoPoint refPoint = new ParseGeoPoint(30, -20);

ParseQuery query = ParseQuery.getQuery("PlaceObject");
query.whereWithinMiles(fieldLocation, refPoint, maxDistanceInMi);
        
// Results will be ordered by distance from refPoint
List<ParseObject> results = query.find();

It's also possible to query for the set of objects that are contained within a particular area using a whereWithinGeoBox() constraint:

final ParseGeoPoint northEast = new ParseGeoPoint(70, -10);
final ParseGeoPoint southWest = new ParseGeoPoint(0, -40);
query = ParseQuery.getQuery("PizzaPlaceObject");
query.whereWithinGeoBox(fieldLocation, southWest, northEast);
List<ParseObject> results = query.find();

Config

Parse Config is a way to configure your applications remotely by storing a single configuration object on Parse. Parse config values can be retrieved as illustrated below.

// Retrieve config
ParseConfig config = ParseConfig.getInstance();

// Retrieve config items
boolean boolVal = config.getBoolean("configSetup");
String strVal = config.getString("welcomeMessage");
int intVal = config.getInt("winningNumber");
Date dateVal = config.getDate("lastUpdate");
Map objectVal = (Map) config.get("data");
List arrayVal = config.getList("betaTestUserIds");
ParseGeoPoint geoPoint = config.getParseGeoPoint("eventLocation");
ParseFile file = config.getParseFile("backgroundImage");

Cloud Code

Cloud Functions

Cloud Functions can be called using the REST API. For example, to call the Cloud Function named hello:

final String helloWorld = ParseCloud.callFunction("hello", null);
assertEqual("Hello world!", helloWorld);

Background Jobs

Triggering background jobs from the REST API requires the Master Key which is not exposed via this library. A possible workaround is to write a wrapper cloud functions that triggers the job. For example, to trigger the job named myJob defined here a wrapper function runMyJob can be used as illustrated below:

// Possible wrapper function to trigger the userMigration cloud job
Parse.Cloud.define("runMyJob", function(request, response) {
  var params = JSON.stringify(request.params);
  console.log('Params: ' + params);
  Parse.Cloud.httpRequest({
	method: 'POST',
	url: 'https://api.my-parse-server.com/1/jobs/myJob',
	headers: {
	  'X-Parse-Application-Id': Parse.applicationId,
	  'X-Parse-Master-Key': Parse.masterKey,
	  'Content-Type': 'application/json'
	},
	body: params,
	success: function(httpResponse) {
	  response.success(httpResponse.text);
	},
	error: function(httpResponse) {
	  response.error("Request failed: " + httpResponse.text);
	}
  });
});

Triggering the job via the wrapper function is then trivial:

final Map<String, String> params = new HashMap<String, String>();
params.put("plan", "Paid");
final String jobTriggerResult = ParseCloud.callFunction("runMyJob", params);
assertEqual("{}", jobTriggerResult, "On successful trigger, result should be empty");

Advanced Usage

The following are examples of functionality that goes beyond what is provided in the REST API, e.g., background operations.

Serializing ParseObjects

CodenameOne supports serialization of objects via the Externalizable interface. It is possible to serialize ParseObjects as well as illustrated below:

final ParseObject gameScore = ParseObject.create("GameScore");
gameScore.put("score", 1337);
gameScore.put("rating", 4.5);
gameScore.put("playerName", "Sean Plott");
gameScore.put("cheatMode", false);
gameScore.save(); // Persist local changes

// ObjectId can be used as serialization key since it is guaranteed to be unique
Storage.getInstance().writeObject(gameScore.getObjectId(), gameScore.asExternalizable());

// Deserialize
ParseObject deserializedObj = ((ExternalizableParseObject) Storage.getInstance().readObject(
     gameScore.getObjectId())).getParseObject();

Bear in mind that ParseObject does not directly implement the Externalizable interface (due to a name conflict on the getObjectId() method). So to serialize, you need to retrieve a serializable version of the ParseObject using the parseObject.asExernalizable() method. Similarly, when deserializing an object, call the getParseObject() on the ExternalizableParseObject retrieved from storage.

Serialization is supported for all data types supported in ParseObjects: ParseFile, embedded ParseObjects, ParseGeoPoint, ParseRelation, etc. See ParseObjectTest for more detailed examples.

Caveat A ParseObject to be serialized must be 'clean', i.e., it should not have any local modifications.

Using parse4cn1 in a Regular Java Project

This library can be used in a regular Java project using the CN1 Java SE port. To do so, follow these steps:

  1. Include dist/parse4cn1.jar and its dependencies (dist/lib/CN1JSON.jar, dist/lib/JavaSE.jar) in your Java project.
  2. Initialize the parse4cn1 in your application as illustrated below:

If your Java application is a non-GUI application, try the following:

Display.init(null);
// <Do something with the parse library here>
Display.getInstance().exitApplication();

If your Java application is a GUI application, it's recommended to use parse4cn1 as follows:

final JFrame f = new MainFrame(); //MainFrame is a simple class that initializes the application's main window
Display.init(f.getContentPane());

Display.getInstance().callSeriallyAndWait(new Runnable() {

    @Override
    public void run() {
        f.setVisible(true); // Set to false if you don't want the GUI to be shown
        // <Do something with the parse library here>
        // Close frame
        f.dispatchEvent(new WindowEvent(f, WindowEvent.WINDOW_CLOSING));
    }
});

Creating Custom ParseObject Sub-classes

In some cases, it might be useful to create custom classes that inherit from ParseObject and handle application-specific logic around the Parse backend. That is as simple as inheriting from ParseObject, implementing the custom logic and/or overriding base class methods. A good example of this approach is the ParseUser class. Take a look at the source code for inspiration.

For example, to enforce that it is impossible to delete the username field, the following is done in ParseUser:

@Override
public void remove(String key) {
    if ("username".equals(key)) {
        LOGGER.error("Can't remove the username key.");
        throw new IllegalArgumentException("Can't remove the username key.");
    }

    super.remove(key);
}

Moreover, a utility method for setting the username with extra validation can be easily defined as follows:

/**
 * Sets the username. Usernames cannot be null or blank.
 * @param username The username to be set.
 */
public void setUsername(String username) {
    if (username == null || username.trim().length() == 0) {
        throw new IllegalArgumentException("Username cannot be null or blank");
    }
    put(KEY_USERNAME, username);
}

Registering Custom Sub-Classes

Note that some operations require instantiating of the custom ParseObject class at runtime. Since CN1 does not support Java reflection, the custom class needs to be manually registered (e.g., right after initializing the parse4cn1 library) as explained in this section.

// Dummy custom class for illustration purposes
public class CustomParseObject extends ParseObject {

    public static final String CLASS_NAME = "CustomParseObject";
    public static final String CUSTOM_FIELD_NAME = "customField";

    private CustomParseObject() {
        super(CLASS_NAME);
        put(CUSTOM_FIELD_NAME, "ReadOnly");
    }

    public String getCustomField() {
        return getString(CUSTOM_FIELD_NAME);
    }

    // Suppose we want to enforce that a field's value cannot be changed,
    // we can achieve that by overriding the put() method as follows:
    @Override
    public void put(String key, Object value) {       
        if (CUSTOM_FIELD_NAME.equals(key) && !"ReadOnly".equals(value)) {
            throw new IllegalArgumentException("Field " + CUSTOM_FIELD_NAME
                    + " must have value 'ReadOnly'");
        }
        super.put(key, value);
    }
}

Suppose you have a class CustomParseObject that extends from ParseObject, you will get an error similar to the following if you try to call ParseObject.create(), ParseObject.fetch() or any other methods that implicitly require instantiation of an object of the custom class: java.lang.ClassCastException: com.parse4cn1.ParseObject cannot be cast to com.parse4cn1.CustomParseObject

To solve this problem, you must first register the custom class as follows:

ParseRegistry.registerSubclass(CustomParseObject.class, CustomParseObject.CLASS_NAME);
ParseRegistry.registerParseFactory(CustomParseObject.CLASS_NAME, new Parse.IParseObjectFactory() {

    @Override
    public <T extends ParseObject> T create(String className) {
        if (CustomParseObject.CLASS_NAME.equals(className)) {
            return (T) new CustomParseObject();
        }
        throw new IllegalArgumentException("Unsupported class name: " + className);
    }
});

Push Notifications

Push Notifications are a great way to keep your users engaged and informed about your app. In order to use push notifications in Parse4CN1, you first need to set up your app for push as explained here.

Installations

An installation object represents an instance of your app on a specific device. It is integral in the Parse push notification service.

Creating and Retrieving an Installation

As explained in the parse4cn1 push notification overview, creating of installations is delegated to the Parse native SDKs that are integrated in parse4cn1.

On Android and iOS, an installation is automatically created if one does not already exist when the following method is called. As at January 2017, push notification for Windows Phone was not yet supported in Parse Server.

// On Android and iOS, this automatically creates a new installation 
// if one does not already exists and saves it to the Parse backend 
ParseInstallation currentInstallation = ParseInstallation.getCurrentInstallation();

Updating an Installation

Most ParseInstallation fields are read-only. Arguably, the only field that is of interest w.r.t. updating an installation is the channels to which it is subscribed. ParseInstallation exposes methods for managing channels as illustrated below:

// Retrieve all currently subscribed channels
List<String> channels = currentInstallation.getSubscribedChannels();

// Subscribe to a channel
currentInstallation.subscribeToChannel(""); //"" is the de-facto broadcast channel

// Subscribe to multiple channels at once 
currentInstallation.subscribeToChannels(Arrays.asList("foo", "bar"));

// Unsubscribe from channel
currentInstallation.unsubscribeFromChannel("");

// Unsubscribe from multiple channels
currentInstallation.unsubscribeFromChannel(Arrays.asList("foo", "bar"));

// Unsubscribe from all channels
currentInstallation.unsubscribeFromAllChannels();
Badging

On iOS, it is possible to set the app badge icon. This can be useful, for example, to update the app badge after a push notification is opened.

On other platforms, the badge can still be set since it's just another field in a ParseInstallation object. However, it will not have the desired effect of app icon badging.

// Set the app badge (useful only on iOS)
currentInstallation.setBadge(5); // Use 0 to clear badge

// Decrement badge e.g. on push open on iOS
currentInstallation.setBadge(Math.max(0, currentInstallation.getBadge() - 1));

Other Operations on a ParseInstallation

A ParseInstallation is simply a sub-class of a ParseObject so it can contain arbitrary user-defined fields provided the names do not conflict with those of pre-defined fields. The pre-defined fields are listed in the REST API documentation.

Moreover, the ParseInstallation class can be queried just like any other ParseObject (sub-)class. See the section on querying for more details. For example, the following code can be used to query for a ParseInstallation with a given installationId:

final ParseQuery<ParseInstallation> query 
    = ParseInstallation.getQuery().whereEqualTo("installationId", installationId);
final List<ParseInstallation> results = query.find();
if (results.size() == 1) {
    return results.get(0);
} else if (results.size() > 1) {
    throw new ParseException(ParseException.PARSE4CN1_MULTIPLE_INSTALLATIONS_FOUND,
        "Found multiple installations with ID "
            + installationId + " (installation IDs must be unique)");
} else {
    throw new ParseException(ParseException.PARSE4CN1_INSTALLATION_NOT_FOUND,
        "Found no installation with ID " + installationId);
}

Since deletion of a ParseInstallation requires the X-Parse-Master-Key header which, by design, is not exposed via parse4cn1 for security reasons, it is impossible to delete a ParseInstallation via parse4cn1. Instead, the Parse dashboard should be used.

Sending Pushes

Push notifications can be sent from the Parse dashboard. This is the recommended approach from a security perspective. However, it is allowed (though not recommended in production apps) to send push notifications from your app. Note that sending of push notifications will work even if the app is not configured for receiving push notifications because sending of push notifications in parse4cn1 is done via the REST API and not via the native Parse SDKs.

Sending Push Messages from App Requires Cloud Code

One of the changes in Parse Server w.r.t. Parse.com is that the master key is now required to send push messages. Since parse4cn1 does not expose the master key, sending push messages from the app requires writing a cloud code function.

parse4cn1's cloud code utility script includes such a cloud code function - sendPushViaRestApi(). As the name implies, this function uses the Push REST API to send push messages (instead of the Javascript SDK which is also available in the cloud code environment). The advantage of this approach is that it gives maximum flexibility in the composition of push messages on the app side and provides a stable API for future extensions to the push payload.

Note that the ParsePush class in parse4cn1 relies on this cloud function to work. Therefore, anyone interested in sending push messages from parse4cn1 must either copy the sample implementation or create their own cloud function with the same parameters. The function takes two parameters - the Parse Server URL and the push payload as per the specification of the HTTP POST payload in the REST API documentation.

When using ParsePush to send push messages, bear in mind that some functionality that is exposed in that class (from the Parse.com days) but is not yet supported in Parse Server (e.g. scheduled pushes as at Jan. 2017) will not work.

Using Channels

The simplest way to send push notifications is using channels. Installations of your app can subscribe to any channels of interest via the ParseInstallation class as illustrated above. Subsequently, they will receive any push messages targeted at that channel. For example, the following code sends a message to all subscribers of the "Test" channel:

final ParsePush parsePush = ParsePush.create();
parsePush.setMessage("Parse + CN1 rocks!!!");
parsePush.setChannel("Test");
parsePush.send();

Using Advanced Targeting

It is also possible to send more targeted push messages using queries. For example, the following push message will be sent only to devices where the user-defined injuryReports field has value true.

final ParsePush parsePush = ParsePush.create();
parsePush.setMessage("Willie Hayes injured by own pop fly.");

// Create a query to target a specific subset of users
final ParseQuery<ParseInstallation> parseQuery = ParseInstallation.getQuery();
parseQuery.whereEqualTo("injuryReports", true);
parsePush.setQuery(parseQuery)
parsePush.send();

Similarly, it's easy to use queries to target specific operating systems. For example, the following one will target only Android and iOS devices in this case with a 'secret' message:

final ParsePush parsePush = ParsePush.create();
parsePush.setContentAvailable(true); // Indicates hidden message for iOS

// Note: The absence of an alert (setMessage()) and a title (setTitle()) is sufficient
// to make a message qualify as a hidden message on Android.
// Hidden messages do not appear to work on Windows Phone as of October 2015
parsePush.setData("secret", "I'm lovin' this thing!");

// Create a query to target only iOS and Android users
final ParseQuery<ParseInstallation> parseQuery = ParseInstallation.getQuery();
parseQuery.whereContainedIn("deviceType", Arrays.asList("ios", "android"));
parsePush.setQuery(parseQuery)
parsePush.send();

Sending Options

Parse allows you to customize your push message with several options. In the previous section, we already illustrated the content-available field (cf. setContentAvailable()) which is an iOS-only field. That example also illustrates the data field which allows you to include arbitrary key-value pairs (bear in mind though that push messages have length limitations!).

The following example illustrates the use of the other customizable push options:

final ParsePush parsePush = ParsePush.create();
parsePush.setMessage("The Mets scored! The game in now tied 1-1.");
parsePush.setBadge("Increment"); // iOS-only; a value may also be used
parsePush.setSound("cheering.caf"); // iOS-only
parsePush.setCategory("aValidCategory"); // iOS-only
parsePush.setTitle("Mets score!"); // Android-only
parsePush.setUri("http://www.example.net"); // Android-only
parsePush.setData("action", "com.example.UPDATE_STATUS");
parsePush.setData("remainingTime", 10); 
parsePush.send();

Scheduling Pushes

It is possible to set an expiration time for a push message:

parsePush = ParsePush.create();
parsePush.setMessage("This message is time-bound");
parsePush.setExpirationTime(time); // time is a java.util.Date object
parsePush.send();

Alternatively, an expiration interval can be set as follows:

parsePush = ParsePush.create();
parsePush.setMessage("This message is time-bound");
parsePush.setExpirationTimeInterval(518400);
parsePush.setPushTime(time); // time is a java.util.Date object
parsePush.send();

Receiving Pushes

Configuring your app to receive push notification goes beyond the scope of the Parse REST API and requires native code. The steps involved in handling incoming push messages in a CN1 app are described in the reference implementation here under "Consistently handling push messages".

Clone this wiki locally