Welcome

Welcome to Backendless, a powerful API services platform. The platform can be used to build and run mobile and web applications. Backendless consists of built-in, general purpose API services and also supports deployment of custom, developer-defined services and functions. The built-in API services support common backend tasks and operations such as user login and logout, password recovery, data persistence, file upload, etc. Developer-defined services and functions can be used for custom server-side business logic. The diagram below illustrates major components of the Backendless architecture:

backendless-architecture

The sections below describe individual service APIs. It is recommended to go through the Setup section first to configure your environment.

Quick Start Guide

This guide will help you develop a basic mobile application up and running with the Backendless 4 backend.

 

Once the product is installed, there are two URLs to keep in mind:

1. Backendless Console URL - available at http://HOSTNAME:8080, where HOSTNAME is the IP address or the hostname confirmed/entered during  the installation. It is not recommended to use localhost if you plan to run your application in a device emulator.
2. API Endpoint URL - this is the URL where the server accepts and processes the API requests. The API endpoint URL is the same as console URL with /api appended at the end. For example, http://HOSTNAME:8080/api. When Backendless Pro is running, you can confirm that the API endpoint is working, by entering it in a browser. You should see the following (or similar) response:
api-endpoint-response

 

Follow the steps below to build your first app with Backendless:

1. Login to Backendless Console. The username and password are what you entered during the installation process. If you cannot remember the login, click the Register link on the Backendless Console login screen and create a new developer account.
 
2. Select or create an app. If you do not have any applications in your account, you will be prompted to create one.
 
3. Click the Download Project Template button:
download-proj-template.zoom50
 
4. Double click the Android icon in the download project template popup to download a project for Android:
android-template
 
5. The downloaded ZIP file contains an Android Studio project. Launch Android Studio and open the project.
 
6. The project contains two Java files: Defaults.java and MainActivity.java. The former class contains your application ID, Android API Key and the API endpoint URL. Below is an example of the class, keep in mind that the values for your application will be different:
package com.examples.consoledemo;

public class Defaults
{
  public static final String APPLICATION_ID = "6F70C24E-6B29-DD1D-FFA9-74BC15FE0900";
  public static final String API_KEY = "D945A1D6-DD28-4122-FFC2-2FC85D37D100";
  public static final String SERVER_URL = "http://localhost:8080/api";   
}
. The latter includes Backendless SDK initialization and sample code which stores an object in the database of your Backendless backend:
package com.examples.consoledemo;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import android.util.Log;
import com.backendless.Backendless;
import com.backendless.async.callback.AsyncCallback;
import com.backendless.exceptions.BackendlessFault;
import java.util.HashMap;
import java.util.Map;

public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Backendless.setUrl( Defaults.SERVER_URL );
        Backendless.initApp( getApplicationContext(), 
                             Defaults.APPLICATION_ID, 
                             Defaults.API_KEY );

        HashMap testObject = new HashMap<>();
        testObject.put( "foo", "bar" );
        Backendless.Data.of( "TestTable" ).save(testObject, 
                                                new AsyncCallback() {
          @Override
          public void handleResponse(Map response) {
            TextView label = new TextView(MainActivity.this);
            label.setText("Object is saved in Backendless. Please check in the console.");
            setContentView(label);
          }

          @Override
          public void handleFault(BackendlessFault fault) {
            Log.e( "MYAPP", "Server reported an error " + fault.getMessage() );
          }
        });

        TextView label = new TextView(this);
        label.setText("Hello world!");

        setContentView(label);
    }
}

 
7. The project is ready to run without any changes. Run the app in an emulator or on a device. When the application runs successfully, it displays a message on the screen informing that an object has been saved in Backendless:
android-app-run
 
8. To verify the object in Backendless, return to Backendless Console, and click the Data icon. The TestTable table shows the persisted object:
saved-object.zoom50

Client-side Setup

Pure Java and Android clients can consume the Backendless services using the class library (JAR) provided in the Backendless SDK for Android/Java. If you downloaded Backendless SDK for Android/Java from our website, make sure to reference backendless.jar located in the /lib folder of the SDK in the project dependencies and the runtime classpath to get access to the API.

Download SDK

The SDK can be downloaded from the Backendless website.

Gradle Setup

If your project uses Gradle, you can add the following configuration to your build.gradle file:

dependencies {
   compile 'com.backendless:backendless:4.0.+'
}

It is not recommended to use the '+' sign in the dependency version number. Make sure to check the current version of the Backendless library (check only 4.x or later) in Maven Central.

SDK from Maven

The backendless client library for Android and Java is available through the Maven repository. Since the version of Backendless deployed to maven changes frequently, make sure to lookup the latest version number from Maven Central. To add a dependency for the Backendless library, add the following to pom.xml:

<dependency>
 <groupId>com.backendless</groupId>
 <artifactId>backendless</artifactId>
 <version>VERSION FROM MAVEN</version>
</dependency>

Android Manifest

Since Backendless APIs communicate with a server, it is necessary to add the android.permission.INTERNET permission to the manifest file. Add the line below to the <manifest> element:

<uses-permission android:name="android.permission.INTERNET" />

Application initialization

Before the Java/Android client uses any of the APIs, the code must initialize the Backendless Application using the following call:

Android apps:

Backendless.initApp( Context, application-id, api-key );

where Context is an instance of android.content.Context. For instance, it may be your Android application main activity.

 

Java programs (command line, etc):

Backendless.initApp( application-id, api-key );

Proguard Configuration

If your Android application uses Proguard, it is necessary to add the following configuration parameters to proguard.cfg:

-dontwarn com.backendless.**
-dontwarn weborb.**
-keep class weborb.** {*;}

If you run your application with minifyEnabled (obfuscation is turned on), it is necessary to add the following statement to proguard.cfg:

-keep class com.backendless.** {*;}

Apps with Google Maps and Geolocation

Android applications using Google Maps, for instance, the Geo service sample app generated by Backendless code generator, require additional configuration in order for Google maps to be displayed. The process of configuring an app to support Google maps consists of the following tasks:

1. Locate your debug keystore file. The file name is debug.keystore, and is created the first time you build your project. By default, it is stored in the same directory as your Android Virtual Device (AVD) files:
OS X and Linux: ~/.android/
Windows Vista and Windows 7: C:\Users\your_user_name\.android\
 
2. If you are using Eclipse with ADT, and you are not sure where your debug keystore is located, you can select Windows > Prefs > Android >Build to check the full path, which you can then paste into a file explorer to locate the directory containing the keystore.
 
3. List the SHA-1 fingerprint.
For Linux or OS X, open a terminal window and enter the following:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android

For Windows Vista and Windows 7, run:

keytool -list -v -keystore "%USERPROFILE%\.android\debug.keystore" -alias androiddebugkey -storepass android -keypass android

 

4. You should see output similar to this:

Alias name: androiddebugkey
Creation date: Jan 01, 2013
Entry type: PrivateKeyEntry
Certificate chain length: 1
Certificate[1]:
Owner: CN=Android Debug, O=Android, C=US
Issuer: CN=Android Debug, O=Android, C=US
Serial number: 4aa9b300
Valid from: Mon Jan 01 08:04:04 UTC 2013 until: Mon Jan 01 18:04:04 PST 2033
Certificate fingerprints:
    MD5:  AE:9F:95:D0:A6:86:89:BC:A8:70:BA:34:FF:6A:AC:F9
    SHA1: BB:0D:AC:74:D3:21:E1:43:07:71:9B:62:90:AF:A1:66:6E:44:5D:75
    Signature algorithm name: SHA1withRSA
    Version: 3

5. The line that begins SHA1 contains the certificate's SHA-1 fingerprint. The fingerprint is the sequence of 20 two-digit hexadecimal numbers separated by colons.
1. In a browser, navigate to the Google APIs Console.
If you haven't used the Google APIs Console before, you're prompted to create a project that you use to track your usage of the Google Maps Android API. Click Create Project; the Console creates a new project called API Project. On the next page, this name appears in the upper left hand corner. To rename or otherwise manage the project, click on its name.
If you're already using the Google APIs Console, you will immediately see a list of your existing projects and the available services. It's still a good idea to use a new project for Google Maps Android API, so select the project name in the upper left hand corner and then click Create.
2. You should see a list of APIs and services in the main window. If you don't, select Services from the left navigation bar.
3. In the list of services displayed in the center of the page, scroll down until you see Google Maps Android API v2. To the right of the entry, click the switch indicator so that it is on.
4. This displays the Google Maps Android API Terms of Service. If you agree to the terms of service, click the checkbox below the terms of service, then click Accept. This returns you to the list of APIs and services.

If your application is registered with the Google Maps Android API v2 service, then you can request an API key. It's possible to register more than one key per project.

1. Navigate to your project in the Google APIs Console.
2. In the Services page, verify that the "Google Maps Android API v2" is enabled.
3. In the left navigation bar, click API Access.
4. In the resulting page, click Create New Android Key....
5. In the resulting dialog, enter the SHA-1 fingerprint, then a semicolon, then your application's package name. For example:

BB:0D:AC:74:D3:21:E1:43:67:71:9B:62:91:AF:A1:66:6E:44:5D:75;com.example.android.mapexample

6. The Google APIs Console responds by displaying Key for Android apps (with certificates) followed by a forty-character API key, for example:

AIzaSyBdVl-cTICSwYKrZ95SuvNw7dbMuDt1KG0

Follow the steps below to include the API key in your application's manifest, contained in the file AndroidManifest.xml. From there, the Maps API reads the key value and passes it to the Google Maps server, which then confirms that you have access to Google Maps data.

1. In AndroidManifest.xml, add the following element as a child of the <application> element, by inserting it just before the closing tag </application>:

<meta-data
   android:name="com.google.android.maps.v2.API_KEY"
   android:value="API_KEY"/>

2. Substitute your API key for API_KEY in the value attribute. This element sets the key com.google.android.maps.v2.API_KEY to the value of your API key, and makes the API key visible to any MapFragment in your application.
3. Save AndroidManifest.xml and re-build your application.

Application ID and API Key

Values for the application-id and api-key headers must be obtained through the Backendless Console:

1. Login to your account and select the application.
2. Click the Manage icon from the vertical icon-menu on the left.
3. The App Settings section is selected by default. The interface contains values for Application ID and API keys for each supported client-side environment.
4. Use the Copy icon to copy the value into the system clipboard.
appsettings-keys.zoom60

Make sure to use the "Android API Key" for the api-key argument.

 

Migration from 3.x to 4.x

Version 4 of Backendless introduces some changes in the client-side API. This section of the developer guide describes the changes which should be made in the client application to migrate it to Backendless version 4.x.

 

The general process of application migration from 3.x to 4.0 is:

1. Create an app in the version 4 of Backendless.
2. Migrate data and files from the 3.x application.
3. Make changes in the API usage in the cloud application (if applicable).

Version Differences

Functional Area

Version 3.x

Version 4.0

Application identification (REST Only)

REST requests must include the application-id and secret-key HTTP headers with the API requests.

Application ID and API key (previously known as secret key) are included into the endpoint URL.

Application initialization API.
This is a mandatory  method call required to initialize a native SDK implementation in a client app. Applies to all SDKs except for REST.

the initApp argument list includes version number.

the initApp argument list does not include version number. See Migrating App Initialization API for additional information.

Data Query.

The API for configuring data query parameters such as sorting, paging and identifying columns of the related objects to return in the response.

Data retrieval API (the "find" operation in the Data service) uses the BackendlessDataQuery

BackendlessDataQuery has been replaced with DataQueryBuilder

 

See Migrating Data Query API for additional details

Retrieval of data collections.
This applies to retrieving data objects, geopoints and listing of files.

Native SDKs
All SDK implementations use the BackendlessCollection class to represent a collection of data from the backend. The class provided a paging mechanism enabling retrieval of additional pages of data.

Native SDK and REST

BackendlessCollection is completely eliminated. Instead the backend returns an array of objects (represented as a native collection in the native SDKs). The totalObjects property is replaced with a separate API (see below)

REST
REST operations returned a JSON structure containing an array of objects (paged) as well as the totalObjects property

Object counting.
This is a mechanism used by the server to calculate:

total number of objects in a table, optionally satisfying a search query.
total number of files in a directory listing, optionally matching a pattern.
total number of geopoints in a category, optionally satisfying search query (search in radius, rectangular area and/or search query).

Object counting is coupled with collection retrieval (see the row above). Total number of data objects/files/geopoints is returned to the client through the totalObjects property in the response object (BackendlessCollection for native SDKs)

Since BackendlessCollection is removed and thus is never returned in the API operations, there is a new API to obtain object/file/geopoint counts.

 

See Migrating Object/File counts API for additional information.

Object relationship management.
This is a mechanism for establishing or breaking down relations between two or more data objects using the API. Once a relationship is established, client application can use data retrieval API to fetch related object(s) for a parent object.

To establish a relationship between a parent object and the related children, client application must save the parent object instance which references the child object(s). Backendless processes the entire object hierarchy, determines which child objects must be saved/updated, persists them and establishes object relationship between the parent and the children. To remove a relationship, parent object instance must be fetched from the server with its related child objects. Client app deleted from the collection any children for which the relationship must be removed and saves back to the server the parent object.

New API is introduced for:

1. establishing a relationship between two or more objects. See the "Set" API.
2. adding related objects to an existing relation. See the "Add" API.
3. removing objects from a relation. See the relation removal API.

 

See Migrating Related Objects API for additional information.

 

Migrating App Data

The process of migrating application data and files from 3.x to 4.0 is:

 

1. Export application from Backendless Cloud. Login to Backendless Console for your Cloud-based backend and select the app. You will see the "Migrate to Backendless 4.0" button:
migrate-button.zoom79
 
2. Click the Migrate to Backendless 4.0 button. You will see the following popup. If you have more than one version in your Backendless Cloud backend, make sure to select the version for which Backendless will prepare a migration archive. The reason a version must be selected is because 4.0 uses the one-version-per-app approach:
migration-popup
 
3. Select a version and click "Create migration archive for 4.0". The first step in replicating your app in 4.0 is creating a "migration archive". Backendless creates two archives, one with data and another with files. The data archive contains the entire backend. It includes, application settings, data table schemas, data objects, users, security roles, all security permissions, business logic, geopoints and geofences.  To initiate creation of the migration archives, click the "Create migration archive for 4.0" button.
 
4. Download archives. Backendless starts preparing the migration archive and will send out an email as soon as the data and file archives are ready. The archives are placed into the /migration-to-4 directory in the File storage of your app. You will see two files with the names matching the following patterns:

<versionName>-data-<timestamp>.zip
<versionName>-files-<timestamp>.zip

 

5. Create an application in the version 4.0 of the Backendless. You can do that by switching to version 4 (see the arrows icon next to the Backendless logo in the developer console) and creating an app there.
6. Upload file archive into 4.0 app. Open Backendless Console for 4.0 and select the app where you will import the data/files. It is recommended to start with a blank/new app to avoid any conflicts during the import. Select the Files section in Backendless console and upload the "files" archive into the root directory. This can be done either by using the Upload File menu or by drag-and-drop of the file into Console. Once the file is uploaded, the console will appear similar to the image below:
uploaded-file-archive.zoom84
 
7. Upload "Data" archive into 4.0 app. Click the Manage icon, then click Import. Click the Browse button located next to "Backendless app" and browse to the "data" archive downloaded from Backendless Cloud. Once the file is selected, click the IMPORT button at the bottom of the screen to initiate the import process:
start-import-v4.zoom56
 
8. Migration is complete. Once Backendless 4.0 finishes processing the data, it will send out an email informing you of the results. You can see the migration progress or its status after it is complete in the migration log available in the Files section of your app:
migration-logv4.zoom90

App Initialization

Support for application versions has been removed in Backendless v4. As an alternative to versions, it is recommended to create multiple copies of the same application and export/import data between the apps. As a result of the version support removal, application initialization in the code does not require version name in the initApp call.

Version 3.x:

// Android applications
Backendless.initApp( Context androidAppContext, 
                     String applicationID, 
                     String secretKey, 
                     String versionName );
                     
// Java applications
Backendless.initApp( String applicationID, 
                     String secretKey, 
                     String versionName );

Version 4.x:

// Android applications
Backendless.initApp( Context androidAppContext, 
                     String applicationID, 
                     String secretKey );
                     
// Java applications
Backendless.initApp( String applicationID, 
                     String secretKey );

Notice that version 4 uses the term "API key" for the concept we used to call "secret key".

Data Query

Data service find operation can accept an argument with additional options instructing the server to sort data, retrieve paged results and return related child objects. The example below demonstrates the usage of the data query argument in versions 3.x and 4.x:

Version 3.x:

IDataStore<Map> contactStorage = Backendless.Data.of( "Contact" );
BackendlessDataQuery dataQuery = new BackendlessDataQuery();

// set where clause
dataQuery.setWhereClause( "age > 30" );

QueryOptions queryOptions = new QueryOptions();
// set related columns
queryOptions.addRelated( "address" );
queryOptions.addRelated( "preferences" );

// request sorting
List<String> sortBy = new ArrayList<String>();
sortBy.add( "name");
queryOptions.setSortBy( sortBy );

// set offset and page size
queryOptions.setPageSize( 20 );
queryOptions.setOffset( 40 );

// set query options into data query
dataQuery.setQueryOptions( queryOptions );

contactStorage.find( dataQuery, new AsyncCallback<BackendlessCollection<Map>>()
{
  @Override
  public void handleResponse( BackendlessCollection<Map> contactObjects )
  {
     Log.i( "MYAPP", "Retrieved " + contactObjects.getCurrentPage().size() + " objects" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
     Log.e( "MYAPP", "Server reported an error " + fault.getMessage() );
  }
});

Version 4.x:

IDataStore<Map> contactStorage = Backendless.Data.of( "Contact" );
DataQueryBuilder queryBuilder = DataQueryBuilder.create();

// set where clause
queryBuilder.setWhereClause( "age > 30" );

// set related columns
queryBuilder.setRelated( "address", "preferences" );

// request sorting
queryBuilder.setSortBy( "name" );

// set offset and page size
queryBuilder.setPageSize( 20 );
queryBuilder.setOffset( 40 );

contactStorage.find( queryBuilder, new AsyncCallback<List>()
{
  @Override
  public void handleResponse( List<Map> contactObjects )
  {
    Log.i( "MYAPP", "Retrieved " + contactObjects.size() + " objects" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "Server reported an error " + fault.getMessage );
  }
} );

 

Additional resources:

Related Objects

Establishing object relationship:

The process of establishing a relationship between multiple objects in Backendless 3.x is by saving the parent object which references its child(ren) in the same object hierarchy/tree. Version 4 changes the mechanism by introducing a dedicated operation which establishes a relationship between the objects. The example below stores two objects in Backendless Data service: Contact and Address and then sets up a relationship between them:

 

The sample code uses synchronous (blocking) API. As a result, it must be executed in a non-UI thread.

Version 3.x:

IDataStore<Map> contactStorage = Backendless.Data.of( "Contact" );
IDataStore<Map> addressStorage = Backendless.Data.of( "Address" );

HashMap joeThePlumber = new HashMap();
joeThePlumber.put( "name", "Joe" );
joeThePlumber.put( "age", 27 );
joeThePlumber.put( "phone", "1-972-5551212" );
joeThePlumber.put( "title", "Plumber" );
joeThePlumber.put( "___class", "Contact" );

HashMap address = new HashMap();
address.put( "street", "123 Main St." );
address.put( "city", "Denver" );
address.put( "state", "Colorado" );
address.put( "___class", "Address" );

Map savedContact = contactStorage.save( joeThePlumber );
Map savedAddress = addressStorage.save( address );

List<Map> addresses = new ArrayList<Map>();
addresses.add( savedAddress );
savedContact.put( "addresses", addresses );

contactStorage.save( savedContact );

Version 4.x:

IDataStore<Map> contactStorage = Backendless.Data.of( "Contact" );
IDataStore<Map> addressStorage = Backendless.Data.of( "Address" );

HashMap joeThePlumber = new HashMap();
joeThePlumber.put( "name", "Joe" );
joeThePlumber.put( "age", 27 );
joeThePlumber.put( "phone", "1-972-5551212" );
joeThePlumber.put( "title", "Plumber" );
joeThePlumber.put( "___class", "Contact" );

HashMap address = new HashMap();
address.put( "street", "123 Main St." );
address.put( "city", "Denver" );
address.put( "state", "Colorado" );
address.put( "___class", "Address" );

Map savedContact = contactStorage.save( joeThePlumber );
Map savedAddress = addressStorage.save( address );

List<Map> addresses = new ArrayList<Map>();
addresses.add( savedAddress );

// for 1:1 relations, use the ":1" notation instead of ":n"
contactStorage.setRelation( savedContact, "addresses:Address:n", addresses );

Deleting/breaking object relationship:

Similar to establishing a relationship between objects, Backendless 3.x relied on the update operation which sends the parent object to the server with an updated list of child objects. Any children removed from the collection on the client-side, are removed on the server as a result of the update call. Version 4 introduces a dedicated method to remove a relationship between objects. The example below removes a relationship between a Contact object and the first related Address object:

Version 3.x:

IDataStore<Map> contactStorage = Backendless.Data.of( "Contact" );
Map parentObject = contactStorage.findById( "XXXX-XXXX-XXXX-XXXXX" );

List<String> relationNames = new ArrayList<String>();
relationNames.add( "addresses" );
contactStorage.loadRelations( parentObject, relationNames );

List<Map> addresses = (List<Map>) parentObject.get( "addresses" );
addresses.remove( 0 );

parentObject.put( "addresses", addresses );
contactStorage.save( parentObject );

Version 4.x:

IDataStore<Map> contactStorage = Backendless.Data.of( "Contact" );

// load parent object
Map parentObject = contactStorage.findById( "XXXX-XXXX-XXXX-XXXXX" );

// create query builder to load related/child objects
LoadRelationsQueryBuilder<Map<String, Object>> relationsQueryBuilder;
relationsQueryBuilder = LoadRelationsQueryBuilder.ofMap();
relationsQueryBuilder.setRelationName( "addresses" );

String parentObjectId = (String) parentObject.get( "objectId" );

// retrieve children for the "addresses" relation
List<Map<String, Object>> addresses = contactStorage.loadRelations( parentObjectId, 
                                                                    relationsQueryBuilder );

// create a list of child objects for which to break the relation
List<Map> relationToBreak = new ArrayList<Map>();
relationToBreak.add( addresses.get( 0 ) );

// delete the relation between the parent object and children for the "addresses" column
contactStorage.deleteRelation( parentObject, "addresses", relationToBreak );

 

Additional resources:

Object/File Counts

Backendless version 4.x introduces new API to calculate total number of objects and files in a remote collection. In the previous version of Backendless, that number was included as a property of the returned collection. See the examples below:

Version 3.x

IDataStore<Map> contactStorage = Backendless.Data.of( "Contact" );

BackendlessDataQuery dataQuery = new BackendlessDataQuery();
dataQuery.setWhereClause( "age > 21" );
contactStorage.find( dataQuery, new AsyncCallback<BackendlessCollection<Map>>()
{
  @Override
  public void handleResponse( BackendlessCollection<Map> foundContacts )
  {
    Log.i( "MYAPP", "Total number of objects matching query - " + foundContacts.getTotalObjects() );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "Server reported an error " + fault.getMessage() );
  }
} );

Version 4.x

IDataStore<Map> contactStorage = Backendless.Data.of( "Contact" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( "age > 21" );
contactStorage.getObjectCount( queryBuilder, new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer objectCount )
  {
    Log.i( "MYAPP", "There are " + objectCount + " objects matching the query" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "Server reported an error - " + fault.getMessage() );
  }
} );

 

Additional resources:

Data/File Paging

Data paging in Backendless 3 is built into the collection returned from the data retrieval operations. Version 4 changes the mechanism by shifting paging operations to the data query class. See the examples below:

Version 3.x

Consider the following data table in Backendless:

sample-data-paging.zoom50
The code for retrieving the data using default page size (10 objects):

final AsyncCallback<BackendlessCollection<Map>> callback = 
    new AsyncCallback<BackendlessCollection<Map>>()
    {
      @Override
      public void handleResponse( BackendlessCollection<Map> restaurantCollection )
      {
        if( restaurantCollection.getCurrentPage().size() != 0 )
        {
          printRestaurants( restaurantCollection.getCurrentPage() );
          restaurantCollection.nextPage( this );
        }
        else
        {
          Log.i( "MYAPP", "Reached the end of collection" );
        }
      }

      @Override
      public void handleFault( BackendlessFault fault )
      {
        Log.e( "MYAPP", "Server reported an error " + fault.getMessage() );
      }
    };

Backendless.Data.of( "Restaurant" ).find( callback );

private static void printRestaurants( List<Map> restaurantsCollection )
{
  Log.i( "MYAPP", "Loaded " + restaurantsCollection.size() +
                  " restaurants in the current page" );

  for( Map restaurant : restaurantsCollection )
    Log.i( "\t" + restaurant.get( "name" ) );
}
The code produces the following output:

Loaded 10 restaurants in the current page
 Artin's Grill
 Istanbul
 Magic Wok
 Olive Garden
 Platia
 Cantina Loredo
 Wendy's
 Chili's
 Kenny's Burgers
 Tony Roma's
Loaded 1 restaurants in the current page
 McDonnald's
Reached the end of collection

Version 4.x

Identical to the screenshot from 3.x above, consider the following data in version 4 of Backendless:

final DataQueryBuilder queryBuilder = DataQueryBuilder.create();

final AsyncCallback<List> callback = new AsyncCallback<List<Map>>()
{
  @Override
  public void handleResponse( List<Map> restaurantCollection )
  {
    if( restaurantCollection.size() != 0 )
    {
      printRestaurants( restaurantCollection );
      queryBuilder.prepareNextPage();
      Backendless.Data.of( "Restaurant" ).find( queryBuilder, this );
    }
    else
    {
      Log.i( "MYAPP", "Reached the end of collection" );
    }
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
     Log.e( "MYAPP", "Server reported an error " + fault.getMessage() );
  }
};

Backendless.Data.of( "Restaurant" ).find( callback );

private static void printRestaurants( List<Map> restaurantsCollection )
{
  Log.i( "MYAPP", "Loaded " + restaurantsCollection.size() +
                  " restaurants in the current page" );

  for( Map restaurant : restaurantsCollection )
    Log.i( "\t" + restaurant.get( "name" ) );
}
The code produces the same output as the code for 3.x

Sync and Async Calls

Most of Backendless APIs for Android and Java are available in synchronous and asynchronous formats. The difference in the method signatures is the asynchronous methods accept the AsyncCallback<T> argument:

package com.backendless.async.callback;

import com.backendless.exceptions.BackendlessFault;

public interface AsyncCallback<T>
{
  void handleResponse( T response );
  void handleFault( BackendlessFault fault );
}

The handleResponse( T response ) method is called when a response for the asynchronous operation is available. If operation results in an error, it is delivered to the handleFault method. See the "Error Handling" section for additional information on how to process errors.

Android applications cannot use synchronous APIs on the main UI thread. The reason for this is the main thread does not allow blocking calls. Since the API requests perform network-based communication, the naturally block. As a result, when using the Backendless APIs make sure to use the asynchronous version of the methods, or create a new thread and use the synchronous API in it:

  new Thread(new Runnable() {
        public void run() {
            // synchronous backendless API call here:
            Backendless.XXXX()
        }
    }).start();

 

Error Handling

When the server reports an error, it is delivered to the client through an instance of the BackendlessFault class. A BackendlessFault object provides access to an error code which uniquely identifies the root cause of the error. In addition to the error code, the fault object may include an error message which provides additional information about the error:

package com.backendless.exceptions;

public class BackendlessFault
{
  // returns the error code
  public String getCode();

  // returns the error message which provides additional information about the error
  public String getMessage();

  public String getDetail();
  public String toString();
}

The asynchronous API calls accept the AsyncCallback argument which receives the fault object through the following method:

public void handleFault( BackendlessFault fault )

 

If a server error occurs during a synchronous/blocking API invocation, it is thrown to the client as a checked exception - BackendlessException:

package com.backendless.exceptions;

public class BackendlessException extends Throwable
{
  // http status code returned by the server or custom code
  public int getHttpStatusCode();

  // Backendless error code
  public String getCode()

  // Error message corresponding to the error code
  public String getMessage()

  // Detailed information (possibly a stack trace) corresponding to the exception
  public String getDetail()
}


Error Codes

Click here for the complete list of Backendless Error Codes

User Service API

Overview

The Backendless User Service provides the functionality related to the user accounts such as user registrations, logins, password recovery and logouts. The core concept which the User Service relies on is the User entity. The structure of the entity is configurable, that is a developer can decide which properties "describe" a user in the context of the application. Typically, properties describing a user are the ones collected during the user registration process.  The User Service provides the API enabling the following functionality for the applications built on top of Backendless:

 

User Registration - Applications use the Backendless' registration API to let the users register and create accounts for subsequent logins. Application developers can restrict access to the server-side resources for specific user accounts or based on roles.
User Login - the API lets the registered users login to establish their identity within the application.
Password Recovery - Backendless supports a complete workflow allowing users to recover lost or forgotten passwords.
User Logout - the API lets the logged in users terminate their session and disassociate their identity from the application.
Updating User Registration - the API supports the operation of updating user information.

Retrieve User Schema

An application can get a list of the properties associated with the user schema by using the following API:

Asynchronous Call Method Signature:

The method call does not block - it returns immediately . The callback argument receives either the response or the fault returned by the Backendless servers.

public void Backendless.UserService.describeUserClass( AsyncCallback<List<UserProperty>> callback );

where:

callback - an object which receives either a return value or an error from the server. The return value from the server is a collection of the UserProperty objects. The class of the callback object must implement the AsyncCallback<List<UserProperty>> interface.

The UserProperty class is defined as:

public class UserProperty
{
  // The method returns true if the property is marked as 'identity'
  public boolean isIdentity();

  // Returns the name of the property
  public String getName();

  // Returns true if the property is required during user registration
  public boolean isRequired();

  // Returns the data type of the property
  public DateTypeEnum getType();
}

Synchronous Call Method Signature:

public List<UserProperty> Backendless.UserService.describeUserClass();

Asynchronous Call Example:

// do not forget to call Backendless.initApp when your app initializes
Backendless.UserService.describeUserClass( new AsyncCallback<List<UserProperty>>()
{
 public void handleResponse( List<UserProperty> properties )
  {
    for( UserProperty userProp : properties )
    {
      Log.i( "MYAPP", "Property name - " + userProp.getName();
      Log.i( "MYAPP", "\trequired - " + userProp.isRequired();
      Log.i( "MYAPP", "\tidentity - " + userProp.isIdentity();
      Log.i( "MYAPP", "\tdata type - " + userProp.getType();
    }
  }

  public void handleFault( BackendlessFault fault )
  {
  }
});

Synchronous Call Example:

// do not forget to call Backendless.initApp when your app initializes
List<UserProperty> properties = Backendless.UserService.describeUserClass();

for( UserProperty userProp : properties )
{
  Log.i( "MYAPP", "Property name - " + userProp.getName();
  Log.i( "MYAPP", "\trequired - " + userProp.isRequired();
  Log.i( "MYAPP", "\tidentity - " + userProp.isIdentity();
  Log.i( "MYAPP", "\tdata type - " + userProp.getType();
}

 

User Registration

The user registration API can be used to create user accounts in the application. A registration request must provide a user object as a collection of key/value properties. The collection must contain all the required properties which must include a property marked as identity as well as the "password" property. The default identity property is "email", however, it can be changed in Backendless Console. Additionally, the "email" property is required if the application is configured to confirm email addresses for the registered users.

Asynchronous Call Method Signature:

The method call does not block - it returns immediately. The AsyncCallback argument receives either the response or the fault returned by the Backendless servers.

Backendless.UserService.register( user, new AsyncCallback<BackendlessUser>() );

where:

user - an instance of the BackendlessUser class which contains property values for the account registration.
callback - an object which receives either a return value or an error from the server. The return value from the server is an instance of the BackendlessUser class with the ID assigned by the server-side. The class of the callback object must implement the AsyncCallback<BackendlessUser> interface.

Synchronous Call Method Signature:

public BackendlessUser Backendless.UserService.register( BackendlessUser userObj ) 
                                                  throws BackendlessException;

Error Codes:

The following errors may occur during User Registration API calls. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Invalid application info (application id or api key)

3009

User registration is disabled for the application

3010

User registration has an unknown property and dynamic properties are disabled for this version of the application

3011

Missing "password" property

3012

Required property is missing

3013

Missing value for the identity property

3014

External registration failed with an error.

3021

General user registration error. Details included with the error message.

3033

User with the same identity already exists

3038

Missing application-id or collection of properties for the registering user

3039

Property "id" cannot be used in the registration call

3040

Email address is in the wrong format

3041

A value for a required property is missing

3043

Duplicate properties in the registration request

8000

Property value exceeds the length limit

Asynchronous Method Sample:

// do not forget to call Backendless.initApp when your app initializes

BackendlessUser user = new BackendlessUser();
user.setProperty( "email", "james.bond@mi6.co.uk" );
user.setPassword( "iAmWatchingU" );

Backendless.UserService.register( user, new AsyncCallback<BackendlessUser>()
{
  public void handleResponse( BackendlessUser registeredUser )
  {
    // user has been registered and now can login
  }
  
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
} );

Synchronous Method Sample:

// do not forget to call Backendless.initApp when your app initializes

BackendlessUser user = new BackendlessUser();
user.setProperty( "email", "james.bond@mi6.co.uk" );
user.setPassword( "iAmWatchingU" );

try
{
  user = Backendless.UserService.register( user );
}
catch( BackendlessException exception )
{
  BackendlessFault fault = exception.getFault();
  // an error has occurred, the error code can be retrieved with fault.getCode()
}

Turning Registration Off

User registration can be disabled for a particular version of the application using the Backendless Console:

1. Login to the console and select the application.
2. Click the Users icon in the vertical icon menu on the left.
3. Click User Registration.

The Registration toggle turns the registration API on or off. When the registration is turned off and a user attempts to register, the system returns error 3009.

registration-togglev4.zoom70

Email Confirmations

Backendless can send out an email requesting new registered users to confirm their email address. This feature can be configured in the Backendless Console:

1. Log into the console and select the application.
2. Click the Users icon in the vertical icon menu on the left.
3. Click User Registration.

email-confirmationv4.zoom70

When email confirmations are turned on (the feature is disabled by default), the "email" user property is required and must contain a value formatted as an email address. To configure the text of the email message, select Email Templates from the Users menu in the console and select the User made registration event.

Login

Registered users can login to establish their identity with the application using the API below. The login operation requires two properties: one marked as user identity and the second is password. Backendless automatically assigns the "AuthenticatedUser" role to all successfully logged in users. The role can be used to differentiate access to various resources (persistent objects, messaging channels, files) between authenticated users and guests.

Asynchronous Methods:

The method call does not block - it returns immediately. The AsyncCallback argument receives either the response or the fault returned by the Backendless servers.

public void Backendless.UserService.login( String login, 
                                           String password, 
                                           AsyncCallback<BackendlessUser> callback );

public void Backendless.UserService.login( String login, 
                                           String password,   
                                           AsyncCallback<BackendlessUser> callback,
                                           boolean stayLoggedIn, );

where:

login - a value for the property marked as identity.
password - user's password
callback - an object which receives either a return value or an error from the server. The class must implement the AsyncCallback<BackendlessUser> interface.
stayLoggedIn - requests to store the user's login information so the login form can be skipped next time the user launches the app. Use the following API to check if the application has user login information from previous runs:

// UserTokenStorageFactory is available in the com.backendless.persistence.local package

String userToken = UserTokenStorageFactory.instance().getStorage().get();

if( userToken != null && !userToken.equals( "" ) )
{  // user login is available, skip the login activity/login form }

Synchronous Method:

public BackendlessUser Backendless.UserService.login( String login, 
                                                      String password, 
                                                      boolean stayLoggedIn ) 
                                                      throws BackendlessException;

where:

login - a value for the property marked as identity.
password - user's password
stayLoggedIn - requests to store the user's login information so the login form can be skipped next time the user launches the app. Use the following API to check if the application has user login information from previous runs:

// UserTokenStorageFactory is available in the com.backendless.persistence.local package

String userToken = UserTokenStorageFactory.instance().getStorage().get();

if( userToken != null && !userToken.equals( "" ) )
{  // user login is available, skip the login activity/login form }

Errors:

The following errors may occur during the Login API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

3000

Login has been disabled for the user account.

3001

Missing login settings, possibly invalid application id or version.

3002

User cannot login because Multiple Logins disabled and there is a logged in user for the account.

3003

Invalid login or password.

3006

Either login or password is an empty string value.

3034

User logins are disabled for the version of the application.

3036

Account locked out due to too many failed logins.

3038

One of the required parameters (application id, version, login or password) is null

3044

Multiple login limit for the same user account has been reached.

8000

Property value exceeds the length limit

 

Asynchronous Method Sample:

// do not forget to call Backendless.initApp in the app initialization code

Backendless.UserService.login( username, password, new AsyncCallback<BackendlessUser>()
{
 public void handleResponse( BackendlessUser user )
  {
    // user has been logged in
  }

  public void handleFault( BackendlessFault fault )
  {
    // login failed, to get the error code call fault.getCode()
  }
});

Synchronous Method Sample:

// do not forget to call Backendless.initApp in the app initialization code

BackendlessUser user;

try
{
  user = Backendless.UserService.login( username, password );
}
catch( BackendlessException exception )
{
  // login failed, to get the error code, call exception.getFault().getCode()
}

Validating User Login

The login operation provides a way to persist the user-token on the client side so it can be used when the application is restarted. This helps in streamlining the user experience since the user of the application does not need to login again. However, when the application restarts, it needs to check if the underlying user token, and hence the user session are still valid. This can be accomplished with the API below:

Non-blocking Method:

public void Backendless.UserService.isValidLogin( AsyncCallback<Boolean> callback );

Blocking Method:

public boolean Backendless.UserService.isValidLogin();

If user token is valid, objectId of the logged in user can be retrieved with the following call:

string currentUserObjectId = UserIdStorageFactory.instance().getStorage().get()

Subsequently the BackendessUser instance can be obtained with the following API:

Non-blocking call:

Backendless.Data.of( BackendlessUser.class ).findById( 
                            currentUserObjectId, 
                            AsyncCallback<BackendlessUser> callback )

Blocking call:

Backendless.Data.of( BackendlessUser.class ).findById( currentUserObjectId )

Example:

Log in a user first. Make sure the stayLoggedIn argument is true. The value of true persists the information about the login for the use by subsequent starts/sessions of the application:

Backendless.UserService.login( "batman@backendless.com", "superm@n", true );

Then, check whether the login is valid - see the example below:

Blocking example:

boolean isValidLogin = Backendless.UserService.isValidLogin();
Log.i( "MYAPP", "[SYNC] Is login valid? - " + isValidLogin );

Non-blocking example:

AsyncCallback<Boolean> isValidLoginCallback = new AsyncCallback<Boolean>()
{
  @Override
  public void handleResponse( Boolean response )
  {
    Log.i( "MYAPP", "[ASYNC] Is login valid? - " + response );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.i( "MYAPP", "Error - " + fault );
  }

};

Backendless.UserService.isValidLogin( isValidLoginCallback );

 

Login with Facebook

Backendless integrates with Facebook to support user authentication and login into a Backendless application with a Facebook account. Using the integration application developer can provide a way to skip the application registration step and allow users to use their Facebook identity to register with and experience the application.

 

In order to integrate Facebook login with Backendless, client application must use the Facebook SDK to authenticate the user and then makes a Backendless API call to link the Facebook identity to a BackendlessUser instance.

 

Property Mapping

Backendless User objects may have their own set of user properties. As a result, when a user logs in using their Facebook account, their Facebook Graph API user fields are mapped to the Backendless user properties via "property mapping". There may be multiple mapping, each must reference  a Facebook user field and a name of a Backendless property. Once a user is authenticated, Backendless obtains the field values from the Facebook user graph object and populates the mapped properties in the BackendlessUser object. For the very first login, Backendless also creates the user in its internal system.

Backendless Configuration

A Backendless application (the server side) must be configured with Facebook's application App ID and secret key. See the "Social Settings" chapter of the user guide (the Manage section) for detailed instructions for configuring Backendless with Facebook.

Integration API

Login with Twitter

Twitter Login

To enable Twitter account log in to your Backendless application:

1. Create your application at https://apps.twitter.com/.
2. In the application's Settings tab, set the Website and Callback URL to https://api.backendless.com
3. From the Keys and Access Tokens tab, copy the Consumer Key and the Consumer Secret values.
4. Click the Permissions tab and select the Request email addresses from users checkbox. Click Update Settings.
5. Log in to your account in the Backendless Console, select your app and click Manage.
6. Scroll down to the Social Settings section and insert the values copied on step 4 into the Twitter Consumer key and Twitter Consumer secret fields.

 

Use following code to allow the users to log in to your application with their Twitter account:

Backendless.UserService.loginWithTwitter( MainActivity.this, new AsyncCallback<BackendlessUser>() 
{ 
  @Override 
  public void handleResponse( BackendlessUser loggedInUser ) 
  { 
    // user logged in successfully
  } 

  @Override 
  public void handleFault( BackendlessFault fault ) 
  { 
    // failed to log in
  } 
} );

Add the fields mapping with Twitter to allow the users to log in to your application with their Twitter account:

Map<String, String> twitterFieldsMappings = new HashMap<String, String>();
twitterFieldsMappings.put( "name", "twitter_name" );

Backendless.UserService.loginWithTwitter( this, 
                                          twitterFieldsMappings, 
                                          new AsyncCallback<BackendlessUser>()
{
  @Override
  public void handleResponse( BackendlessUser backendlessUser )
  {
    // user logged in successfully
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
    // failed to log in
  }
} );

 

 

Login with Google

This guide describes the process of configuring your Backendless backend to enable the Google account sign-in in your application.

 

Adding Google+ API and generating client ID

2. Choose an existing project or create a new one.
newproject-google-signin
3. Select the Google+ API:
select-google-plus
4. Click Enable to enable the API:
click-to-enable
5. Once the API is enabled, click the Go to credentials button:
goto-credentials
6. Select Android in the Where will you be calling the API from? drop-down list.
Also select User data in the What data will you be accessing? section as shown below:
credentials
7. Click the What credentials do I need? button.

 

8. Copy the command starting with keytool... using the copy icon:
copy-keytool-command
9. Open a command prompt/terminal window. Run the keytool command, but make sure to set the value for the -keystore argument. The value must be the proper path to the debug or the production keystore. For example, for the Android apps, the default location of the keystore is ~/.android/debug.keystore, the default password is android.
10. The output of the keytool command includes the SHA-1 certificate fingerprint. Copy its value and paste into the field on the Add credentials to your project screen located right below the keytool command box:
copy-sha1
11. Enter the package name which you will use in your application and click Create client ID.
12. Once the client ID is generated, enter the name of the product on the final screen and click Continue.
product-name
13. Click Done on the final screen.

 

Generating config file for client project

2. Click the Pick a platform button and make a platform (Android or iOS) selection.
3. Select the app from the dropdown, enter package name (Android) or bundle ID (iOS).
4. Click Choose and configure services.
5. Click Google Sign-in, then click Enable Google Sign-In.
6. Click Continue to Generate configuration files.
7. Click Download google-services.json.

 

Configuring Backend

2. Select the project and click Credentials.
3. The credentials should now include a "Web client" which is automatically generated by Google.
webclient
4. Click Web client.
5. Copy Client ID and Client secret into Google Plus ID and Google Plus Secret located in the Manage > App Settings > Social Settings in Backendless console:
client-id
 
google-client-id-v4

 

Developing Client App

1. Add google-services.json generated in the Generating config file for the client project section to the root of the "app" module in the Android project.
2. Add the following code in the onCreate method.
@Override
  public void onCreate( Bundle savedInstanceState )
  {
    super.onCreate( savedInstanceState );

    FragmentActivity fragmentActivity = (FragmentActivity)this.getActivity();

    gSignInOptions = new GoogleSignInOptions.Builder( GoogleSignInOptions.DEFAULT_SIGN_IN )
            .requestEmail().requestProfile().requestId().requestIdToken( SERVER_CLIENT_ID )
            .build();

    // Build a GoogleApiClient with access to GoogleSignIn.API and the options above.
    GoogleApiClient.Builder apiCliBuilder = new GoogleApiClient.Builder( fragmentActivity );
    gApiClient = apiCliBuilder
            .enableAutoManage( fragmentActivity, this )
            .addApi( Auth.GOOGLE_SIGN_IN_API, gSignInOptions ).build();
  }

3. The SERVER_CLIENT_ID variable in the code above must be the value of the Web Client ID from the Credentials screen:
web-client-id
4. To start the sign in process, use the following code, where SIGN_IN_INTENT_ID is an integer identifying the intent (it will be used later in the code):
public int SIGN_IN_INTENT_ID = 1;
Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent( gApiClient );
this.startActivityForResult( signInIntent, SIGN_IN_INTENT_ID );
5. The intent displays a screen where the user selects her account and optionally enters the credentials. Once the activity result is available, it is delivered to the following method. The implementation of the method further delegates the sign in process:
@Override
  public void onActivityResult( int requestCode, int resultCode, Intent data )
  {
    super.onActivityResult(requestCode, resultCode, data);

    // Result returned from launching the Intent from GoogleSignInApi.getSignInIntent(...);
    if( requestCode == SIGN_IN_INTENT_ID) 
    {
      GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);

      if (result.isSuccess()) 
      {
        refreshUIonLogin( this.getActivity(), result.getSignInAccount() );
        loginInBackendless( result.getSignInAccount() );
      } 
      else
      {
        logined = false;
        refreshUIonLogout();
        // Signed out, show unauthenticated UI.
      }
    }
  }

  private void loginInBackendless(final GoogleSignInAccount acct)
  {
    Log.d( TAG, "handleSignInResult: try login to backendless" );

    final MainActivity mainActivity = (MainActivity)this.getActivity();
    final String accountName = acct.getEmail();
    final String scopes = "oauth2:" + Scopes.PLUS_LOGIN + " " + 
                          Scopes.PLUS_ME + " " + Scopes.PROFILE + " " + 
                          Scopes.EMAIL;

    AsyncTask<Void, Void, String> task = new AsyncTask<Void, Void, String>(){
      @Override
      protected String doInBackground( Void... params )
      {
        String token = null;
        try
        {
          token = GoogleAuthUtil.getToken( mainActivity, accountName, scopes );
          GoogleAuthUtil.invalidateToken( mainActivity, token );
          handleAccessTokenInBackendless( acct.getIdToken(), token );
        }
        catch( UserRecoverableAuthException e )
        {
          startActivityForResult( e.getIntent(), REQUEST_AUTHORIZATION );
        }
        catch( Exception e )
        {
          Log.e( TAG, e );
        }
        return token;
      }
    };

    task.execute( );
  }



private void handleAccessTokenInBackendless( String idToken, String accessToken )
  {
    Log.d( TAG, "idToken: "+idToken+ ", accessToken: "+accessToken );

    Map<String, String> googlePlusFieldsMapping = new HashMap<String, String>();
    googlePlusFieldsMapping.put( "given_name", "gp_given_name" );
    googlePlusFieldsMapping.put( "family_name", "gp_family_name" );
    googlePlusFieldsMapping.put( "gender", "gender" );
    googlePlusFieldsMapping.put("email", "email");
    List<String> permissions = new ArrayList<String>();

    if (idToken != null && accessToken != null)
    Backendless.UserService.loginWithGooglePlusSdk( idToken, 
                                                    accessToken, 
                                                    googlePlusFieldsMapping, 
                                                    permissions, 
                                                    new AsyncCallback<BackendlessUser>()
    {
      @Override
      public void handleResponse( BackendlessUser backendlessUser )
      {
        Log.i( TAG, "Logged in to backendless, user id is: " + backendlessUser.getObjectId() );
        refreshUIonLoginBackendless(LoginFragment.this.getActivity(), backendlessUser );
      }

      @Override
      public void handleFault( BackendlessFault backendlessFault )
      {
        Log.e( TAG, "Could not login to backendless: " + 
                    backendlessFault.getMessage() + 
                    " code: " + backendlessFault.getCode() );
      }
    });
  }

 

User Properties

When a user registers with an application, he provides information which establishes user's identity. For one application these properties may include name, phone number and email address, for another it could be user id, age and gender. Backendless User Service allows each application to have a custom set of properties associated with the user entity.

Defining properties with the API

User properties must be defined in the object representing a user. When user registers or user object is updated, Backendless saves the property values in the Users table of Backendless Data service. If a property is not declared on the server-side, Backendless automatically creates a definition for it. This behavior can be turned off/on using the Dynamic Schema Definition configuration setting in the console (select Data > Configuration). The setting is turned on by default:

dynamic-schema-definition.zoom70

 

Set/Update User Property

When setting or updating a user property, the values must be of "primitive" data types (boolean, string, number, date). To assign a non-primitive value, use the Data relation API.

The following API can be used to set or update properties in a user object:

Methods are available in the BackendlessUser class. When registering a new user, the methods below can be used on a new instance of BackendlessUser. When updating an existing user, the BackendlessUser instance representing the user must be retrieved from the server (either via login or the data object retrieval APIs).

// set property value. The property name is the value of the "key" argument
public void setProperty( String key, Object value )

// adds properties to the existing ones
public void putProperties( Map<String, Object> properties )

// removes all existing properties (including the built-in ones such 
// as email and password, and sets the provided properties
public void setProperties( Map<String, Object> properties )e

Retrieve User Property Values

The following API can be used to get a user property value:

// get property value. Return value must be cast to the expected data type.
public Object getProperty( String key )

// get all user object properties
public Map<String, Object> getProperties()

There is a special consideration for a user property containing a collection of related data. For any one-to-many user property, the related data is returned as an array. Before application casts the array object to an array of a specific type, it must check if the size of the array is greater than zero. The following code demonstrates the recommended approach:

// suppose the user object contains a relation user property called "events":
Object[] eventsObjectArray = (Object[]) user.getProperty( "events" );
Event[] eventsArray;

// if array is not empty, it can be cast to an array of specific type
if( eventsObjectArray != null && eventsObjectArray.length > 0 )
  eventsArray = (Event[]) eventsObjectArray;

Example:

The example below describes how to retrieve the phoneNumber user property.

public void loginUserAndGetProperties()
{
  Backendless.UserService.login( "spidey@backendless.com", 
                                 "greeng0blin", 
                                 new AsyncCallback<BackendlessUser>()
  {
    @Override
    public void handleResponse( BackendlessUser loggedUser )
    {
      Toast.makeText( MainActivity.this, 
                      "User has been logged in: " + loggedUser, 
                      Toast.LENGTH_SHORT ).show();

      BackendlessUser user = Backendless.UserService.CurrentUser();
      if( user != null )
      {
        // get user's phone number (i.e. custom property)
        String phoneNumber = (String) user.getProperty( "phoneNumber" );
        Toast.makeText( MainActivity.this, 
                        String.format( phone number: %s", phoneNumber ), 
                        Toast.LENGTH_SHORT ).show();
      }
      else
      {
        Toast.makeText( MainActivity.this, 
                        "User hasn't been logged", 
                        Toast.LENGTH_SHORT ).show();
      }
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
      new AlertDialog.Builder( MainActivity.this ).
        setMessage( "Server reported an error: " + fault ).
        setIcon( android.R.drawable.ic_dialog_alert ).
        setPositiveButton( android.R.string.ok, null ).show();
    }
  } );
}

Defining properties with Console

User property management is available in the schema editor of the Users table. To open the schema editor:

1. Login to Backendless Console and select your application.
2. Click the Data icon, and select the Users table.
3. Click the Schema menu.

There are built-in user properties, such as email, name and password. The email and name properties can be deleted if they do not apply to your app, although it is recommended to keep the email property, since it is used for other purposes (for instance, password recovery). The password property cannot be deleted.

Identity Property

It is required that one of the properties is marked as identity. This is the property Backendless uses for the Login and Restore Password operations. As users register, Backendless ensures the value for the identity property is unique  in the context of a specific version of the application.

Password Property

"password" is a special property. Backendless automatically adds the property when an application is created. The following rules apply to the password property:

Password cannot be removed from the application.
Password cannot be used as identity.
Password is always a required property

users-schema.zoom80

To add a new property, click the New button.

Update User Object

User property values of the logged in users can be updated with an API operation described below. This operation is useful if an application needs to provide to the users the functionality for updating user profiles and registration properties. A user must be logged in in order to update his registration properties.

Asynchronous Method:

The method call does not block - it returns immediately. The AsyncCallback argument receives either the response or the fault returned by the Backendless servers.

public void Backendless.UserService.update( BackendlessUser user, 
                                            AsyncCallback<BackendlessUser> );

where

user - an instance of the BackendlessUser class which contains property values to update the user account with.
callback - an object which receives either a return value or an error from the server. The return value from the server is an updated instance of the BackendlessUser class. The class of the callback object must implement the AsyncCallback<BackendlessUser> interface.

Synchronous Method:

public BackendlessUser Backendless.UserService.update( BackendlessUser user );

where

user - an instance of the BackendlessUser class which contains property values to update the user account with.

Errors:

The following errors may occur during the Update User Properties API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

3018

The property marked as "identity" is being updated and another user already has the specified value which must be unique.

3024

General "update registration" error. Error message should contain additional details.

3028

User is not logged in.

3029

Cannot modify properties of another user. Returned when one user is logged and the call attempts to modify properties of another user.

3030

Unable to locate user account - invalid user id.

3031

A new "dynamic" property is being added, but dynamic property definition is disabled.

3045

Required properties in the provided object do not contain values.

Asynchronous Method:

// do not forget to call Backendless.initApp in the app initialization code

Backendless.UserService.login( username, password, new AsyncCallback<BackendlessUser>()
{
 public void handleResponse( BackendlessUser user )
  {
    // user has been logged in, now user properties can be updated
    user.setProperty( "phoneNumber", "5551212" );
    Backendless.UserService.update( user, new AsyncCallback<BackendlessUser>()    
    {
      public void handleResponse( BackendlessUser user )
      {
        // user has been updated
      }
  
      public void handleFault( BackendlessFault fault )
      {
        // user update failed, to get the error code call fault.getCode()
      }
    });
  }

  public void handleFault( BackendlessFault fault )
  {
    // login failed, to get the error code call fault.getCode()
  }
});

Synchronous Method:

// do not forget to call Backendless.initApp in the app initialization code

BackendlessUser user;

try
{
  user = Backendless.UserService.login( username, password );
}
catch( BackendlessException exception )
{
  // login failed, to get the error code, call exception.getFault().getCode()
}

try
{
  user.setProperty( "phoneNumber", "5551212" );
  user = Backendless.UserService.update( user );
}
catch( BackendlessException exception )
{
  // update failed, to get the error code, call exception.getFault().getCode()
}

 

Get Current User

An Android or a Java application can retrieve an instance of BackendlessUser representing the currently logged in user using the following API call:

public BackendlessUser Backendless.UserService.CurrentUser();

If a user is not logged in, the method returns null.

Logout

The Logout operation terminates user session and disassociates the AuthenticatedUser role from the subsequent requests made by the client application.

Asynchronous Method:

The method call does not block - it returns immediately. The AsyncCallback argument receives either the response or the fault returned by the Backendless servers.

public void Backendless.UserService.logout( AsyncCallback<Void> callback )

where

callback - an object which is notified when the server completes the logout operation or returns an error. The class must implement the AsyncCallback<Void> interface.

Synchronous Method:

public void Backendless.UserService.logout() throws BackendlessException;

Users logged in with Facebook must be logged out using the logOut() method on the Facebook's LoginManager class.

Errors:

The following errors may occur during the Logout API call.See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

3007

Invalid application-id or version.

3023

General error while executing logout. Error details should be available in the message property.

Example:

Asynchronous Method:

// do not forget to call Backendless.initApp in the app initialization code

Backendless.UserService.login( username, password, new AsyncCallback<BackendlessUser>()
{
 public void handleResponse( BackendlessUser user )
  {
    // user has been logged in
    // now, let's logout
    Backendless.UserService.logout( new AsyncCallback<Void>() 
    {
      public void handleResponse( Void response )
      {
        // user has been logged out.
      }

      public void handleFault( BackendlessFault fault )
      {
        // something went wrong and logout failed, to get the error code call fault.getCode()
      }
    }); 
  }

  public void handleFault( BackendlessFault fault )
  {
    // login failed, to get the error code call fault.getCode()
  }
});

Synchronous Method:

// do not forget to call Backendless.initApp in the app initialization code

BackendlessUser user;

try
{
  user = Backendless.UserService.login( username, password );
}
catch( BackendlessException exception )
{
  // login failed, to get the error code, call exception.getFault().getCode()
}

try
{
  // now log out:
  Backendless.UserService.logout();
}
catch( BackendlessException exception )
{
  // logout failed, to get the error code, call exception.getFault().getCode()
}

Password Reset

Backendless supports two kinds of password reset:

1. Reset to system (backend) generated password.
2. Reset to password assigned by the user.

With the first option, an email is sent to the user with a regenerated password assigned to the user account. With the second option, an email is sent with a link to a webpage which contains a form where the user can enter the new password.

You can choose the type of reset for your application using Backendless Console. To change the password reset approach navigate to Users > Email Templates and select either User requests password recovery or User requests password recovery by link. Only one of these options can be active at a time. To make an option active, make sure the Do not send email for the event checkbox is not selected. If you make a change, make sure to click the SAVE button at the bottom of the screen.

 

For example, the following screenshot shows a configuration where the system will send out a link so the user can assign his own password. Notice the red color for the User requests password recovery option - this indicated that the option is disabled for the app:

active-mail-template.zoom80

 

In both cases (system-assigned or user-assigned passwords), the password reset sequence must initiated through the API. When the system assigns a password, it is still possible to update the user password using the Data Service API.

 

For the "password change via a link" option Backendless provides a default HTML form the link in the email points to. The HTML page is located in the File storage of the application where it can be edited and customized for branding purposes. The files are located in the /web/templates/change_password directory:

password-change-template.zoom80

Asynchronous Method:

The method call does not block - it returns immediately. The AsyncCallback argument receives either the response or the fault returned by the Backendless servers.

   
public void Backendless.UserService.restorePassword( String identityValue, 
                                                     AsyncCallback<Void> callback )

where

identityValue - a value for the property marked as identity which uniquely identifies the user within the application.
callback - an object which is notified when the server completes the operation or returns an error. The class must implement the AsyncCallback<Void> interface.

Synchronous Method:

   
public void Backendless.UserService.restorePassword( String identityValue ) 
                                                    throws BackendlessException;

where

identityValue - a value for the property marked as identity which uniquely identifies the user within the application.

Errors:

The following errors may occur during the Password Recovery API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

3020

Unable to find user with the specified login (invalid user identity).

3025

General password recovery error. Additional details should be available in the "message" property of the response.

3038

One of the requirement arguments (application id, version or user identity) is missing.

Asynchronous Method Example:

   
// do not forget to call Backendless.initApp in the app initialization code 

Backendless.UserService.restorePassword( "james.bond", new AsyncCallback<Void>()
{
 public void handleResponse( Void response )
  {
    // Backendless has completed the operation - an email has been sent to the user
  }

  public void handleFault( BackendlessFault fault )
  {
    // password revovery failed, to get the error code call fault.getCode()
  }
});

Synchronous Method Example:

   
// do not forget to call Backendless.initApp in the app initialization code 

try
{
 Backendless.UserService.restorePassword( "james.bond" );
}
catch( BackendlessException exception )
{
  // password recovery failed, to get the error code, call exception.getFault().getCode()
}

 

User to Geo Relations

Backendless Geo Service manages application's geo location data and provides APIs to work with geopoints. Backendless supports integration between user objects managed by User Service and geopoints for the scenarios when a logical connection between the two entity types must exist in an application.

 

Relations between a user object and a geopoint are built the same way as between a data object and a geo point. The User-to-Geo integration is implemented through object relations. The Users table schema may declare a table column with a special data type - "GeoPoint Relationship". As a result, the user objects in the table may contain a reference to one or more geopoints. Backendless provides an API to establish a relationship between a user object and geopoints. Additionally, when a user object is retrieved by using the data retrieval API, any related geopoints can be retrieved by using the same mechanisms used for loading data relations. The user-to-geo relation is bidirectional, it means a geopoint may reference a user object in its metadata. You can learn more about it in the Relations with Data Objects section of the Geolocation service documentation.

 

A relation between a user object and a geopoint (or a collection of) can be established by using either the "code first" or the "schema first" approaches. With the former, the relationship is established through an API call. If the relationship is not declared in the schema, Backendless creates it based on the information in the API. With the "schema first" approach, application developer can declare a user-to-geo property first. In either one of these approaches, once a relationship is declared, user objects and geopoints may be linked together by using the Backendless console as well. For information on how to declare Users to Geo relation columns and how to link User objects with GeoPoints using Backendless console, see the Relations with Geo Objects of the Data Service documentation.

 

To create a relation both the user object and geopoint(s) must already exist in the Backendless database. It is not possible to create a relation with an object which has not been previously saved.

Creating User-to-Geo Relations with the API

Creating a relationship between a user object and a geopoint (or a collection of) uses the same API as setting/adding related objects to a data object. In the case of the user-to-geo relations, the related entity is a geopoint or a collection of geopoints.

The example below demonstrates how to assign coordinates to a user's location, that is link a user object with one or several geopoints. A user object is retrieved for the example purposes (to have an object the example will be working with).

One-to-One Relation

Synchronous call

BackendlessUser user = Backendless.Data.of( BackendlessUser.class ).findFirst();
GeoPoint geoPoint = new GeoPoint( 48.85, 2.35 );
user.setProperty( "location", geoPoint );
Backendless.Data.of( BackendlessUser.class ).save( user );

Asynchronous call:

final AsyncCallback<BackendlessUser> saveUserCallback = new AsyncCallback<BackendlessUser>()
{
  @Override
  public void handleResponse( BackendlessUser user )
  {
    // user object has been updated with geopoint
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
  }
};

AsyncCallback<BackendlessUser> findUserCallback = new AsyncCallback<BackendlessUser>()
{
  @Override
  public void handleResponse( BackendlessUser user )
  {
    GeoPoint geoPoint = new GeoPoint( 48.85, 2.35 );
    user.setProperty( "location", geoPoint );
    Backendless.Data.of( BackendlessUser.class ).save( user, saveUserCallback );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
  }
};

Backendless.Data.of( BackendlessUser.class ).findFirst( findUserCallback );

Backendless Console can be used to verify the new relation. The geopoint will show up as a property in the location column of the user object.

One-to-Many Relation

Synchronous call:

GeoPoint geoPoint1 = new GeoPoint( 48.85, 2.35 );
GeoPoint geoPoint2 = new GeoPoint( 40.40, 3.68 );
List<GeoPoint> locations = new ArrayList<GeoPoint>();
locations.add( geoPoint1 );
locations.add( geoPoint2 );
user.setProperty( "locations", locations );
Backendless.Data.of( BackendlessUser.class ).save( user );

Asynchronous call:

final AsyncCallback<BackendlessUser> saveUserCallback = new AsyncCallback<BackendlessUser>()
{
  @Override
  public void handleResponse( BackendlessUser user )
  {
    // user object has been updated with geopoint
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {

  }
};

AsyncCallback<BackendlessUser> findUserCallback = new AsyncCallback<BackendlessUser>()
{
  @Override
  public void handleResponse( BackendlessUser user )
  {
    GeoPoint geoPoint1 = new GeoPoint( 48.85, 2.35 );
    GeoPoint geoPoint2 = new GeoPoint( 40.40, 3.68 );
    List<GeoPoint> locations = new ArrayList<GeoPoint>();
    locations.add( geoPoint1 );
    locations.add( geoPoint2 );
    user.setProperty( "locations", locations );
    Backendless.Data.of( BackendlessUser.class ).save( user, saveUserCallback );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
  }
};

Backendless.Data.of( BackendlessUser.class ).findFirst( findUserCallback );

Backendless Console can be used to verify the new relation. The geopoint will show up as a property in the locations column of the user object.

Security and User Roles

Application's user accounts can be mapped to roles using the API documented below. A Backendless backend contains some system roles and may include developer-defined roles. There several system roles available in all Backendless backends. These roles are automatically assigned to every user under various conditions. Each role can configured with global, container and asset permissions.

 

Core security roles are assigned based on user authentication status and whether the user logged in via "classic" backendless login or through a social network. These roles are:

Security Role

Assigned to user when an API request is made by:

NotAuthenticatedUser

...a user who is not logged in.

AuthenticatedUser

...a user who is logged in.

SocialUser

...a user who is logged in via a social network (Facebook, Twitter, Google)

FacebookUser

...a user who is logged in via Facebook

GooglePlusUser

...a user who is logged in via Google

TwitterUser

...a user who is logged in via Twitter

 

Additionally the following roles are automatically assigned by Backendless to an API request when a corresponding secret API key is used. By configuring access permissions to these roles an application may allow to custom server-side code (ServerCodeUser role), but completely reject access to the client apps (all other roles).

Security Role

Role is assigned when API uses secret key:

ASUser

ActionScript secret key

AndroidUser

Android secret key

DotNetUser

.NET secret key

IOSUser

iOS secret key

JSUser

JavaScript secret key

RestUser

REST secret key

ServerCodeUser

CodeRunner secret key

 

Backendless assigns multiple security roles for the same API request. For example, an API request made from a JavaScript application where user is not logged will have the following roles assigned: NotAuthenticatedUser and JSUser.

 

When an API call originates from business logic (Java, JS or Codeless), Backendless assigns the ServerCodeUser role.

Business logic is the only exception to the rule for assigning NotAuthenticatedUser and AuthenticatedUser roles. When business logic makes an API call and there is no authenticated user in the context of the call, Backendless assigns only the ServerCodeUser role. Otherwise, if there is an authenticated user, then both ServerCodeUser and AuthenticatedUser roles are assigned to the request.

 

Adding Roles

A developer-defined role can be added using Backendless Console:

1. Login to Console and select an application.
2. Click the Users icon.
3. Click the Security and Permissions section.
4. Click the Add Role button and enter a name for the role.
5. Click OK to save the role.

Role Permissions

A role may have multiple permissions assigned to it. A permission may either allow (GRANT) or reject (DENY) an API operation. Every operation which can be restricted with a permission has a subject. For example, the subject of a Data Service operation is a data record. For the File Service operations, the subject is a file.

All permissions are organized into a hierarchy: The global permissions apply to everything in the system. They either allow (GRANT) or reject (DENY) an API operation without distinguishing the subject. For example, a global permission may reject the Data Service's find operations for the users in the NotAuthenticatedRole. As a result, any not-authenticated user will not be able to retrieve an object from any data table in the application.

The asset container permissions inherit from the global ones and apply to a specific asset container, such as a data table. These permissions are scoped to a specific container type. For example, you may reject the get operation on file directory /privatefiles/ for the users in role X. As a result, any user who belongs to the role X will not be able to retrieve files from that directory.

Finally, the asset permissions inherit from the ones for the corresponding asset container (if they are available) or from the global set. For example, an asset permission may reject access to a specific data object for one role and grant access to the same object for another role.

The diagram below illustrates the permissions inheritance ladder:

permission-inheritance

Once a role is assigned to a user account, any permissions assigned to the role (either granting or denying an API operation) automatically apply to the operations executed by the application on behalf of the user.

Retrieving Available User Roles

This operation returns a list of the roles associated with the user account currently logged in to the Backendless application. If the request is sent without logging in, the system will return only one role - NotAuthenticatedUser.

Asynchronous Method:

The method call does not block - it returns immediately. The AsyncCallback arAssigning a Role to a Usergument receives either the response or the fault returned by the Backendless servers.

Backendless.UserService.getUserRoles( new AsyncCallback<List<String>>()); 

Synchronous Method:

List<String> Backendless.UserService.getUserRoles() throws BackendlessException; 

Asynchronous Method Example:

Before retrieving the user roles, log in as described in the Login section. Then, retrieve the user roles as follows:

//login user 
Backendless.UserService.login( "username@foo.com", 
                               "secretpassword", 
                               new AsyncCallback<BackendlessUser>() 
{ 
  @Override 
  public void handleResponse( BackendlessUser backendlessUser ) 
  { 
    // user has been logged in. Get user roles. 
    Backendless.UserService.getUserRoles( 
                                new AsyncCallback<List<String>>() 
    { 
      @Override 
      public void handleResponse( List<String> userRoles ) 
      { 
          //here we get all user roles - "userRoles". 
      } 

      @Override 
      public void handleFault( BackendlessFault backendlessFault ) 
      { 
         // get user roles, to get the error code call backendlessFault.getCode() 
      } 
    } ); 
  } 

  @Override 
  public void handleFault( BackendlessFault backendlessFault ) 
  { 
    // login failed, to get the error code call backendlessFault.getCode() 
  } 
} );

Assigning a Role to a User:

This operation is available only from Custom Business Logic. Applications must use the "Code Runner API key" in order for the API call to be  accepted on the server. The reason for this restriction is that a malicious use of this API can easily compromise application's security. As a result, this API must be used from a controlled environment.

 

Asynchronous Method:

The method call does not block - it returns immediately. The AsyncCallback argument receives either the response or the fault returned by the Backendless servers.

public void Backendless.UserService.assignRole( String identity, 
                                                String roleName, 
                                                AsyncCallback<Void> callback );

where:

identity - a value for a user property marked as identity.
roleName - the name of the role to assign to the user.
callback - an object which receives either a return value or an error from the server. The class must implement the AsyncCallback<Void> interface.

Synchronous Method:

public void Backendless.UserService.assignRole( String identity, String roleName ) 
                                                        throws BackendlessException;

where:

identity - user identification. A value for the property marked as identity.
roleName - name of the role to assign to the user account.

Unassigning a Role from a User:

This operation is available only from Custom Business Logic. Applications must use the "Code Runner API key" in order for the API call to be  accepted on the server. The reason for this restriction is that a malicious use of this API can easily compromise application's security. As a result, this API must be used from a controlled environment.

Asynchronous Method:

The method call does not block - it returns immediately. The AsyncCallback argument receives either the response or the fault returned by the Backendless servers.

public void Backendless.UserService.unassignRole( String identity, 
                                                  String roleName, 
                                                  AsyncCallback<Void> callback );

where:

identity - user identification. A value for the user property marked as identity.
roleName - name of the role to remove from the user account.
callback - an object which receives either a return value or an error from the server. The class must implement the AsyncCallback<Void> interface.

Synchronous Method:

public void Backendless.UserService.unassignRole( String identity, String roleName ) 
                                                            throws BackendlessException;

where:

identity - user identity. A value for the user property marked as identity.
roleName - name of the role to remove from the user account.

Errors:

The following errors may occur during the API calls described above. See the Error Handling section for details on how to retrieve the error code when the server returns an error.

Error Code

Description

2002

Version is disabled or provided wrong application info (application id or secret key)

2005

Could not find role.

3038

One of the required parameters (user identity or roleName) is null.

3057

Could not find user by id or identity.

3058

Could not assign role to user.

3059

Could not unassign role to user.

Global Permissions

The global service permissions apply to all operations of a Backendless service. For example, global Data Service permissions assigned to a role apply to all API operations without distinguishing the subject of an operation. (A subject is a resource targeted by an API operation - for example a data object in a "save" call).

Global permissions can be viewed and managed in Backendless Console.  After selecting an application, click Users, then Security Roles:

user-rolesv4.zoom70

 

Every Backendless service is represented by a column. Each cell in a column is a group of API operations. For example "Find" under "Data" represents all Find operations of the Data Service. The check mark in a cell indicates that that the operation is globally permitted. Each global permission has two states:

Grant - represented by a green check mark - grants the rights to execute an operation for the users in the selected role.
Deny - represented by a red X - denies the rights to execute an operation.

Asset Container Permissions

Asset Container permissions apply to all subjects within a specific container. Supported containers may be:

Data tables                - a permission assigned at that level applies to all operations on the table. For example, storing, deleting or updating objects in the table and running queries.
Messaging channels        - a permission assigned to a channel applies to all operations on the channel - publishing a message, subscribing to receive messages, etc.
File directories        - a permission assigned to a file directory applies to all file-related operations in the directory - file upload, fetching a file, etc.
Geo categories        - a permission assigned to a geo category applies to all geo point-related operation in the category - adding, deleting or updating a geo point
Media tubes        - a permission assigned to a media tube applies to all operation on media streams in the tube.

To view, assign or modify an asset container permission, use a corresponding screen in the Backendless Console. For example, to restrict access to a data table, switch to the Data view, select a table and click the Permissions menu. The user interface has two views - one is for managing user permissions and the other for roles. To modify permissions for a user account:

1. Click the User Permissions menu.
2. Enter the user name in the search field to locate the user account..
3. Click an icon representing the permission state to modify the permission.

user-permissionsv4.zoom70

Similarly permissions can be assigned or modified for specific roles - use the Role Permissions menu.

Asset Permissions

Asset permissions are the most granular in the Backendless security system as they apply to a specific object. Currently this type of permissions is available only for data objects stored in Backendless Data Service and files stored in Backendless File Service. Permissions assigned to an asset (data objects or files) supersede all other permissions - including global and asset container ones. Similar to the asset container permissions, asset permissions may apply to specific user accounts or roles. This approach becomes particularly useful when different objects/files in the same container (table or directory) should have different access levels. For example, suppose a data table called Orders contains purchase orders entered by different sales people. Every sales person should be able to see only their purchase orders, but the sales manager should see the purchase orders of his subordinates. This can be accomplished by using an asset permission scheme.

Data Service Asset Permissions

Permissions for individual data objects can be managed either with Backendless Console or using the API. To manage object's permissions with console:

1. Login to console and select an application.
2. Click Data, then select the table containing the object for which permissions should be managed.
3. There is a special "lock" icon in the ACL column for every object in the table.
acl-iconv4.zoom80
4. Click the lock icon to manage object's permissions.
5. The permissions user interface includes two menu items: Users Permissions and Roles Permissions. The former is responsible for assigning permissions for the selected object to specific users. The latter does the same but for application roles.

The screenshot below explains various parts of the user interface:

roles-permissions-v4.zoom80

The intersection of each column and a role/user either grants or denies access to the operations represented by the column. The intersections may contain the following visualizations:

ico-checkmark-disable (gray check mark)        - INHERIT. This state represents the permissions for the user/role and the operation are inherited from either asset container or global permissions.
ico-checkmark2 (green check mark) - GRANT. This state represents that the permission is granted for the user/role to perform the corresponding operation.
ico-error (red cross)                - DENY. This state represents that the permission to execute the corresponding operation is denied to the user/role.

 

Data Service API

Overview

The Backendless Data Service is a highly scalable object storage system. The system has the qualities of both a traditional structured/relational (SQL) storage and an unstructured NoSQL database. Objects are stored in tables, which have columns with assigned data types. A table can be created dynamically, when an object is being persisted and there is no table corresponding to the object's type. Similarly, when an object is saved or updated  and contains properties which do not have corresponding columns, Backendless dynamically creates the columns in the table.

 

There are two formats for representing data objects: using custom classes or using dictionaries. With the custom class approach all properties of a class are mapped to data table columns. Instances of class are saved in a data table with the same name as the name of the class. When using dictionaries, objects are structured as key/value pairs where keys identify column names and values their respective values. See the Data Object section for additional details.

 

Data Service supports table relations which means a table may declare a column referencing another table. At the object level it is implemented through an object property which contains a reference to another object (one to one relation) or a collection of objects (one to many relations). When an object is retrieved from the server, its related objects can be retrieved in the same request (single-step retrieval) or later on through a separate request (two-step retrieval).

 

Object search in Backendless can be done using the SQL syntax. A search request is sent to a specific table and a search query identifies a condition the found object must match. As a result, the query must be the format of what goes into the WHERE clause of a traditional SQL query. For more details see the Search with the Where clause section of the guide.

Data Object

Backendless database stores objects in data tables. A persisted object is stored in a language-independent format. For the Android and Java applications, there are two ways to represent data objects on the client side:

 

1. Data objects can be stored/retrieved as instances of java.util.Map. For example, the following code saves an object with properties "name" and "age" in the "Person" table:
HashMap person = new HashMap();
person.put( "name", "Joe" );
person.put( "age", 25 );
Backendless.Data.of( "Person" ).save( person );
. This approach is referenced as "Java Map" further in the documentation.
or
 
2. Data objects can be represented as instances of custom classes mapped to data tables. Object's class does not need to implement any special Backendless interfaces or extend a class from the Backendless SDK. For example, the following code saves an object with properties "name" and "age" in the "Person" table:
public class Person
{
  // using public fields, but you can 
  // use getter/setter methods too
  public String name;
  public int age;
}

Person person = new Person();
person.name = "Joe";
person.age = 25;
Backendless.Data.of( Person.class ).save( person );
This approach is referenced as "Custom Class"  further in the documentation.
There are several requirements imposed on the classes of the objects saved in the Backendless database:
The class must contain the default, public, no-argument constructor.
The class must contain either at least one public field or a pair of getter and setter methods conforming to the Java Bean naming convention.
Optional requirement - Backendless automatically assigns a unique ID to every persisted object. If the application needs to have access to the assigned ID, the class must declare the following field:
public String objectId;
Optional requirement - in addition to objectId, Backendless maintains two other properties for every persisted object - created and updated. The former contains the timestamp when the object was initially created in the Backendless database. The latter is updated every time the object is updated. To get access to these values, the class must declare the following fields:
public Date created;
public Date updated;  

 

Column Name Mapping

When your application uses custom classes to represent data objects stored in the database, Backendless maps the column names in the database to the fields/properties declared in a custom class. For example, consider the following class:

package com.sample;

public class Person
{
  public String objectId;
  public String name;
  public int age;
}
When an instance of the class is saved in the Backendless database, it creates a table with the following schema:

person-table

 

As you can see the names of the columns match the names of the fields in the class. However, sometimes that mapping cannot be observed and a field in the class must have a different name or capitalization than the name of the column. In order to override the default mapping, Backendless supports an annotation which allows to map a property to a column. Consider the following example:

package com.sample;

import weborb.service.MapToProperty;

public class Person
{
  public String objectId;
  @MapToProperty( property = "Name" )
  public String name;
  
  @MapToProperty( property = "Age" )
  public int age;
}

The example demonstrates the mapping of the "name" and "age" fields in the client application to the "Name" and "Age" columns in the database. The mapping is bidirectional, it means it works for both saving objects in and retrieving from the database.

 

Class to Table Mapping

When you use custom classes in your application for data persistence, Backendless maps the database table names to the names of the classes. For example, the following code is automatically mapped to the Person table:

package com.sample;

public class Person
{
  public String objectId;
  public String name;
  public int age;
}

 

The Person table in the Backendless database:

person-table

 

When the name of the class is different than the name of the corresponding table in the database, it is necessary to customize the mapping. This can be achieved with the following API:

Backendless.Data.mapTableToClass( String tableName, Class clazz )

where:

tableName        - name of the table to map to a class.

clazz        - reference to a class to map to the table.

 

The API is not necessary if the class name is the same as the table name. The only exception to this rule is when your application retrieves related objects using the "Custom Class" approach. In this case, if application should establish mappings between client-side classes and the related (children) tables. For example, consider the following schema:

 

Order (parent) table:

parent-order-table

 

OrderItem (child) table:

child-orderitem-table

 

If any related OrderItem objects are retrieved along the parent Order object (via auto-load or single-step relation retrieval), then the application must establish a mapping for the OrderItem table before the data is retrieved.

Backendless.Data.mapTableToClass( "OrderItem", OrderItem.class )

Saving Data Objects

The API to save an object can be used for two separate scenarios: (1) creating new objects in the database and (2) updating existing objects.

If an object has been previously saved, it is updated in the database, otherwise it is saved (created). The save operation checks if the object has objectId assigned by the server. In that case, the object is updated, otherwise it is created in the Backendless data store. In case when there is no table for the persisted object, Backendless creates one and maps table's columns to the object's properties.

The objectId property is automatically assigned to all objects in the database when they are initially saved. See the Data Object section for details on objectId.

Synchronous Method:

public Map Backendless.Data.of( "TABLE-NAME" ).save( Map entity ) throws BackendlessException
public <E> E Backendless.Data.of( E ).save( E entity ) throws BackendlessException

Asynchronous Method:

public void Backendless.Data.of( "TABLE-NAME" ).save( Map entity, AsyncCallback<Map> responder )
public <E> void Backendless.Data.of( E ).save( E entity, AsyncCallback<E> responder )

where:

TABLE-NAME - Name of the table where the object represented by java.util.Map will be saved.
E - Java class of the data object to save.
entity - Java object to persist, must be of type E or java.util.Map (depending on the method used).
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

Return Value:

The synchronous method returns the saved object. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

 

public void saveNewContact()
{
  HashMap contact = new HashMap();
  contact.put( "name", "Jack Daniels" );
  contact.put( "age", 147 );
  contact.put( "phone", "777-777-777" );
  contact.put( "title", "Favorites" );
  
  // save object synchronously
  Map savedContact = Backendless.Persistence.of( "Contact" ).save( contact );

  // save object asynchronously
  Backendless.Persistence.of( "Contact" ).save( contact, new AsyncCallback<Map>() {
      public void handleResponse( Map response )
      {
        // new Contact instance has been saved
      }

      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    });
}
Consider the following class:
package com.sample;

public class Contact
{
  private String objectId;
  private String name;
  private int age;
  private String phone;
  private String title;

  public String getObjectId() {
    return objectId;
  }

  public void setObjectId( String objectId ) {
    this.objectId = objectId;
  }

  public String getName() {
    return name;
  }

  public void setName( String name ) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge( int age ) {
    this.age = age;
  }

  public String getPhone() {
    return phone;
  }

  public void setPhone( String phone ) {
    this.phone = phone;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle( String title ) {
    this.title = title;
  }
}
The following code saves a new instance of the Contact class:
public void saveNewContact()
{
  Contact contact = new Contact();
  contact.setName( "Jack Daniels" );
  contact.setAge( 147 );
  contact.setPhone( "777-777-777" );
  contact.setTitle( "Favorites" );

  // save object synchronously
  Contact savedContact = Backendless.Persistence.save( contact );

  // save object asynchronously
  Backendless.Persistence.save( contact, new AsyncCallback<Contact>() {
      public void handleResponse( Contact response )
      {
        // new Contact instance has been saved
      }

      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    });
}

 

Updating Data Objects

Updating a data object which has been previously stored in the Backendless data store can be done using the same API as for saving the object initially. Once an object has objectId assigned, the call to save the object will update it on the server.

Synchronous Method:

public Map Backendless.Persistence.of( "TABLE-NAME" ).save( Map entity ) throws BackendlessException
public <E> E Backendless.Persistence.of( E ).save( E entity ) throws BackendlessException

Asynchronous Method:

public void Backendless.Persistence.of( "TABLE-NAME" ).save( Map entity, AsyncCallback<E> responder )
public <E> void Backendless.Persistence.of( E ).save( E entity, AsyncCallback<E> responder )

where:

TABLE-NAME - Name of the table where the object represented by java.util.Map will be updated. There must be objectId property in the map. The value of the property identifies the object which will be updated.
E - Java class of the data object to update.
entity - Java object to persist, must be of type E or java.util.Map (depending on the method used).
responder - a responder object which will receive a callback when the method successfully updates the object or if an error occurs. Applies to the asynchronous method only.

Return Value:

The synchronous method returns the updated object. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

Synchronous API:

public void updateContact()
{
    // create new contact object first. Then we will update it.
    HashMap contact = new HashMap();
    contact.put( "name", "Jack Daniels" );
    contact.put( "age", 147 );
    contact.put( "phone", "777-777-777" );
    contact.put( "title", "Favorites" );

    Map savedContact = Backendless.Persistence.of( "Contact" ).save( contact );

    // now update the saved object
    savedContact.put( "title", "Most favorite" );
    savedContact.put( "phone", "666-666-666" );
    Backendless.Persistence.of( "Contact" ).save( savedContact );
 }

Asynchronous API

public void updateContact()
{
    HashMap contact = new HashMap();
    contact.put( "name", "Jack Daniels" );
    contact.put( "age", 147 );
    contact.put( "phone", "777-777-777" );
    contact.put( "title", "Favorites" );
    Backendless.Persistence.save( contact, new AsyncCallback<Map>() {
      public void handleResponse( Map savedContact )
      {
        // New contact object has been saved, now it can be updated       
        savedContact.put( "title", "Most favorite" );
        savedContact.put( "phone", "666-666-666" );

        Backendless.Persistence.save( savedContact, new AsyncCallback<Map>() {
          @Override
          public void handleResponse( Map response )
          {
            // Contact objecthas been updated
          }
          @Override
          public void handleFault( BackendlessFault fault )
          {
            // an error has occurred, the error code can be retrieved with fault.getCode()
          }
        } );
      }
      @Override
      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    });
 }
Consider the following class:
package com.sample;

public class Contact
{
  private String objectId;
  private String name;
  private int age;
  private String phone;
  private String title;

  public String getObjectId() {
    return objectId;
  }

  public void setObjectId( String objectId ) {
    this.objectId = objectId;
  }

  public String getName() {
    return name;
  }

  public void setName( String name ) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge( int age ) {
    this.age = age;
  }

  public String getPhone() {
    return phone;
  }

  public void setPhone( String phone ) {
    this.phone = phone;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle( String title ) {
    this.title = title;
  }
}
The following code saves a new instance of the Contact class and subsequently updates it:

Synchronous API:

public void updateContact()
{
    // Create a contact object first. This way (for the sake of the example)
    // there will be a saved object which will be updated after it is created.
    Contact contact = new Contact();
    contact.setName( "Jack Daniels" );
    contact.setAge( 147 );
    contact.setPhone( "777-777-777" );
    contact.setTitle( "Favorites" );

    Contact savedContact = Backendless.Persistence.save( contact );

    // now update the saved object
    savedContact.setTitle( "Most favorite" );
    savedContact.setPhone( "666-666-666" );
    Backendless.Persistence.save( savedContact );
 }

Asynchronous API

public void updateContact()
{
    // Create a contact object first. This way (for the sake of the example)
    // there will be a saved object which will be updated after it is created.
    Contact contact = new Contact();
    contact.setName( "Jack Daniels" );
    contact.setAge( 147 );
    contact.setPhone( "777-777-777" );
    contact.setTitle( "Favorites" );

    Backendless.Persistence.save( contact, new AsyncCallback<Contact>() {
      public void handleResponse( Contact savedContact )
      {
        savedContact.setTitle( "Most favorite" );
        savedContact.setPhone( "666-666-666" );

        Backendless.Persistence.save( savedContact, new AsyncCallback<Contact>() {
          @Override
          public void handleResponse( Contact response )
          {
            // Contact instance has been updated
          }
          @Override
          public void handleFault( BackendlessFault fault )
          {
            // an error has occurred, the error code can be retrieved with fault.getCode()
          }
        } );
      }
      @Override
      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    });
 }

 

Deleting Data Objects

The API completely removes an object from the persistent store. If the object is successfully delete, the API returns the timestamp of the exact deletion time in milliseconds.

Synchronous Method:

Long result = Backendless.Persistence.of( "TABLE-NAME" ).remove( Map entity )
Long result = Backendless.Persistence.of( E ).remove( E entity );

Asynchronous Method:

public void Backendless.Persistence.of( "TABLE-NAME" ).remove( Map entity, AsyncCallback<Long> responder )
Backendless.Persistence.of( E ).remove( E entity, AsyncCallback<Long> responder );

 

where:

TABLE-NAME - Name of the table where the object represented by java.util.Map will be deleted from. There must be objectId property in the map. The value of the property identifies the object which will be deleted.
E - Java class of the data object to delete.
entity - Java object to delete.
responder - a responder object which will receive a callback when the object is deleted or if an error occurs. Applies to the asynchronous method only.

Return Value:

The synchronous method returns the timestamp when the server-side removed the object from the data store. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

Synchronous API:

public void deleteContact()
{
    // create new contact object first. Then we will delete it.
    HashMap contact = new HashMap();
    contact.put( "name", "Jack Daniels" );
    contact.put( "age", 147 );
    contact.put( "phone", "777-777-777" );
    contact.put( "title", "Favorites" );

    Map savedContact = Backendless.Persistence.of( "Contact" ).save( contact );

    // now delete the saved object
    Backendless.Persistence.of( "Contact" ).remove( savedContact );
 }

Asynchronous API

public void deleteContact()
{
    // create new contact object first. Then we will delete it.
    HashMap contact = new HashMap();
    contact.put( "name", "Jack Daniels" );
    contact.put( "age", 147 );
    contact.put( "phone", "777-777-777" );
    contact.put( "title", "Favorites" );
    Backendless.Persistence.save( contact, new AsyncCallback<Map>() {
      public void handleResponse( Map savedContact )
      {
        // New contact object has been saved, now it can be deleted
        Backendless.Persistence.remove( savedContact, new AsyncCallback<Map>() {
          @Override
          public void handleResponse( Long response )
          {
            // Contact objectdhas been deleted
          }
          @Override
          public void handleFault( BackendlessFault fault )
          {
            // an error has occurred, the error code can be retrieved with fault.getCode()
          }
        } );
      }
      @Override
      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    });
 }
Consider the following class:
package com.sample;

public class Contact
{
  private String objectId;
  private String name;
  private int age;
  private String phone;
  private String title;

  public String getObjectId() {
    return objectId;
  }

  public void setObjectId( String objectId ) {
    this.objectId = objectId;
  }

  public String getName() {
    return name;
  }

  public void setName( String name ) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge( int age ) {
    this.age = age;
  }

  public String getPhone() {
    return phone;
  }

  public void setPhone( String phone ) {
    this.phone = phone;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle( String title ) {
    this.title = title;
  }
}
The following code saves a new instance of the Contact class and subsequently updates it:

Synchronous API:

public void deleteContact()
{
    // make sure to put the initApp call somewhere early on 
    // in the app - main activity is the best place
  
    // save a new object first, so there is something to delete.
    Contact contact = new Contact();
    contact.setName( "Jack Daniels" );
    contact.setAge( 147 );
    contact.setPhone( "777-777-777" );
    contact.setTitle( "Favorites" );
    Contact savedContact = Backendless.Persistence.save( contact );

    // now delete the saved object
    Long result = Backendless.Persistence.of( Contact.class ).remove( savedContact );
 }

Asynchronous API

public void deleteContact()
{
    // put the initApp call somewhere early on in your app, perhaps main activity

    // create a new object, so there is something to delete
    Contact contact = new Contact();
    contact.setName( "Jack Daniels" );
    contact.setAge( 147 );
    contact.setPhone( "777-777-777" );
    contact.setTitle( "Favorites" );

    Backendless.Persistence.save( contact, new AsyncCallback<Contact>() 
    {
      public void handleResponse( Contact savedContact )
      {
        Backendless.Persistence.of( Contact.class ).remove( savedContact, 
                                                            new AsyncCallback<Long>() 
        {
          public void handleResponse( Long response )
          {
            // Contact has been deleted. The response is the 
            // time in milliseconds when the object was deleted
          }
          public void handleFault( BackendlessFault fault )
          {
            // an error has occurred, the error code can be 
            // retrieved with fault.getCode()
          }
        } );
      }
      @Override
      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    });
 }

 

Retrieving Schema Definition

Backendless provides API for data table schema introspection. The API returns information about table's columns and their data types, whether a value for a column is required or if there is a default value.

Asynchronous Method:

Backendless.Persistence.describe( String tableName, AsyncCallback<List<ObjectProperty>> callback ) 

where:

tableName - name of the table to get the schema definition for.
callback - a responder with references to the methods called back upon successful invocation or an error.

Synchronous Method:

public List<ObjectProperty> Backendless.Persistence.describe( String tableName )

where:

tableName - name of the table to get the schema definition for.

Return value:

Method returns a collection of the ObjectProperty instances. The ObjectProperty class includes the following Java bean properties:

autoLoad:boolean - applies only to relations. If true, the property is set to auto-load related data for the data retrieval queries.
customRegex:String - a regular expression assigned to the column as a validator. The validator applies when a new object is saved in the table or an existing one is updated.
defaultValue:Object - a default value assigned to any object saved/updated in the table where the column does not have a value.
isPrimaryKey:boolean - true if the column is or is a part of a primary key.
name:String - contains the name of a property Consider the
relatedTable:String - contains the name of the related table(s)
required:boolean - defines whether a property is optional or required for the requests which save the initial object or update an existing one.
type:String - defines the property type

Example:

Suppose the Person table has the following schema - there are two developer-defined columns, age and name.

person-schema.zoom70

The following code retrieves the schema definition for the Person table:

Backendless.Persistence.describe( "Person", new AsyncCallback<List<ObjectProperty>>() 
   { 
     public void handleResponse( List<ObjectProperty> tableProperties ) 
     {     
       Iterator<ObjectProperty> iterator = tableProperties;
       
       while( iterator.hasMore() )
       {
         ObjectProperty propDef = iterator.next();
         Log.i( "MYAPP", "property name - " + propDef.getName() );
         Log.i( "MYAPP", "\tis property required - " + propDef.isRequired() );
         Log.i( "MYAPP", "\tproperty data type - " + propDef.getType() );
         Log.i( "MYAPP", "\tdefault value - " + propDef.getDefaultValue() );
         Log.i( "MYAPP", "\tis property identity - " + propDef.getPrimaryKey() );
       }
     } 

     public void handleFault( BackendlessFault backendlessFault ) 
     {              
     } 
} ); 
The code produces the following output:

property name - objectId
  is property required - false
  property data type - STRING_ID
  default value - null
  is property primary key - true
property name - age
  is property required - false
  property data type - INT
  default value - null
  is property primary key - false
property name - updated
  is property required - false
  property data type - DATETIME
  default value - null
  is property primary key - false
property name - name
  is property required - false
  property data type - STRING
  default value - null
  is property primary key - false
property name - created
  is property required - false
  property data type - DATETIME
  default value - null
  is property primary key - false
property name - ownerId
  is property required - false
  property data type - STRING
  default value - null
  is property primary key - false

 

Get Object Count

The Object Count API provides a way to obtain the following values from the server:

Number of objects in a table
Number of objects matching query
Number of related objects

 

Asynchronous Method:

// get object count for all objects in the table
public void Backendless.Data.of( "TABLE-NAME" ).getObjectCount( 
                                                AsyncCallback<Integer> callback );

// get object count for all objects in the table which match the query
public void Backendless.Data.of( "TABLE-NAME" ).getObjectCount( 
                                                DataQueryBuilder queryBuilder,
                                                AsyncCallback<Integer> callback );
// get object count for all objects in the table
public void Backendless.Data.of( E ).getObjectCount( AsyncCallback<Integer> callback );

// get object count for all objects in the table which match the query
public void Backendless.Data.of( E ).getObjectCount( DataQueryBuilder queryBuilder,
                                                     AsyncCallback<Integer> callback );

Synchronous Method:

// get object count for all objects in the table
public int Backendless.Data.of( "TABLE-NAME" ).getObjectCount();

// get object count for all objects in the table which match the query
public int Backendless.Data.of( "TABLE-NAME" ).getObjectCount( DataQueryBuilder queryBuilder );
// get object count for all objects in the table
public int Backendless.Data.of( E ).getObjectCount();

// get object count for all objects in the table which match the query
public int Backendless.Data.of( E ).getObjectCount( DataQueryBuilder queryBuilder );

where:

TABLE-NAME - Name of the table where to calculate the object count.
E - Java class of the data object which identifies the table where to calculate the object count.
queryBuilder - Instance of com.backendless.persistence.DataQueryBuilder. When present in the arguments, the object must contain a whereClause query. The query is used by the server to identify a collection of objects.
callback - a callback object which will receive a callback when the method successfully calculates the object count. Applies to the asynchronous method only.

Return Value:

The synchronous methods returns an integer value which is the object count. A asynchronous call receives a callback with the object count value.

Example:

Total object count for a table:

The following sample request retrieves total number of objects in table Order:

Backendless.Data.of( "Order" ).getObjectCount( new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer integer )
  {
    Log.i( "MYAPP", "total objects in the Order table - " + integer );
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
    Log.i( "MYAPP", "error - " + backendlessFault.getMessage() );
  }
} );
Backendless.Data.of( Order.class ).getObjectCount( new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer integer )
  {
    Log.i( "MYAPP", "total objects in the Order table - " + integer );
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
    Log.i( "MYAPP", "error - " + backendlessFault.getMessage() );
  }
} );

Object count for a query:

The following sample request retrieves total number of objects in table Order which satisfy the condition of orderAmount > 100:

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( "orderAmount > 100" );
Backendless.Data.of( "Order" ).getObjectCount( queryBuilder,
                                               new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer integer )
  {
    Log.i( "MYAPP", "found objects " + integer );
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
    Log.i( "MYAPP, "error - " + backendlessFault.getMessage() );
  }
} );
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( "orderAmount > 100" );
Backendless.Data.of( Order.class ).getObjectCount( queryBuilder,
                                                   new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer integer )
  {
    Log.i( "MYAPP", "found objects " + integer );
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
    Log.i( "MYAPP, "error - " + backendlessFault.getMessage() );
  }
} );

Related object count:

The following sample request retrieves total number of related "child" objects for a parent object. The parent table is Person. It contains a relation column called address pointing to the Address table. The query below retrieves a count of related child objects for a parent object with objectID of XXXX-XXXX-XXXX-XXXX. The whereClause query syntax for this scenario is:
 
Person[address].objectId = 'XXXX-XXXX-XXXX-XXXX'

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( "Person[address].objectId = 'XXXX-XXXX-XXXX-XXXX'" );
Backendless.Data.of( "Address" ).getObjectCount( queryBuilder,
                                                 new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer integer )
  {
    Log.i( "MYAPP", "found objects " + integer );
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
    Log.i( "MYAPP, "error - " + backendlessFault.getMessage() );
  }
} );
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( "Person[address].objectId = 'XXXX-XXXX-XXXX-XXXX'" );
Backendless.Data.of( Address.class ).getObjectCount( queryBuilder,
                                                   new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer integer )
  {
    Log.i( "MYAPP", "found objects " + integer );
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
    Log.i( "MYAPP, "error - " + backendlessFault.getMessage() );
  }
} );

 

Basic Object Retrieval

Backendless supports multiple basic search operations. These include finding an object by ID, finding first or last object in the collection or retrieving the entire persisted collection. Each method is available in both synchronous and asynchronous versions:

Retrieving Data Objects

Synchronous Methods:

Retrieve data objects with the default paging setting from a table:
public List<Map> Backendless.Persistence.of( "TABLE-NAME" ).find()
Find first data object from a table. The first data object is the first one saved in the data store:
public Map Backendless.Persistence.of( "TABLE-NAME" ).findFirst() 
Find last data object from a table. The last data object is the last one saved in the data store:
public Map Backendless.Persistence.of( "TABLE-NAME" ).findLast() 
Find a data object by its ID:
public Map Backendless.Persistence.of( "TABLE-NAME" ).findById( String objectId ) 
Retrieve data objects with the default paging setting from a table. Returned collection will contain objects of type E (the name of the class must match the name of the table):
public List<E> Backendless.Persistence.of( E ).find() 
Find first data object of class E. The first data object is the first one saved in the data store:
public E Backendless.Persistence.of( E ).findFirst() 
Find last data object of type E. The last data object is the last one saved in the data store:
public E Backendless.Persistence.of( E ).findLast() 
Find a data object by its ID:
public E Backendless.Persistence.of( E ).findById( String objectId ) 

Asynchronous Methods:

Retrieve data objects with the default paging setting from a table:
public void Backendless.Persistence.of( "TABLE-NAME" ).find( AsyncCallback<List<Map>> responder )
Find first data object from a table. The first data object is the first one saved in the data store:
public void Backendless.Persistence.of( "TABLE-NAME" ).findFirst( AsyncCallback<Map> responder )
Find last data object from a table. The last data object is the last one saved in the data store:
public void Backendless.Persistence.of( "TABLE-NAME" ).findLast( AsyncCallback<Map> responder ) 
Find a data object by its ID:
public void Backendless.Persistence.of( "TABLE-NAME" ).findById( String objectId, 
                                                                 AsyncCallback<Map> responder ) 
Retrieve data objects with the default paging setting from a table. Returned collection will contain objects of type E (the name of the class must match the name of the table):
public void Backendless.Persistence.of( E ).find( AsyncCallback<List<E>> responder );
Find first data object from a table. The name of the class E must match the name of the table. The first data object is the first one saved in the data store:
public void Backendless.Persistence.of( E ).findFirst( AsyncCallback<E> responder )
Find last data object from a table. The name of the class E must match the name of the table. The last data object is the last one saved in the data store:
public void Backendless.Persistence.of( E ).findLast( AsyncCallback<E> responder )
Find a data object by its ID. The name of the class E must match the name of the table:
public void Backendless.Persistence.of( E ).findById( String objectId, 
                                                      AsyncCallback<E> responder )

where:

TABLE-NAME - Name of the table from where the data is retrieved from.
E - Java class identifying the table where from where the data must be loaded from. For example, if the table is "Person", the argument should Person.class.
responder - a responder object which will receive a callback when the method successfully returns the result or if an error occurs. Applies to the asynchronous methods only.

Example:

The following code demonstrates various search queries:

Load contacts using default paging:

Synchronous call:
List<Map> result = Backendless.Persistence.of( "Contact" ).find();
Asynchronous call:
Backendless.Persistence.of( "Contact" ).find( new AsyncCallback<List<Map>>(){
  @Override
  public void handleResponse( List<Map> foundContacts )
  {
    // every loaded object from the "Contact" table is now an individual java.util.Map
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find first contact:

Synchronous call:
Map firstContact = Backendless.Persistence.of( "Contact" ).findFirst();
Asynchronous call:
Backendless.Persistence.of( "Contact" ).findFirst( new AsyncCallback<Map>(){
  @Override
  public void handleResponse( Map contact )
  {
    // first contact instance has been found
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find last contact:

Synchronous call:
Map firstContact = Backendless.Persistence.of( "Contact" ).findLast();
Asynchronous call:
Backendless.Persistence.of( "Contact" ).findLast( new AsyncCallback<Map>(){
  @Override
  public void handleResponse( Map contact )
  {
    // last contact instance has been found
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find contact by objectId:

Synchronous call:
// Save a contact object first. Once it is saved, we will retrieve it by assigned objectId
HashMap contact = new HashMap();
contact.put( "name", "Jack Daniels" );
contact.put( "age", 147 );
contact.put( "phone", "777-777-777" );
contact.put( "title", "Favorites" );
 
// save object synchronously
Map savedContact = Backendless.Persistence.of( "Contact" ).save( contact );

// now retrieve the object using it's objectId
Map lastContact = Backendless.Persistence.of( "Contact" ).
                                    findById( savedContact.get( "objectId" ) );
Asynchronous call:
// Save a contact object first. Once it is saved, we will retrieve it by assigned objectId
HashMap contact = new HashMap();
contact.put( "name", "Jack Daniels" );
contact.put( "age", 147 );
contact.put( "phone", "777-777-777" );
contact.put( "title", "Favorites" );
 
Backendless.Persistence.of( "Contact" ).save( contact, new AsyncCallback<Map>()
{
  @Override
  public void handleResponse( Map savedContact )
  {
    // now that the object is saved and has objectId, retrieve it using "findById"
    Backendless.Persistence.of( Contact.class ).findById( savedContact.get( "objectId" ), 
                                                          new AsyncCallback<Map>() {
      @Override
      public void handleResponse( Map response )
      {
        // an object from the "Contact" table has been found by it's objectId
      }
      @Override
      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    } );
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
} );
Consider the following class:
package com.sample;

public class Contact
{
  private String objectId;
  private String name;
  private int age;
  private String phone;
  private String title;

  public String getObjectId() {
    return objectId;
  }

  public void setObjectId( String objectId ) {
    this.objectId = objectId;
  }

  public String getName() {
    return name;
  }

  public void setName( String name ) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge( int age ) {
    this.age = age;
  }

  public String getPhone() {
    return phone;
  }

  public void setPhone( String phone ) {
    this.phone = phone;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle( String title ) {
    this.title = title;
  }
}
The following code demonstrates various search queries:

Load contacts using default paging:

Synchronous call:
List<Contact> result = Backendless.Persistence.of( Contact.class ).find();
Asynchronous call:
Backendless.Persistence.of( Contact.class).find( new AsyncCallback<List<Contact>>(){
  @Override
  public void handleResponse( List<Contact> foundContacts )
  {
    // all Contact instances have been found
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find first contact:

Synchronous call:
Contact firstContact = Backendless.Persistence.of( Contact.class ).findFirst();
Asynchronous call:
Backendless.Persistence.of( Contact.class).findFirst( new AsyncCallback<Contact>(){
  @Override
  public void handleResponse( Contact contact )
  {
    // first contact instance has been found
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find last contact:

Synchronous call:
Contact firstContact = Backendless.Persistence.of( Contact.class ).findLast();
Asynchronous call:
Backendless.Persistence.of( Contact.class).findLast( new AsyncCallback<Contact>(){
  @Override
  public void handleResponse( Contact contact )
  {
    // last contact instance has been found
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find contact by objectId:

Synchronous call:
// Save a contact object first. Once it is saved, we will retrieve it by assigned objectId
Contact contact = new Contact();
contact.setName( "Jack Daniels" );
contact.setAge( 147 );
contact.setPhone( "777-777-777" );
contact.setTitle( "Favorites" );
 
// save object synchronously
Contact savedContact = Backendless.Persistence.save( contact );

// now retrieve the object using it's objectId
Contact lastContact = Backendless.Persistence.of( Contact.class ).findById( savedContact.getObjectId() );
Asynchronous call:
// Save a contact object first. Once it is saved, we will retrieve it by assigned objectId
Contact contact = new Contact();
contact.setName( "Jack Daniels" );
contact.setAge( 147 );
contact.setPhone( "777-777-777" );
contact.setTitle( "Favorites" );
 
Backendless.Persistence.save( contact, new AsyncCallback<Contact>()
{
  @Override
  public void handleResponse( Contact savedContact )
  {
    // now that the object is saved and has objectId, retrieve it using "findById"
    Backendless.Persistence.of( Contact.class ).findById( savedContact.getObjectId(), 
                                                          new AsyncCallback<Contact>() {
      @Override
      public void handleResponse( Contact response )
      {
        // a Contact instance has been found by objectId
      }
      @Override
      public void handleFault( BackendlessFault fault )
      {
        // an error has occurred, the error code can be retrieved with fault.getCode()
      }
    } );
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
} );

 

Advanced Object Retrieval

Advanced search use-cases supported by Backendless include:

Search with a query (a "where clause" search) -retrieves data objects which satisfy a condition.
Paged data retrieval - retrieves a "page" of data of the given size from the specified offset.
Sorted data object retrieval - retrieves a collection of data objects sorted by specified properties.
Retrieval of related objects - fetching data objects through special "relation" properties. See retrieval of related data objects.

 

Backendless supports the options listed above with a special class - DataQueryBuilder. The class includes various property setters to configure paging and sorting, specify a search query (the where clause), and/or to request retrieval of objects for specific related properties. For information about relation retrieval, see the Relations (Retrieve) chapter of the documentation.

 

Method Signature:

Synchronous Method:

// Return value from the server is a collection of java.util.Map objects; 
List<Map> result;

// The data table must be identified by name. The API
// returns a collection of Map objects.
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
result = Backendless.Data.of( "TABLE-NAME" ).find( queryBuilder );
List<E> result = Backendless.Data.of( E ).find( DataQueryBuilder queryBuilder );

Asynchronous Method:

Backendless.Data.of( "TABLE-NAME" ).find( DataQueryBuilder query, 
                                          AsyncCallback<List<Map>> );
Backendless.Data.of( E ).find( DataQueryBuilder queryBuilder, 
                               AsyncCallback<List<E>> );

where:

TABLE-NAME - Name of the table to retrieve data from.
E - Java class identifying the table to retrieve data from. The name of the class must match the name of the table.
queryBuilder - an instance of DataQueryBuilder - contains the search query and other search options.
responder - a responder object which will receive a callback when the search operation returns the result. Applies to the asynchronous method only.

Return Value:

The synchronous method returns a collection of strongly typed objects found as a result of the query execution. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Examples

For examples on using the advanced object retrieval API, see the following sub-sections:

Search with the Where clause

A where clause is a condition which references columns of a data table where the query is sent. For any given query Backendless returns the data records which match the condition in the where clause. A where clause is a query string which must conform to a subset of the SQL-92 standard. The subset of the syntax supported by Backendless is the part that goes into the WHERE clause of an SQL query. This is the reason why we refer to it as the where clause. For example, consider the following data objects, specifically notice the columns name and age:

sample-table.zoom70

 

Suppose you need to retrieve an object where the value of the name property is "Joe". This can be accomplished with the following where clause:

name = 'Joe'

 

Or if you need all objects where the age column contains a value less than 30:

age < 30

 

Just like in SQL, columns can be combined in a single where clause. The following condition would retrieve all objects containing letter J in the name and the age column's value is greater than 20:

name LIKE 'J%' and age > 20

 

Condition

Sample where clause

Column type

Value in a column equals a string literal.

name = 'foo'

STRING, TEXT or EXTENDED STRING

Value in a column does not equal a string literal.

name != 'foo'

STRING, TEXT or EXTENDED STRING

Value in a column is not assigned

name is null

STRING, TEXT or EXTENDED STRING

Value in a column is assigned

name is not null

STRING, TEXT or EXTENDED STRING

Value in a column contains a substring literal

name LIKE '%foo%'

STRING, TEXT or EXTENDED STRING

Value in a column ends with a substring literal

name LIKE '%foo'

STRING, TEXT or EXTENDED STRING

Value in a column starts with a substring literal

name LIKE 'foo%'

STRING, TEXT or EXTENDED STRING

Value in a column is one of enumerated string literals.

name IN ('value1', 'value2', 'value3)
 
or
 
name = 'value1' OR name = 'value2' or name = 'value3'

STRING, TEXT or EXTENDED STRING

Value in a column equals a numeric literal

age = 21

INT or DOUBLE

Value in a column is greater than a numeric literal

age > 21

INT or DOUBLE

Value in a column is greater than or equal a numeric literal

age >= 21

INT or DOUBLE

Value in a column is less than a numeric literal

age < 21

INT or DOUBLE

Value in a column is less than or equal a numeric literal

age <= 21

INT or DOUBLE

 

You do not need to declare/write any custom classes when using the Map approach. Data records are represented as java.util.Map objects. Column names become map property names and the object values are corresponding property values.
Consider the following class:
package com.sample;

public class Contact
{
  private String objectId;
  private String name;
  private int age;
  private String phone;
  private String title;

  public String getObjectId() {
    return objectId;
  }

  public void setObjectId( String objectId ) {
    this.objectId = objectId;
  }

  public String getName() {
    return name;
  }

  public void setName( String name ) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge( int age ) {
    this.age = age;
  }

  public String getPhone() {
    return phone;
  }

  public void setPhone( String phone ) {
    this.phone = phone;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle( String title ) {
    this.title = title;
  }
}

 

Find all contacts where the value of the "age" property equals 47:

String whereClause = "age = 47";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

// ***********************************************************
// Synchronous API:
// ***********************************************************
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );

// ***********************************************************
// Asynchronous API:
// ***********************************************************
Backendless.Data.of( "Contact" ).find( queryBuilder, 
                                       new AsyncCallback<List<Map>>(){
  @Override
  public void handleResponse( List<Map> foundContacts )
  {
    // every loaded object from the "Contact" table is now an individual java.util.Map
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});
String whereClause = "age = 47";
DataQueryBuilder queryBuilder = new DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

// ***********************************************************
// Synchronous API:
// ***********************************************************
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

// ***********************************************************
// Asynchronous API:
// ***********************************************************
Backendless.Data.of( Contact.class ).find( queryBuilder, 
                                           new AsyncCallback<List<Contact>>(){
  @Override
  public void handleResponse( List<Contact> foundContacts )
  {
    // the "foundContact" collection now contains instances of the Contact class.
    // each instance represents an object stored on the server.
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find all contacts where the value of the "age" property is greater than 21:

String whereClause = "age > 21";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

// ***********************************************************
// Synchronous API:
// ***********************************************************
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );

// ***********************************************************
// Asynchronous API:
// ***********************************************************
Backendless.Data.of( "Contact" ).find( queryBuilder, 
                                       new AsyncCallback<List<Map>>(){
  @Override
  public void handleResponse( List<Map> foundContacts )
  {
    // every loaded object from the "Contact" table is now an individual java.util.Map
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});
String whereClause = "age > 21";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

// ***********************************************************
// Synchronous API:
// ***********************************************************
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

// ***********************************************************
// Asynchronous API:
// ***********************************************************
Backendless.Data.of( Contact.class ).find( queryBuilder, 
                                           new AsyncCallback<List<Contact>>(){
  @Override
  public void handleResponse( List<Contact> foundContacts )
  {
    // the "foundContact" collection now contains instances of the Contact class.
    // each instance represents an object stored on the server.
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find all contacts by name:

String whereClause = "name = 'Jack Daniels'";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

// ***********************************************************
// Synchronous API:
// ***********************************************************
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );

// ***********************************************************
// Asynchronous API:
// ***********************************************************
Backendless.Data.of( "Contact" ).find( queryBuilder, 
                                       new AsyncCallback<List<Map>>(){
  @Override
  public void handleResponse( List<Map> foundContacts )
  {
    // every loaded object from the "Contact" table is now an individual java.util.Map
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});
String whereClause = "name = 'Jack Daniels'";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

// ***********************************************************
// Synchronous API:
// ***********************************************************
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

// ***********************************************************
// Asynchronous API:
// ***********************************************************
Backendless.Data.of( Contact.class ).find( queryBuilder, 
                                           new AsyncCallback<List<Contact>>(){
  @Override
  public void handleResponse( List<Contact> foundContacts )
  {
    // the "foundContact" collection now contains instances of the Contact class.
    // each instance represents an object stored on the server.
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

Find all contacts by partial name match:

String whereClause = "name LIKE 'Jack%'";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

// ***********************************************************
// Synchronous API:
// ***********************************************************
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );

// ***********************************************************
// Asynchronous API:
// ***********************************************************
Backendless.Data.of( "Contact" ).find( queryBuilder, 
                                       new AsyncCallback<List<Map>>(){
  @Override
  public void handleResponse( List<Map> foundContacts )
  {
    // every loaded object from the "Contact" table is now an individual java.util.Map
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});
String whereClause = "name LIKE 'Jack%'";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

// ***********************************************************
// Synchronous API:
// ***********************************************************
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

// ***********************************************************
// Asynchronous API:
// ***********************************************************
Backendless.Data.of( Contact.class ).find( queryBuilder, 
                                           new AsyncCallback<List<Contact>>(){
  @Override
  public void handleResponse( List<Contact> foundContacts )
  {
    // the "foundContact" collection now contains instances of the Contact class.
    // each instance represents an object stored on the server.
  }
  @Override
  public void handleFault( BackendlessFault fault )
  {
    // an error has occurred, the error code can be retrieved with fault.getCode()
  }
});

 

Search with SubQuery

Search with subquery allows your apps to run two queries in the context of a single data retrieval request. The first query identifies a set of objects which is fed into the second query. Consider the following data schema:

users-blogpost-comment

Suppose your app needs to get all blog posts where the person who posted a comment has a specific email address (for example, @backendless.com ). Using subqueries, it can be done with the following whereClause sent to the BlogPost table:
 

objectId in (Comment[author.email LIKE '%@backendless.com'].blogPost.objectId)

 

Let's review how this whereClause is processed by Backendless:

 

1. Backendless detects that the query at the top level uses the in()  operator.
2. It checks if the contents of the in() operator have the format of a subquery and if so, evaluates it accordingly.
3. The subquery contains an "internal whereClause", which is:
author.email LIKE '%@backendless.com'
4. The internal whereClause is applied to the objects in the Comment table.
5. For all found Comment objects, Backendless gets a list of values for the blogPost.objectId column.
6. The resulting set is fed into the in() operator which fetches the final set of objects from the BlogPost table.

 

General SubQuery Syntax

The general syntax for subqueries is:

 

searchColumnNameOrRelatedColumnName IN
  (TableName[internalWhereClause].columnOrRelatedColumnName.optionalcolumnName)

 

How it works:

1. If internalWhereClause  is present, it is executed in the table identified by TableName.
2. In the resulting set of records, values must be selected for the column identified by
columnOrRelatedColumnName.optionalColumnName.
3. If internalWhereClause is not present, the contents of the IN operator have the following syntax. It represents the entire set of values identified by the specified column: TableName.columnOrRelatedColumnName.optionalColumnName
4. The resulting set of values is used in the IN operator, thus the final query ends up being:
searchColumnOrRelatedColumnName IN ( value1, value2,,&nbsp;valueN )

 

Additional Examples

 

Consider the following data schema:

customer-order-items

1. Get all OrderItem  objects for a customer from New York:
objectId in (Order[customer.address = 'New York'].items.objectId)
 
2. Get all Customer objects who spent more than 1.99 on an item:
objectId in (Order[items.itemPrice > 1.99].customer.objectId)

Using Dates in Search

There is a special consideration for the whereClause-based queries which reference a column of the DATETIME data type. Typically a DATETIME column is referenced in a comparison against a scalar value describing a specific date or a timestamp. The scalar value can be a number of milliseconds since the epoch (UNIX timestamp as milliseconds) or a string. Backendless supports a variety of date formats for the latter. For example, the queries below will find all the objects which were updated after March 23rd, 2015:

updated > '23-Mar-2015'

updated > '03/23/2015'

updated > 1427068800000

Comparison Operators

Backendless supports the following date comparison operators:

Column's value is after the specified date/time: use either > or the after keyword:

birthDate > '22-Dec-1980'

birthDate after 1427068800000

Column's value is before the specified date/time: use either < or the before keyword:

birthDate < '22-Dec-1980'

birthDate before 1427068800000

Column's value is either at or after the specified date/time: use either => or the at or after keyword:

birthDate >= '28-10-1988'

birthDate at or after '10/28/1988 00:00:00 GMT-0200'

Column's value is either at or before the specified date/time: use either <= or the at or before keyword:

birthDate >= '28-10-1988'

birthDate at or after '10/28/1988 00:00:00 GMT-0200'

Note: the whereClause-based queries can be tested in the Backendless Console with the SQL Search turned on.

Supported Date Formats

Date/time string values may be in any of the following formats. The pattern letters have the same definition as in Java's SimpleDateFormat:

EEE MMM dd HH:mm:ss zzz yyyy
yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
MM/dd/yyyy HH:mm:ss 'GMT'z
MM.dd.yyyy HH:mm:ss 'GMT'z
MM-dd-yyyy HH:mm:ss 'GMT'z
MM/dd/yyyy HH:mm:ss z
MM.dd.yyyy HH:mm:ss z
MM.dd.yyyy HH:mm:ss
MM-dd-yyyy HH:mm:ss
MM/dd/yyyy HH:mm:ss
MM.dd.yyyy
MM-dd-yyyy
MM/dd/yyyy HH:mm:ss 'GMT'Z
MM/dd/yyyy HH:mm
MM/dd/yyyy
dd/MMM/yyyy
dd-MMM-yyyy
EEEEE, d MMMMM yyyy
yyyy/MM/d/HH:mm:ss
yyyy-MM-dd'T'HH:mm:ss
EEEEE, MMMMM d, yyyy
MMMMM d, yyyy
yyyy M d
yyyyMMMd
yyyy-MMM-d
yyyy-M-d, E
'Date' yyyy-MM-dd
yyyy-MM-dd'T'HH:mm:ssZ
yyyy-MM-dd'T'HH:mmZ
yyyy-MM-dd
yyyy-'W'w
yyyy-DDD
d MMMMM yyyy, HH'h' mm'm' ss's'

Example

String whereClause = "updated after " + calendar.getTime();
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );
String whereClause = "updated after " + calendar.getTime();
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause );

List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

 

Data retrieval with Paging

Backendless operations returning a collection of objects automatically break up the complete result set into pages. Backendless paging uses two parameters to enable paged data retrieval:

page size - identifies how many objects a single page contains
offset - identifies the position of the object from which to retrieve the number of records identified by page size

 

The diagrams below illustrate the paging process. The first diagram shows the first request to retrieve a collection of records. The request includes the parameters of 5 objects in the page, starting from offset 0:

findrequest1

 

To retrieve the next page of data, the client must set the offset to the index of the first not retrieved object, which is 5:

findrequest2

 

Backendless server sets the maximum allowed value for the page size to 100 objects. The minimum value is 1.

 

Android applications must use the DataQueryBuilder class to set the paging parameters. The example below configures DataQueryBuilder to load 25 objects starting from offset 50. The query is used to retrieve objects from the Person table:

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setPageSize( 25 ).setOffset( 50 );
Backendless.Data.of( "Person" ).find( queryBuilder, 
                                      new AsyncCallback<List<Map>>()
  {
    @Override
    public void handleResponse( List<Map> response )
    {
      // the "response" object is a collection of java.util.Map objects.
      // each item in the collection represents an object from the "Person" table
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
       // use the getCode(), getMessage() or getDetail() on the fault object
       // to see the details of the error
    }
  });
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setPageSize( 25 ).setOffset( 50 );
Backendless.Data.of( Person.class ).find( queryBuilder, 
                                          new AsyncCallback<List<Person>>()
  {
    @Override
    public void handleResponse( List<Person> response )
    {
      // the "response" object is a collection of Person objects.
      // each item in the collection represents an object from the "Person" table 
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
       // use the getCode(), getMessage() or getDetail() on the fault object
       // to see the details of the error
    }
    });

Application can use the same queryBuilder object to recalculate offset (and if necessary change page size) and use it to retrieve additional pages from the server using the same data retrieval method:

// calculate offset to get the next page of data
queryBuilder.prepareNextPage();

// retrieve the next page of data using the same API as for the first page
Backendless.Data.of( "Person" ).find( queryBuilder,
                                      new AsyncCallback<List<Map>>()
  {
    @Override
    public void handleResponse( List<Map> response )
    {
      // the "response" object is a collection of java.util.Map objects.
      // each item in the collection represents an object from the "Person" table
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
       // use the getCode(), getMessage() or getDetail() on the fault object
       // to see the details of the error
    }
  });
// calculate offset to get the next page of data
queryBuilder.prepareNextPage();

// retrieve the next page of data using the same API as for the first page
Backendless.Data.of( Person.class ).find( queryBuilder, 
                                          new AsyncCallback<List<Person>>()
  {
    @Override
    public void handleResponse( List<Person> response )
    {
      // the "response" object is a collection of Person objects.
      // each item in the collection represents an object from the "Person" table 
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
       // use the getCode(), getMessage() or getDetail() on the fault object
       // to see the details of the error
    }
    });

 

 

 

Sorting

Data retrieval and search API can request the server to return sorted data. Data can be sorted by one or more columns. The sorting direction can be either ascending (default) or descending for each individual column. Android applications must use the DataQueryBuilder class to set the sorting option. The example below configures DataQueryBuilder to sort results by the name and age columns. Value in the age column are sorted in the descending order. The query is used to retrieve objects from the Person table:

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setSortBy( "name", "age DESC" );
Backendless.Data.of( "Person" ).find( queryBuilder, 
                                      new AsyncCallback<List<Map>>()
  {
    @Override
    public void handleResponse( List<Map> response )
    {
      // the "response" object is a collection of java.util.Map objects.
      // each item in the collection represents an object from the "Person" table
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
       // use the getCode(), getMessage() or getDetail() on the fault object
       // to see the details of the error
    }
  });
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setSortBy( "name", "age DESC" );
Backendless.Data.of( Person.class ).find( queryBuilder, 
                                          new AsyncCallback<List<Person>>()
  {
    @Override
    public void handleResponse( List<Person> response )
    {
      // the "response" object is a collection of Person objects.
      // each item in the collection represents an object from the "Person" table 
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
       // use the getCode(), getMessage() or getDetail() on the fault object
       // to see the details of the error
    }
    });

The descending sorting order can be requested by adding DESC to the name of the column (separated by space).

Search by distance

With the ability to link data objects with geo points, you can search for data objects by distance. This type of search returns a paged set of data objects which satisfy the whereClause condition and located within the specified distance. Distance-based search uses a special function in whereClause of the search request. The syntax of the function is:

distance( 
   center point latitude, 
   center point longitude, 
   columnname which contains geo point.latitude,
   columnname which contains geo point.longitude )<operator> units-function(value)

where:

<operator> - Possible values are <, >, =, >=, <=
units-function - Defines the units of measure for the distance. Possible values are:
ft( X ) - the distance value X is expressed in feet
km( X ) - the distance value X is expressed in kilometers
mi( X ) - the distance value X is expressed in miles
yd( X ) -  the distance value X is expressed in yards

For example, the following whereClause expression searches for data objects located within 200 miles from the point at 30.26715, -97.74306. Each data object must have the "coordinates" property of type GeoPoint.

distance( 30.26715, -97.74306, coordinates.latitude, coordinates.longitude ) < mi(200)

The following example demonstrates a search-by-distance query. The example uses three data objects stored in the Friend table: Bob, Jane, and Fred who respectively live in Austin, Houston, San Antonio. The search query in the example finds all friends who live within the specified distance. Before running the search query, create the objects in the data storage with the corresponding geo points.

 

The application must be setup with demo data in order to run the example and see the distance operator in action. See the setup code in the expandable block below:

You do not need to declare/write any custom classes when using the Map approach. Data records are represented as java.util.Map objects. Column names become map property names and the object values are corresponding property values.
The Friend class definition:
import com.backendless.geo.GeoPoint;

public class Friend
{
  private String name;
  private String phoneNumber;
  private GeoPoint coordinates;

  public String getName()
  {
    return name;
  }

  public void setName( String name )
  {
    this.name = name;
  }

  public String getPhoneNumber()
  {
    return phoneNumber;
  }

  public void setPhoneNumber( String phoneNumber )
  {
    this.phoneNumber = phoneNumber;
  }

  public GeoPoint getCoordinates()
  {
    return coordinates;
  }

  public void setCoordinates( GeoPoint coordinated )
  {
    this.coordinates = coordinated;
  }
}

 

Run the following query/code to store a data object representing Bob with a link to his home in Austin, TX:

HashMap bob = new HashMap();
bob.put( "name", "Bob" );
bob.put( "phoneNumber", "512-555-1212" );

GeoPoint geopoint = new GeoPoint( 29.76328, -95.36327 )
geopoint.addCategory( "Home" );
geopoint.addMetadata( "description", "Bob's home" );
bob.put( "coordinates", geopoint );

Backendless.Data.of( "Friend" ).save( bob );
Friend bob = new Friend();
bob.setName( "Bob" );
bob.setPhoneNumber( "512-555-1212" );
bob.setCoordinates( new GeoPoint( 29.76328, -95.36327 ) );
bob.getCoordinates().addCategory( "Home" );
bob.getCoordinates().addMetadata( "description", "Bob's home" );
Backendless.Data.of( Friend.class ).save( bob );

 

Run the following query/code to store a data object representing Jane with a link to her home in Houston, TX:

HashMap jane = new HashMap();
jane.put( "name", "Jane" );
jane.put( "phoneNumber", "281-555-1212" );

GeoPoint geopoint = new GeoPoint( 29.76328, -95.36327 )
geopoint.addCategory( "Home" );
geopoint.addMetadata( "description", "Jane's home" );
jane.put( "coordinates", geopoint );

Backendless.Data.of( "Friend" ).save( jane );
Friend jane = new Friend();
jane.setName( "Jane" );
jane.setPhoneNumber( "281-555-1212" );
jane.setCoordinates( new GeoPoint( 29.76328, -95.36327 ) );
jane.getCoordinates().addCategory( "Home" );
jane.getCoordinates().addMetadata( "description", "Jane's home" );
Backendless.Data.of( Friend.class ).save( jane );

 

Run the following query/code to store a data object representing Fred with a link to his home in San Antonio, TX:

HashMap fred = new HashMap();
fred.put( "name", "Fred" );
fred.put( "phoneNumber", "210-555-1212" );

GeoPoint geopoint = new GeoPoint( 29.42412, -98.49363 )
geopoint.addCategory( "Home" );
geopoint.addMetadata( "description", "Fred's home" );
fred.put( "coordinates", geopoint );

Backendless.Data.of( "Friend" ).save( fred );
Friend fred = new Friend();
fred.setName( "Fred" );
fred.setPhoneNumber( "210-555-1212" );
fred.setCoordinates( new GeoPoint( 29.42412, -98.49363 ) );
fred.getCoordinates().addCategory( "Home" );
fred.getCoordinates().addMetadata( "description", "Fred's home" );
Backendless.Data.of( Friend.class ).save( fred );

 

 

Once the data is in the persistent object and geo location storage, you can verify it in Backendless Console by opening the Geolocation screen and selecting the Home geocategory:

distance-search

 

Suppose you need to get all objects located within 300 miles radius from Beaumont, TX, which has the GPS coordinates of 30.084, -94.145. The following code/query performs that distance-based search:

String whereClause = "distance( 30.26715, -97.74306, " +
                     "coordinates.latitude, coordinates.longitude ) < mi(200)";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause ).setRelationsDepth( 1 );
List<Map> friends = Backendless.Data.of( "Friend" ).find( queryBuilder );

String format = "%s lives at %f, %f tagged as '%s'";

for( Map friend : friends )
{
  GeoPoint coordinates = (GeoPoint) friend.get( "coordinates" );
  Log.i( "MYAPP", String.format( format, friend.get( "name" ), 
                                 coordinates.getLatitude(), 
                                 coordinates.getLongitude(),   
                                (String) coordinates.getMetadata( "description" ) ) );
}
String whereClause = "distance( 30.26715, -97.74306, " + 
                     "coordinates.latitude, coordinates.longitude ) < mi(200)";
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause ).setRelationsDepth( 1 );
List<Friend> friends = Backendless.Data.of( Friend.class ).find( queryBuilder );

String format = "%s lives at %f, %f tagged as '%s'";

for( Friend friend : friends )
{
  GeoPoint coordinates = friend.getCoordinates();
  Log.i( "MYAPP", String.format( format, friend.getName(), 
                                 coordinates.getLatitude(), 
                                 coordinates.getLongitude(),   
                                 (String) coordinates.getMetadata( "description" ) ) );
}

The search returns all data objects within the specified distance. Each data object has the Coordinates property containing the coordinates of a geo point associated with this data object.

Relations Overview

A data object stored in Backendless may reference other objects from the same or other data tables. These references are called relations. When an object references a single object, it is a one-to-one relation. If a reference is for a collection of objects, it is a one-to-many relation. Relations can be established using either API or Backendless Console. When working with console, a relation in a table must be declared through a relation column. Console allows two types of relation columns: data object relationship and geopoint relationship. The former identifies a relation between two data tables, while the later declares a relationship between a data table and geopoint storage. It is not required to declare relation columns in console - Backendless can derive these relations from the API calls and subsequently creates columns in data tables.

 

The rest of this chapter describes relationship management using Backendless console. For the API reference, see the corresponding chapters.

Declaring a Relation Between Tables

Creating Relations Between Objects in Related Tables

Editing (Update/Delete) Relations

Deleting Relation Column

Declaring a Relation Between Tables

The instructions below describe how to declare a relation between two tables. As an example, the instructions below create a relation column address in the Person table. The new column will reference the Address table:

1. Select the Person table where a relation column should be declared.
2. Click the Schema menu.
3. Click the New button. The pop-up window will display as shown below:

adding-new-column.zoom80

4. Enter column name that will identify the relation.
5. Click the Type drop-down list and select the Data Object Relationship option.
6. Select a related table and the cardinality of the relations from the corresponding drop-down menus. The one-to-one relation means that a table's object can be linked with only one object from the related table. The one-to-many relation means that a table's object can be linked with more than one object from the related table.

relationship-column.zoom80

7. Click CREATE to save the changes.

Once a relationship column is declared, it will appear along other columns in the Data Browser view of the Backendless Console.

Creating Relations Between Objects in Related Tables

Once you have declared a relation between table schemas, you can establish a relation between objects in these tables. Follow the instructions below to link the objects:

1. Click the table name where you declared a relation. Console displays the columns representing relations slightly different than the other ones. The header for these columns includes:
- name of the related table;
- relation type (cardinality) visualized as either a single red line for one-to-one relations or three red lines for one-to-many relations;
- the "auto-load" checkbox.

relation-column-data-browser.zoom70

2. Every object in the table displays a plus icon on mouse hover which can be used to create a relation between that object and one from the related table:
plus-for-relation
3. Click the plus icon for the object to create a relation. The Set Related Object pop-up window will display the list of objects from the related table.

set-relation-popup.zoom70

4. Each object in the displayed popup has either a radio button or a checkbox next to the object's data. Radio buttons are used for one-to-one relations, while checkboxes apply for the one-to-many relations. Select the object(s) which will be linked to the parent object.
5. Click the ADD RELATION button to save the changes.
6. Once a relation between the objects is established, the related object(s) appear as hyperlinks which can be clicked to navigate to them:
relation-created

Editing (Update/Delete) Relations

You can edit the relations between the data objects. Editing a relation allows changing the related object or breaking the relation between the objects.

1. Click the name of the table containing the object with relations. Click the plus icon in the cell displaying the related object (the same plus icon you used to create a relation above):

editing-relation-plus-icon.zoom80

2. The Set Related Object pop-up window will open and display the related object. If you want to link a data object with other object(s), click the radio-button or check-box(s) next to the necessary object(s).
remove-relation.zoom70
3. Depending on the made changes, the action button at the bottom of the popup will say either REMOVE RELATION or UPDATE RELATION. Click the button to finalize the changes.

Deleting Relation Column

When a relation column is deleted, all references between the related objects are also removed. This operation does not delete the actual objects - it removes the column and any references between the related objects.

To delete a relation column using the Backendless Console:

1. Click the name of the table where you want to delete a relation column.
2. Click the Schema menu
3. Click the check-box next to the column you need to delete.
4. Click the Delete menu:

delete-relation-column

 

Relations API (Set/Add)

This and other relation API sections refer to objects in a relation as parent and child(ren). The parent object is the one that contains a reference to a related object. A child object is one that is being referenced by the parent.

There are two operations supporting creating a relation between objects - setting a relation and adding a relation:

Setting a relation - sets specified child objects as the related objects for the parent. If the parent had other child objects prior to the operation, the relation between them and the parent is removed. This is a replacement operation.
Adding a relation - this operation adds specified child objects to the collection of existing child objects. This is a concatenation operation. For one-to-one relations, if the parent object has a child for the specified column at the time when the operation is called, an error is returned back to the client

 

Other implementation details:

Both parent and child objects must exist in the Backendless storage.
If a column identifies a 1:1 relation which already exists between the parent and some other object, the "set" operation must replace the child object with the one identified in the operation. The "add" operation must return an error.
Add/set methods must return the number of objects added/set, that is, for example, if a whereClause is used and identifies an object (among others) which is already in the relation, that object must not be "accounted for" in the returned number of object.
If add/set receives a non-existing object, it must be ignored and the returned count of objects set/added must not reflect the ignored object.
Both operations create the relation column if it does not exist at the time of the method call.
 

There are two ways to add/set a relation:

Explicit relation list - an API request identifies specific objects to be added/set as children of the parent object. Available APIs:
Set Relation
Add Relation
Implicit relation list - an API request uses a query (the where clause) to identify objects which should be added/set as children of the parent object. Available APIs:
Set Relation
Add Relation

Set Relation with objects

API request replaces related objects with the ones identified in the API call. Child objects must be explicitly defined by referencing their IDs.

Synchronous Method:

Integer result = Backendless.Persistence.of( "TABLE-NAME" ).setRelation( 
                                                   Map parentObject,
                                                   String relationColumnName,
                                                   Collection<Map> children );
Integer result = Backendless.Persistence.of( E ).setRelation( 
                                                   E parentObject,
                                                   String relationColumnName,
                                                   Collection<T> children );

Asynchronous Method:

Backendless.Persistence.of( "TABLE-NAME" ).setRelation( 
                                             Map parentObject,
                                             String relationColumnName,
                                             Collection<Map> children,
                                             AsyncCallback<Integer> callback );
Backendless.Persistence.of( E ).setRelation( 
                                   E parentObject,
                                   String relationColumnName,
                                   Collection<T> children,
                                   AsyncCallback;<Integer> callback );

where:

TABLE-NAME - name of the table where the parent object is stored.
E - Java class of the parent object. The class name identifies the table where the parent object is stored.
parentObject - the object which will be assigned related children for relatedColumnName. When this argument is an instance of java.util.Map (for the map-based approach), it must contain the "objectId" property.
relationColumnName - name of the column identifying the relation. Objects from the children collection will be set as related for the  column in parentObject. The column name may optionally include table name separated by the colon character as well as cardinality which determines the type of relationship (one to one or one to many) (see the note below):

If the column does not exist in the parent table at the time when the API is called, the value of the "relationColumnName" argument must include the name of the child table separated by colon and the cardinality notation. The cardinality is expressed as ":1" for one-to-one relations and ":n" for one-to-many relations. For example, the value of "myOrder:Order:1" will create a one-to-one relation column "myOrder" in the parent table. The column will point to the Order child table. Likewise, the value of "myOrder:Order:n" will create a one-to-many relation column "myOrder" pointing to the Order table.

children - a collection of child objects to set into the relation identified by relatedColumnName. For the one-to-one relations the collection must contain one element.
callback - a responder object which will receive a callback when the relation has been set or if an error occurs. Applies to the asynchronous method only.

Return Value:

Number of child objects set into the relation. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

The example below sets a relation for a one-to-one column named address declared in the Person table. The column must be declared in the table prior to the execution of the request shown below. This necessity is explained by missing table name qualifier in the relationColumnName argument - notice the relation column is "address". If the argument value were "address:Address:1", then the column would be created automatically.
relation-column
 

HashMap<String, Object> parentObject = new HashMap<String, Object>();
parentObject.put( "objectId", "41230622-DC4D-204F-FF5A-F893A0324800" );

HashMap<String, Object> childObject = new HashMap<String, Object>();
childObject.put( "objectId", "3464C734-F5B8-09F1-FFD3-18647D12E700" );

ArrayList<Map> children = new ArrayList<Map>();
children.add( childObject );

Backendless.Data.of( "Person" ).setRelation( parentObject, "address", children, 
                                             new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer response )
  {
    Log.i( "MYAPP", "relation has been set" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
  }
} );
Person personObject = // personObject retrieval is out of scope in this example
Address addressObject = // addressObject retrieval is out of scope in this example

ArrayList<Address> addressCollection = new ArrayList<Address>();
addressCollection.add( addressObject );

Backendless.Data.of( Person.class ).setRelation( personObject, "address", addressCollection,
        new AsyncCallback<Integer>()
        {
          @Override
          public void handleResponse( Integer response )
          {
            Log.i( "MYAPP", "relation has been set");
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
            Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );

Set Relation with condition

API request replaces related objects with the ones identified in the API call. Child objects are referenced implicitly - through a whereClause defining the condition for object selection.

Synchronous Method:

Integer result = Backendless.Persistence.of( "TABLE-NAME" ).setRelation( 
                                                   Map parentObject,
                                                   String relationColumnName,
                                                   String whereClause );
Integer result = Backendless.Persistence.of( E ).setRelation( 
                                                   E parentObject,
                                                   String relationColumnName,
                                                   String whereClause );

Asynchronous Method:

Backendless.Persistence.of( "TABLE-NAME" ).setRelation( 
                                             Map parentObject,
                                             String relationColumnName,
                                             String whereClause,
                                             AsyncCallback<Integer> callback );
Backendless.Persistence.of( E ).setRelation( 
                                   E parentObject,
                                   String relationColumnName,
                                   String whereClause,
                                   AsyncCallback;<Integer> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - Java class of the parent object. The class name identifies the table where the parent object is stored.
parentObject - The object which will be assigned related children for relatedColumnName. When this argument is an instance of java.util.Map (for the map-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Objects from the children collection will be set as related for the  column in parentObject. The column name may optionally include table name separated by the colon character as well as cardinality which determines the type of relationship (one to one or one to many) (see the note below):

If the column does not exist in the parent table at the time when the API is called, the value of the "relationColumnName" argument must include the name of the child table separated by colon and the cardinality notation. The cardinality is expressed as ":1" for one-to-one relations and ":n" for one-to-many relations. For example, the value of "myOrder:Order:1" will create a one-to-one relation column "myOrder" in the parent table. The column will point to the Order child table. Likewise, the value of "myOrder:Order:n" will create a one-to-many relation column "myOrder" pointing to the Order table.

whereClause - a where clause condition identifying objects in the child table which will be set as the related objects for the parent object.
callback - a responder object which will receive a callback when the relation has been set or if an error occurs. Applies to the asynchronous method only.

Return Value:

Number of child objects set into the relation. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

The following request creates a relation between a Person object and all objects in the Users table which match the provided query. The query is specified in the whereClause argument:

name='Joe' or name = 'Frank'.

As a result of the operation, all User objects where the name property is either Joe or Frank will be set in the relation. The relation column is created if it does not exist. This is done because the column contains the child table qualifier, defined as ":Users" right after the column name.

HashMap<String, Object> parentObject = new HashMap<String, Object>();
parentObject.put( "objectId", "41230622-DC4D-204F-FF5A-F893A0324800" );

Backendless.Data.of( "Person" ).setRelation( parentObject, 
                                             "users:Users:n", 
                                             "name = \"Joe\" or name = \"Frank\"", 
                                             new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer response )
  {
    Log.i( "MYAPP", "relation has been set" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
  }
} );
Person personObject = // personObject retrieval is out of scope in this example

Backendless.Data.of( Person.class ).setRelation( personObject, 
                                                 "users:Users:n", 
                                                 "name = \"Joe\" or name = \"Frank\"",
        new AsyncCallback<Integer>()
        {
          @Override
          public void handleResponse( Integer response )
          {
            Log.i( "MYAPP", "relation has been set");
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
            Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );

Add Relation with objects

This API request adds related objects to the existing collection. Child objects to add to the relation must be explicitly defined by referencing their IDs.

Synchronous Method:

Integer result = Backendless.Persistence.of( "TABLE-NAME" ).addRelation( 
                                                   Map parentObject,
                                                   String relationColumnName,
                                                   Collection<Map> children );
Integer result = Backendless.Persistence.of( E ).addRelation( 
                                                   E parentObject,
                                                   String relationColumnName,
                                                   Collection<T> children );

Asynchronous Method:

Backendless.Persistence.of( "TABLE-NAME" ).addRelation( 
                                             Map parentObject,
                                             String relationColumnName,
                                             Collection<Map> children,
                                             AsyncCallback<Integer> callback );
Backendless.Persistence.of( E ).addRelation( 
                                   E parentObject,
                                   String relationColumnName,
                                   Collection<T> children,
                                   AsyncCallback;<Integer> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - Java class of the parent object. The class name identifies the table where the parent object is stored.
parentObject - The object which will be assigned related children for relatedColumnName. When this argument is an instance of java.util.Map (for the map-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Objects from the children collection will be added as related for the  column in parentObject. The column name may optionally include table name separated by the colon character as well as cardinality which determines the type of relationship (one to one or one to many) (see the note below):

If the column does not exist in the parent table at the time when the API is called, the value of the "relationColumnName" argument must include the name of the child table separated by colon and the cardinality notation. The cardinality is expressed as ":1" for one-to-one relations and ":n" for one-to-many relations. For example, the value of "myOrder:Order:1" will create a one-to-one relation column "myOrder" in the parent table. The column will point to the Order child table. Likewise, the value of "myOrder:Order:n" will create a one-to-many relation column "myOrder" pointing to the Order table.

children - A collection of child objects to add to the relation identified by relatedColumnName.
callback - a responder object which will receive a callback when the related objects have been added or if an error occurs. Applies to the asynchronous method only.

Return Value:

Number of child objects added to the relation. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

The example below adds a related object for a one-to-one column named address declared in the Person table. If the relation already contains an object, server returns an error since the addRelation API for one-to-one relations can add an object only once. The column must be declared in the table prior to the execution of the request shown below. This necessity is explained by missing table name qualifier in the relationColumnName argument - notice the relation column name is "address". If it were specified as "address:Address", then the column would be created automatically.
relation-column
 

HashMap<String, Object> parentObject = new HashMap<String, Object>();
parentObject.put( "objectId", "41230622-DC4D-204F-FF5A-F893A0324800" );

HashMap<String, Object> childObject = new HashMap<String, Object>();
childObject.put( "objectId", "3464C734-F5B8-09F1-FFD3-18647D12E700" );

ArrayList<Map> children = new ArrayList<Map>();
children.add( childObject );

Backendless.Data.of( "Person" ).addRelation( parentObject, "address:Address:n", children, 
                                             new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer response )
  {
    Log.i( "MYAPP", "related object has been added" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
  }
} );
Person personObject = // personObject retrieval is out of scope in this example
Address addressObject = // addressObject retrieval is out of scope in this example

ArrayList<Address> addressCollection = new ArrayList<Address>();
addressCollection.add( addressObject );

Backendless.Data.of( Person.class ).addRelation( personObject, "address:Address:n", addressCollection,
        new AsyncCallback<Integer>()
        {
          @Override
          public void handleResponse( Integer response )
          {
            Log.i( "MYAPP", "related objects have been added");
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
            Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );

Add Relation with condition

This API request adds related objects to the existing collection. Child objects to add to the relation are defined through a whereClause condition.

Synchronous Method:

Integer result = Backendless.Persistence.of( "TABLE-NAME" ).addRelation( 
                                                   Map parentObject,
                                                   String relationColumnName,
                                                   String whereClause );
Integer result = Backendless.Persistence.of( E ).addRelation( 
                                                   E parentObject,
                                                   String relationColumnName,
                                                   String whereClause );

Asynchronous Method:

Backendless.Persistence.of( "TABLE-NAME" ).addRelation( 
                                             Map parentObject,
                                             String relationColumnName,
                                             String whereClause,
                                             AsyncCallback<Integer> callback );
Backendless.Persistence.of( E ).addRelation( 
                                   E parentObject,
                                   String relationColumnName,
                                   String whereClause,
                                   AsyncCallback;<Integer> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - Java class of the parent object. The class name identifies the table where the parent object is stored.
parentObject - The object which will be assigned related children for relatedColumnName. When this argument is an instance of java.util.Map (for the map-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Objects from the children collection will be added as related for the  column in parentObject. The column name may optionally include table name separated by the colon character as well as cardinality which determines the type of relationship (one to one or one to many) (see the note below):

If the column does not exist in the parent table at the time when the API is called, the value of the "relationColumnName" argument must include the name of the child table separated by colon and the cardinality notation. The cardinality is expressed as ":1" for one-to-one relations and ":n" for one-to-many relations. For example, the value of "myOrder:Order:1" will create a one-to-one relation column "myOrder" in the parent table. The column will point to the Order child table. Likewise, the value of "myOrder:Order:n" will create a one-to-many relation column "myOrder" pointing to the Order table.

whereClause - a where clause condition identifying objects in the child table which will be added as related objects to the parent object.
callback - a responder object which will receive a callback when the related objects have been added or if an error occurs. Applies to the asynchronous method only.

Return Value:

Number of child objects added to the relation. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

The following request adds objects from the Users table to a related property/column the Person object. Child objects added to the relation  must match the provided query. The query is specified in the whereClause argument:

name='Joe' or name = 'Frank'.

As a result of the operation, all User objects where the name property is either Joe or Frank will be added to the relation. The relation column is created if it does not exist. This is done because the column contains the child table qualifier, defined as ":Users" right after the column name.

HashMap<String, Object> parentObject = new HashMap<String, Object>();
parentObject.put( "objectId", "41230622-DC4D-204F-FF5A-F893A0324800" );

Backendless.Data.of( "Person" ).addRelation( parentObject, 
                                             "users:Users:n", 
                                             "name = \"Joe\" or name = \"Frank\"", 
                                             new AsyncCallback<Integer>()
{
  @Override
  public void handleResponse( Integer response )
  {
    Log.i( "MYAPP", "related objects have been added" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
  }
} );
Person personObject = // personObject retrieval is out of scope in this example

Backendless.Data.of( Person.class ).addRelation( personObject, 
                                                 "users:Users:n", 
                                                 "name = \"Joe\" or name = \"Frank\"",
        new AsyncCallback<Integer>()
        {
          @Override
          public void handleResponse( Integer response )
          {
            Log.i( "MYAPP", "related objects have been added");
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
            Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );

Relations API (Delete)

This and other relation API sections refer to objects in a relation as parent and child(ren). The parent object is the one that contains a reference to a related object. A child object is one that is being referenced by the parent.

Relation deletion API disconnects a relation between the parent object and some (or all) related children. The API does not delete any objects, it is responsible strictly for breaking the relationship. For object deletion API, see the Deleting Data Objects section of the documentation.

 

There are two ways to delete a relation:

Explicit deletion list - an API request identifies specific child objects to be disconnected from the parent object.

Implicit deletion list - an API request uses a condition (the where clause) to identify the child objects which should be disconnected from the parent object.

Delete Relation with objects

API request must identify the child objects to delete from the relation explicitly, by referencing their IDs.

Synchronous Method:

Integer result = Backendless.Persistence.of( "TABLE-NAME" ).deleteRelation( 
                                                   Map parentObject,
                                                   String relationColumnName,
                                                   Collection<Map> children );
Integer result = Backendless.Persistence.of( E ).deleteRelation( 
                                                   E parentObject,
                                                   String relationColumnName,
                                                   Collection<T> children );

Asynchronous Method:

Backendless.Persistence.of( "TABLE-NAME" ).deleteRelation( 
                                             Map parentObject,
                                             String relationColumnName,
                                             Collection<Map> children,
                                             AsyncCallback<Integer> callback );
Backendless.Persistence.of( E ).deleteRelation( 
                                   E parentObject,
                                   String relationColumnName,
                                   Collection<T> children,
                                   AsyncCallback;<Integer> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - Java class of the parent object. The class name identifies the table where the parent object is stored.
parentObject - The object for which the relation with the specified children will be deleted. When this argument is an instance of java.util.Map (for the map-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Relationship between the specified objects from the children collection will be deleted for the column in parentObject.
children - A collection of child objects for which the relationship with the parentObject will be deleted.
callback - a responder object which will receive a callback when the relation has been deleted or if an error occurs. Applies to the asynchronous method only.

Return Value:

Number of child objects for which the relationship has been deleted. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

The example below deletes a relation between a Person object and its children. The child objects are referenced explicitly in the API call (see the object IDs in the collection as "XXXXX-XXXXX-XXXXX-XXXXX" and "ZZZZ-ZZZZ-ZZZZZ-ZZZZZ").The relation column is address.

HashMap<String, Object> parentObject = new HashMap<String, Object>();
parentObject.put( "objectId", "41230622-DC4D-204F-FF5A-F893A0324800" );

HashMap<String, Object> childObject1 = new HashMap<String, Object>();
childObject1.put( "objectId", "XXXX-XXXX-XXXX-XXXXX" );

HashMap<String, Object> childObject2 = new HashMap<String, Object>();
childObject2.put( "objectId", "ZZZZ-ZZZZ-ZZZZZ-ZZZZZ" );

ArrayList<Map> children = new ArrayList<Map>();
children.add( childObject1 );
children.add( childObject2 );

Backendless.Data.of( "Person" ).deleteRelation( parentObject, "address", children, 
                                             new AsyncCallback<Void>()
{
  @Override
  public void handleResponse( Void response )
  {
    Log.i( "MYAPP", "relation has been deleted" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
  }
} );
Person personObject = // personObject retrieval is out of scope in this example
Address addressObject1 = // addressObject retrieval is out of scope in this example
Address addressObject2 = // addressObject retrieval is out of scope in this example
ArrayList<Address> addressCollection = new ArrayList<Address>();
addressCollection.add( addressObject1 );
addressCollection.add( addressObject2 );

Backendless.Data.of( Person.class ).deleteRelation( personObject, "address", addressCollection,
        new AsyncCallback<Integer>()
        {
          @Override
          public void handleResponse( Integer response )
          {
            Log.i( "MYAPP", "relation has been deleted");
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
            Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );

Delete Relation with condition

API request must identify the child objects to delete from the relation implicitely through a whereClause condition.

Synchronous Method:

Integer result = Backendless.Persistence.of( "TABLE-NAME" ).deleteRelation( 
                                                   Map parentObject,
                                                   String relationColumnName,
                                                   String whereClause );
Integer result = Backendless.Persistence.of( E ).deleteRelation( 
                                                   E parentObject,
                                                   String relationColumnName,
                                                   String whereClause );

Asynchronous Method:

Backendless.Persistence.of( "TABLE-NAME" ).deleteRelation( 
                                             Map parentObject,
                                             String relationColumnName,
                                             String whereClause,
                                             AsyncCallback<Integer> callback );
Backendless.Persistence.of( E ).deleteRelation( 
                                   E parentObject,
                                   String relationColumnName,
                                   String whereClause,
                                   AsyncCallback;<Integer> callback );

where:

TABLE-NAME - Name of the table where the parent object is stored.
E - Java class of the parent object. The class name identifies the table where the parent object is stored.
parentObject - The object for which the relation with the identified children will be deleted. When this argument is an instance of java.util.Map (for the map-based approach), it must contain the "objectId" property.
relationColumnName - Name of the column identifying the relation. Relationship between the child objects identified by whereClause will be deleted for this column in parentObject.
whereClause - a where clause condition identifying the objects in the child table which will be removed from the relation to the parent object.
callback - a responder object which will receive a callback when the relation has been deleted or if an error occurs. Applies to the asynchronous method only.

Return Value:

Number of child objects for which the relationship has been deleted. The asynchronous call receives the return value through a callback executed on the AsyncCallback object.

Example:

The following request deletes a relation between a Person object and all the related objects in the related table identified by column "user" which match the provided query:

name='Joe' or name = 'Frank'

As a result of the operation, all related objects where the name property is either Joe or Frank will be deleted from the relation.

HashMap<String, Object> parentObject = new HashMap<String, Object>();
parentObject.put( "objectId", "41230622-DC4D-204F-FF5A-F893A0324800" );

Backendless.Data.of( "Person" ).deleteRelation( parentObject, 
                                                "user", 
                                                "name = \"Joe\" or name = \"Frank\"", 
                                                new AsyncCallback<Void>()
{
  @Override
  public void handleResponse( Void response )
  {
    Log.i( "MYAPP", "relation has been deleted" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
  }
} );
Person personObject = // personObject retrieval is out of scope in this example

Backendless.Data.of( Person.class ).deleteRelation( personObject, 
                                                    "user", 
                                                    "name = \"Joe\" or name = \"Frank\"",
        new AsyncCallback<Integer>()
        {
          @Override
          public void handleResponse( Integer response )
          {
            Log.i( "MYAPP", "relation has been deleted");
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
            Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );

Relations (Retrieve)

When a data object or a collection of objects is retrieved using the client API, Backendless does not automatically include related objects into the returned object hierarchy. The reason for this is it would make it very easy to load a lot of unnecessary data which could impact application's performance. There are multiple ways to retrieve related data:

Relations auto-load - a mechanism built-into Backendless Console for configuring specific relations to be included into responses with the parent objects.
Single-step relations retrieval - initializing and including related objects into the response when running a find query for parent objects.
Two-step relations retrieval - a process of retrieving relations where the first step is to load the parent object and the second step is to load specific relations for the given parent.
Loading with relations depth - retrieving related objects by specifying the depth of the object hierarchy
Relation paging API - enables "chunked" (or paged) retrieval of related objects
Inverted Retrieval - loading related child objects by condition applied to parent - load related objects using a search query where the condition applies to the parent object properties.

Auto Load

The auto-load feature is designed to be used for quick prototyping. One of its benefits is reduction of client-side code. However, for production quality apps, it is not recommended to use the auto-load relation retrieval as it can greatly impact performance.

By default when an object is retrieved from Backendless using any of the find APIs (basic or advanced), its related objects are not included into the response, unless explicitly referenced in the request. This behavior can be easily modified using Backendless Console:

auto-load

For any two tables A and B where A has a relationship column linking it to B, the console includes the "auto load" checkbox for the relationship column. Selecting the checkbox instructs Backendless to return a paged set of related B objects when the parent instance of A is retrieved through an API call. For example, in the image above, the shown table has a one-to-many "address" relationship with the Address table. When the "auto load" checkbox in the "address" column is selected, a paged set of the related Address objects will be included into the response for a find query for the shown table.

You can disable the auto-load option by setting the relation depth parameter to 0 in an API call.

Single Step Retrieval

Single Step Retrieval loads only a partial set of the related objects (default size of the retrieved related collection is 10). To load additional related objects, use the Relation Paging API.

This approach allows retrieval of a partial set of the related objects along with the parent object in a single find request. Each relationship property (column) must be uniquely identified by name using the API documented below.

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.addRelated( "RELATED-PROPERTY-NAME" )
queryBuilder.addRelated( "RELATED-PROPERTY-NAME.RELATION-OF-RELATION" );

Then load data using the constructed queryBuilder object with:

Synchronous call:
List<Map> collection = Backendless.Data.of( "TABLE-NAME" ).find( queryBuilder );
Asynchronous call:
Backendless.Data.of( "TABLE-NAME" ).find( queryBuilder, AsyncCallback<List<Map>> callback );
Synchronous call:
List<T> collection = Backendless.Data.of( T.class ).find( queryBuilder );
Asynchronous call:
Backendless.Data.of( T.class ).find( queryBuilder, AsyncCallback<List<T>> callback );

where

RELATED-PROPERTY-NAME - name of a related property to load. For example, if table Person has a relation "homeAddress" pointing to an object in the Address table, the value would be "homeAddress". The syntax allows to add relations of relations. For example, if the same Address table has a relation "country" pointing to the Country table, then "homeAddress.country" would instruct the related Country object to be loaded as well.
queryBuilder - an instance of the com.backendless.persistence.DataQueryBuilder class. The class is used to identify related properties for relation retrieval, paging, sorting and search with whereClause.
TABLE-NAME - Name of the table where the data should be retrieved from
T.class - Reference to a class which identifies the table from which the data should be loaded from.

 

Two-step Retrieval

Two Step Retrieval loads only a partial set of the related objects (default size of the retrieved related collection is 10). To load additional related objects, use the Relation Paging API.

With this approach a collection of related objects for a specific relation property in a parent object is retrieved from the server. The client application must know parent object's objectId. The API loads a collection of related children for one property at a time. Child objects retrieved in paged sets, see the Relation Paging API sectionfor additional details.

 

Suppose the Person table has a one-to-many relationship column friends pointing to the Users table. The code below retrieves related BackendlessUser objects  for a specific Person object:

Prepare LoadRelationsQueryBuilder:
LoadRelationsQueryBuilder<Map<String, Object>> loadRelationsQueryBuilder; 
loadRelationsQueryBuilder = LoadRelationsQueryBuilder.ofMap();
loadRelationsQueryBuilder.setRelationName( "friends" );
Synchronous call:
String parentObjectId = // removed for brevity
List<Map<String, Object>> friends;
friends = Backendless.Data.of( "Person" ).loadRelations( parentObjectId, 
                                                         loadRelationsQueryBuilder );
for( Map friend: friends )
  Log.i( "MYAPP", friend.get( "email" ));
Asynchronous call:
String parentObjectId = // removed for brevity
Backendless.Data.of( "Person" ).loadRelations( parentObjectId,
        loadRelationsQueryBuilder,
        new AsyncCallback<List<Map<String, Object>>>()
        {
          @Override
          public void handleResponse( List<Map<String, Object>> friends )
          {
             for( Map friend: friends )
               Log.i( "MYAPP", friend.get( "email" ));
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
             Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );
Prepare LoadRelationsQueryBuilder:
LoadRelationsQueryBuilder<BackendlessUser> loadRelationsQueryBuilder; 
loadRelationsQueryBuilder = LoadRelationsQueryBuilder.of( BackendlessUser.class );
loadRelationsQueryBuilder.setRelationName( "friends" );
Synchronous call:
String parentObjectId = // removed for brevity
List<BackendlessUser> friends;
friends = Backendless.Data.of( Person.class ).loadRelations( objectId, 
                                                             loadRelationsQueryBuilder );
for( BackendlessUser friend: friends )
  Log.i( "MYAPP", friend.getEmail() );
Asynchronous call:
String parentObjectId = // removed for brevity
Backendless.Data.of( "Person" ).loadRelations( objectId,
        loadRelationsQueryBuilder,
        new AsyncCallback<List<BackendlessUser>>()
        {
          @Override
          public void handleResponse( List<BackendlessUser> friends )
          {
             for( BackendlessUser friend: friends )
               Log.i( "MYAPP", friend.getEmail() );
          }

          @Override
          public void handleFault( BackendlessFault fault )
          {
             Log.e( "MYAPP", "server reported an error - " + fault.getMessage() );
          }
        } );

 

Retrieval with Relation Depth

The Data Service API supports a mechanism for loading related objects without identifying each by its name. Instead, the API includes a parameter which specifies the "depth" of the relations to include into the response. Consider the following diagram:

relation-depth

The diagram shows a hierarchy for class structure - the Order class has two relations: with OrderItem and Customer classes. Each in turn has a relation to the Manufacturer and Address classes. When an instance or a collection of Order objects is retrieved from Backendless, the API may include a parameter specifying the depth of relations to include into the response. If the relation depth is 1, then all related instances of OrderItem and Customer will be included into each Order object. If the relation depth is 2, then not only OrderItem and Customer instances will be included, but the corresponding Manufacturer and Address objects as well.

Loading relations with relation depth retrieves only a partial set of the related objects (default size of the retrieved related collection is 10). To load additional related objects, use the Relation Paging API.

API methods supporting relations depth

Synchronous methods:
public Map Backendless.Data.of( "TABLE-NAME" ).findFirst( int relationsDepth );
public Map Backendless.Data.of( "TABLE-NAME" ).findLast( int relationsDepth );
public Map Backendless.Data.of( "TABLE-NAME" ).findById( String objectId, int relationsDepth );
public List<Map> Backendless.Data.of( "TABLE-NAME" ).find( DataQueryBuilder queryBuilder );
Asynchronous methods:
public void Backendless.Data.of( "TABLE-NAME" ).findFirst( int relationsDepth,
                                                      AsyncCallback<Map> responder  );
public void Backendless.Data.of( "TABLE-NAME" ).findLast( int relationsDepth, 
                                                      AsyncCallback<Map> responder  );
public void Backendless.Data.of( "TABLE-NAME" ).findById( String objectId, int relationsDepth, 
                                                      AsyncCallback<Map> responder  );
public void Backendless.Data.of( "TABLE-NAME" ).find( DataQueryBuilder dataQuery, 
                                                      AsyncCallback<List<Map>> responder );
Synchronous methods:
public T Backendless.Data.of( T.class ).findFirst( int relationsDepth );
public T Backendless.Data.of( T.class ).findLast( int relationsDepth );
public T Backendless.Data.of( T.class ).findById( String objectId, int relationsDepth );
public List<T> Backendless.Data.of( T.class ).find( DataQueryBuilder queryBuilder );
Asynchronous methods:
public void Backendless.Data.of( T.class ).findFirst( int relationsDepth,
                                                      AsyncCallback<E> responder  );
public void Backendless.Data.of( T.class ).findLast( int relationsDepth, 
                                                      AsyncCallback<E> responder  );
public void Backendless.Data.of( T.class ).findById( String objectId, int relationsDepth, 
                                                      AsyncCallback<E> responder  );
public void Backendless.Data.of( T.class ).find( DataQueryBuilder queryBuilder, 
                                                      AsyncCallback<List<T>> responder );

Example:

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryOptions.setRelationsDepth( 2 );

// synchronous call
List<Map> collection = Backendless.Data.of( "Foo" ).find( queryBuilder );

// asynchronous call
Backendless.Data.of( "Foo" ).find( queryBuilder, 
                            new AsyncCallback<List<Map>>()
    {
      @Override
      public void handleResponse( List<Map> response )
      {

      }

      @Override
      public void handleFault( BackendlessFault fault )
      {

      }
    });
DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryOptions.setRelationsDepth( 2 );

// synchronous call
List<Foo> collection = Backendless.Data.of( Foo.class ).find( queryBuilder );

// asynchronous call
Backendless.Data.of( Foo.class ).find( queryBuilder, 
                   new AsyncCallback<List<Foo>>()
{
  @Override
  public void handleResponse( List<Foo> response )
  {

  }

  @Override
  public void handleFault( BackendlessFault fault )
  {

  }
});

 

Relation Paging

API operations described above provide a way to retrieve a limited (initial) set of related data. In most real world scenarios, there may be more related objects than returned by Single Step, Two Step or Relation Depth Retrieval APIs. To obtain a complete set of related objects Backendless supports Relation Paging API described below.

1. To understand the concept of paged data retrieval, see the Data retrieval with Paging section of the documentation.
2. When working with paging, it may be important to know the total size of the related collection. See the Get Object Count section for details on how to get the related objects count.

Android and Java applications must use com.backendless.persistence.LoadRelationsQueryBuilder which facilitates retrieval of related objects for a parent object. The class is responsible for maintaining the page size and offset parameters used in the paging process.

 

Before an instance of LoadRelationsQueryBuilder can be used in a data retrieval request, it must be initialized as shown below. Notice the setRelationName call which identifies a related column for which the related objects will be loaded:

LoadRelationsQueryBuilder<Map<String, Object>> loadRelationsQueryBuilder;
loadRelationsQueryBuilder = LoadRelationsQueryBuilder.ofMap();
loadRelationsQueryBuilder.setRelationName( "NAME-OF-RELATED-COLUMN" );
LoadRelationsQueryBuilder<CHILDCLASS> loadRelationsQueryBuilder;

// Generic reference to CHILDCLASS is needed so that related objects
// from the servers are returned to the client as instances of CHILDCLASS.
loadRelationsQueryBuilder = LoadRelationsQueryBuilder.of( CHILDCLASS.class );
loadRelationsQueryBuilder.setRelationName( "NAME-OF-RELATED-COLUMN" );

where:

CHILDCLASS - Used in the "Custom Class" approach only. A Java class which identifies the related table.
NAME-OF-RELATED-COLUMN - Name of the related column in the parent table for which the related objects will be retrieved.

 

Once a query builder object is created, the following API can be used to retrieve a page of related objects:

// asynchronous API
Backendless.Data.of( "PARENT-TABLE-NAME" ).loadRelations( 
                            String parentObjectId,
                            LoadRelationsQueryBuilder queryBuilder,
                            AsyncCallback<List<Map<String, Object>>> callback );

// synchronous API
List<Map<String, Object>> relatedObjects;
relatedObjects = Backendless.Data.of( "PARENT-TABLE-NAME" ).loadRelations( 
                            String parentObjectId,
                            LoadRelationsQueryBuilder queryBuilder );
// asynchronous API
Backendless.Data.of( PARENT-CLASS ).loadRelations( 
                      LoadRelationsQueryBuilder<List<Map<String, Object>>> queryBuilder,
                      AsyncCallback<List<CHILD-CLASS>> callback );

// synchronous API
List<CHILD-CLASS> relatedObjects;
relatedObjects = Backendless.Data.of( PARENT-CLASS ).loadRelations( 
                      LoadRelationsQueryBuilder<CHILD-CLASS queryBuilder );

where:

parentObjectId -  id of the object for which the related objects will be retrieved.
PARENT-TABLE-NAME - name of the table which contains the parent object identified by parentObjectId.
queryBuilder - LoadRelationsQueryBuilder initialized as shown above. Used in subsequent calls to request additional pages of related data objects.
callback - a responder object which will receive a callback when a page of related objects is retrieved from the server. Applies to the asynchronous method only.

 

The LoadRelationsQueryBuilder class contains methods which recalculate offset in order to obtain next, previous or a specific page. After loadRelationsQueryBuilder is modified, call the API shown above to get next, previous or a specific page of data.

LoadRelationsQueryBuilder loadRelationsQueryBuilder = // initialized as shown above

// sets page size which will be used in subsequent related object retrieval.
loadRelationsQueryBuilder.setPageSize( int pageSize );

// calculates offset of the next page based on set page size. 
loadRelationsQueryBuilder.prepareNextPage();

// calculates offset of the previous page based on set page size
loadRelationsQueryBuilder.preparePreviousPage();

// sets offset explicitly
loadRelationsQueryBuilder.setOffset( int offset );

Inverted Relation Retrieval

Backendless supports a special query syntax for loading a subset of child objects for a specific parent. Consider the following table schemas:

 

PhoneBook table:

phonebook-table-schema

 

Contact table:

contact-table-schema

 

Address table:

address-table-schema

These tables will be used to demonstrate how to query Backendless for conditional related object retrieval.

The Map-based approach does not require you defining classes for the objects stored in Backendless. Instead your code can use java.util.Map to to store and retrieve objects in/from Backendless.
Consider the following class definitions for the entities from the diagram: PhoneBook class:
package com.sample.entity;

import java.util.List;

public class PhoneBook
{
  private String objectId;
  private Contact owner;
  private List<Contact> contacts;

  public String getObjectId()
  {
    return objectId;
  }

  public void setObjectId( String objectId )
  {
    this.objectId = objectId;
  }

  public Contact getOwner()
  {
    return owner;
  }

  public void setOwner( Contact owner )
  {
    this.owner = owner;
  }

  public List<Contact> getContacts()
  {
    return contacts;
  }

  public void setContacts( List<Contact> contacts )
  {
    this.contacts = contacts;
  }
}
Contact class:
package com.sample.entity;

import java.util.Date;

public class Contact
{
  private String objectId;
  private String name;
  private int age;
  private String phone;
  private String title;
  private Address address;
  private Date updated;

  public String getObjectId()
  {
    return objectId;
  }

  public void setObjectId( String objectId )
  {
    this.objectId = objectId;
  }

  public String getName()
  {
    return name;
  }

  public void setName( String name )
  {
    this.name = name;
  }

  public int getAge()
  {
    return age;
  }

  public void setAge( int age )
  {
    this.age = age;
  }

  public String getPhone()
  {
    return phone;
  }

  public void setPhone( String phone )
  {
    this.phone = phone;
  }

  public String getTitle()
  {
    return title;
  }

  public void setTitle( String title )
  {
    this.title = title;
  }

  public Address getAddress()
  {
    return address;
  }

  public void setAddress( Address address )
  {
    this.address = address;
  }

  public Date getUpdated()
  {
    return updated;
  }

  public void setUpdated( Date updated )
  {
    this.updated = updated;
  }
}
Address class:
package com.sample.entity;

public class Address
{
  private String street;
  private String city;
  private String state;

  public String getStreet()
  {
    return street;
  }

  public void setStreet( String street )
  {
    this.street = street;
  }

  public String getCity()
  {
    return city;
  }

  public void setCity( String city )
  {
    this.city = city;
  }

  public String getState()
  {
    return state;
  }

  public void setState( String state )
  {
    this.state = state;
  }
}

 

The general structure of a whereClause query to load a collection of child objects for a specific parent object is:

ParentTableName[ relatedColumnName ].parentColumnName COLUMN-VALUE-CONDITION

Both columns relatedColumnName and parentColumnName must be declared in a table with name of ParentTableName. The relatedColumnName must be a relation column. The table relatedColumnName points to is the table where the objects must be loaded from.  The examples below demonstrate the usage of this syntax:

Find all contacts in a city for a specific phone book:

StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ).append( "'" );
whereClause.append( " and " );
whereClause.append( "address.city = 'Smallville'" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "address.city = 'Smallville'" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

Find all contacts for the specific phone book where the city name contains letter 'a':

StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ) ).append( "'" );
whereClause.append( " and " );
whereClause.append( "address.city like '%a%'" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "address.city like '%a%'" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

Find all contacts where age is greater than 20 for a specific phone book:

StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ) ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age > 20" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age > 20" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

Find all contacts for a specific phone book where age is within the specified range:

StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ) ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age >= 21 and age <= 30" )

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age >= 21 and age <= 30" )

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

Find all contacts for a specific phone book where age is greater than 20 and the city is Tokyo:

StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.get( "objectId" ) ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age > 20 and address.city = 'Tokyo'" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Map> result = Backendless.Data.of( "Contact" ).find( queryBuilder );
StringBuilder whereClause = new StringBuilder();
whereClause.append( "PhoneBook[contacts]" );
whereClause.append( ".objectId='" ).append( savedPhoneBook.getObjectId() ).append( "'" );
whereClause.append( " and " );
whereClause.append( "age > 20 and address.city = 'Tokyo'" );

DataQueryBuilder queryBuilder = DataQueryBuilder.create();
queryBuilder.setWhereClause( whereClause.toString() );
List<Contact> result = Backendless.Data.of( Contact.class ).find( queryBuilder );

Relations with Geo Points

Backendless Geo Service manages application's geo location data and provides APIs to work with Geo points. Backendless supports integration between data objects managed by Data Service and geo points for the scenarios when a logical connection between the two entity types must exist in an application. For instance, in a taxi ordering application a data object may represent a taxi car, while a geo point represents its location on the map. Linking the two entity types together provides great benefits such as retrieving both objects at once and managing as a consistent, cohesive object hierarchy.

The Data-to-Geo integration is implemented through object relations. Data table schema may declare a table column with a special data type - "GeoPoint Relationship". As a result, the data objects in the table may contain a reference to one or more GeoPoints. When a data object with a related GeoPoint is saved, Backendless persists information about both the data object and the geo point in the corresponding persistent systems and sets up the relationship. Likewise, when a data object is retrieved by using the API, any related geo points can be retrieved using the same principle for loading data relations. The data-to-geo relation is bidirectional, it means a geo point may reference a data object in its metadata. You can learn more about it in the Relations with Data Objects section of the Geolocation documentation.

The relationship between a data object and a geo point (or a collection of) can be established by using either the "code first" or the "schema first" approaches. With the former, the relationship is determined by the data structure persisted with the API. If a data object references a GeoPoint (or a collection of) in one of its properties, Backendless interprets it as a relation and, as a result, will create a relation column in the data table schema. With the latter ("schema first") approach, application developer can declare a relationship in the data table schema first. In either one of these approaches, once a relationship is declared, data objects and geo points may be linked together by using the Backendless console as well.

 

This chapter consists of the following sections:

Declaring a Data-to-Geo Relationship in Table Schema

Linking a Data Object with Geo Points

Update/Delete Relations

Deleting Relation Column in Table Schema

Establishing Relations with Geo Points via API

Declaring a Data-to-Geo Relationship in Table Schema


To declare a relationship in a data table schema:

1. Select a table where a relation column should be declared.
2. Click the Schema menu.
3. Click the New button. Enter a name for the new column in the Name field. This column will represent a data-to-geo relationship.
4. Select the Geopoint Relationship option from the Type drop-down list.
geo-point-relationship-type
5. Select the constraints and/or cardinality of the relation from the corresponding drop-down menus.
 
Constraints:
There are two available constraints: "Not Null (Required)" and "Unique Value". The former (NN) establishes the column as required. It means when a new object is created or an existing one is updated, server will be expecting a value for the column. The latter constraint (UQ) will enforce a rule that the column must contain a unique value, thus no two objects will be able to contain the same GeoPoint.
 
Cardinality:
The one-to-one relation means that a table's object can be linked with only one geo point, while the one-to-many relation means that a table's object can be linked with multiple geo points.
location-column
6. Click the CREATE button to save the changes.

Linking a Data Object with Geo Points


Once a data-to-geo relationship column is declared, data objects from the table can be linked to geo point(s) as described below:

1. Click the name of the table containing an object you want to link with a geo point.
2. Table columns representing the data-to-geo relationships are identified as "GEOPOINT relationship" in the header row. The cardinality of the relation is visualized as one red line for the one-to-one relations and three red lines for the one-to-many relations:
geopoint-column.zoom70
3. To create a relationship between an object and a geopoint, click the plus icon next in the cell for a GEOPOINT column.
plus-icon-for-geopoint.zoom70
4. The Set Related GeoPoint pop-up window will display the list of the geo points. Use the Geo Category drop-down list to select a geo category from which the points should be displayed. Additionally, you can use the search bar to locate a geopoint by its metadata:
geopoint-search.zoom80
5. If you declared a one-to-one relation for a table the object belongs to, you will be able to link this object with only one geo point (by the means of a radio button). If it is a one-to-many relationship, the interface uses check boxes, which allow for multiple selection. Click a radio-button or select check-boxes next to the geo points which you want to link with the data object.
6. Click the ADD RELATION button to save the changes.

Once a relation is established, it is shown in the data browser as a hyperlink. The hyperlink for the one-to-one relations displays the coordinates of the related geo point. For the one-to-many relations the link says "multiple Geopoints". In both cases, the link opens the Geolocation screen of the console which displays the related geo point(s). Additionally, the link has a mouse-over preview for the related geopoints:

geopoint-preview

Update/Delete Relations


To update a data-to-geo relation, use the same plus icon that opened the popup to create a geopoint relation. The popup tracks the selection changes and allows update or deletion of the relation.

Deleting Relation Column in Table Schema


A data-to-geo relationship can be removed at the schema level. When a relationship column is removed, the "links" between the corresponding data objects and geopoints are deleted, however, it does not remove the objects themselves.

To delete a relationship definition between a data table and the geo points:

1. Click the name of the table which contains a "GeoPoint relationship" column you need to remove.
2. Click the Schema menu
3. Click the check-box next to the column you need to delete.
4. Click the Delete menu.
delete-geopoint-relation-column

Establishing Relations with Geo Points via API


Creating a relationship between a data object and a geo point (or a collection of) uses the same API as saving a data object with a related entity. In the case of data-to-geo relations, the related entity is a geopoint or a collection of geopoints. Consider the example that below saves a data object with a related geopoint. The geopoint is also persisted in the Geo Service:

 

The example below demonstrates how to link a taxi (a data object) with its location (geo point). First, declare the TaxiCab class:

The Map-based approach does not require you defining classes for the objects stored in Backendless. Instead your code can use java.util.Map to to store and retrieve objects in/from Backendless.
public class TaxiCab
{
  public String carmake;
  public String carmodel;
  public GeoPoint location;
  public List<GeoPoint> previousDropOffs;

  public String getCarmake()
  {
    return carmake;
  }

  public void setCarmake( String carmake )
  {
    this.carmake = carmake;
  }

  public String getCarmodel()
  {
    return carmodel;
  }

  public void setCarmodel( String carmodel )
  {
    this.carmodel = carmodel;
  }

  public GeoPoint getLocation()
  {
    return location;
  }

  public void setLocation( GeoPoint location )
  {
    this.location = location;
  }

  public List<GeoPoint> getPreviousDropOffs()
  {
    return previousDropOffs;
  }

  public void setPreviousDropOffs( List<GeoPoint> previousDropOffs )
  {
    this.previousDropOffs = previousDropOffs;
  }
}

 

To set one-to-one or one-to-may relations:

The code uses synchronous API - when executing on Android, make sure to run it on a non-UI thread, or change the code to use the asynchronous API.

GeoPoint point = new GeoPoint();
point.setLatitude( 40.7148 );
point.setLongitude( -74.0059 );
point.addCategory( "taxi" );
point.addMetadata( "service_area", "NYC" );
point = Backendless.Geo.savePoint( point );

List<GeoPoint> previousDropOffs = new ArrayList<GeoPoint>();

GeoPoint dropOff1 = new GeoPoint( 40.757977, -73.98557 );
dropOff1.addMetadata( "name", "Times Square" );
dropOff1.addCategory( "DropOffs" );
dropOff1 = Backendless.Geo.savePoint( dropOff1 );
previousDropOffs.add( dropOff1 );

GeoPoint dropOff2 = new GeoPoint( 40.748379, -73.985565 );
dropOff2.addMetadata( "name", "Empire State Building" );
dropOff2.addCategory( "DropOffs" );
dropOff2 = Backendless.Geo.savePoint( dropOff2 );
previousDropOffs.add( dropOff2 );

TaxiCab taxi = new TaxiCab();
taxi.carmake = "Toyota";
taxi.carmodel = "Prius";
taxi = Backendless.Data.of( TaxiCab.class ).save( taxi );

ArrayList<GeoPoint> location = new ArrayList<GeoPoint>();
location.add( point );
// create a one to one relation between the data object and a geopoint
Backendless.Data.of( TaxiCab.class ).setRelation( taxi, "location:GeoPoint:1", location );

// link several points to data object
Backendless.Data.of( TaxiCab.class ).setRelation( taxi, "previousDropOffs:GeoPoint:n", previousDropOffs );
GeoPoint point = new GeoPoint();
point.setLatitude( 40.7148 );
point.setLongitude( -74.0059 );
point.addCategory( "taxi" );
point.addMetadata( "service_area", "NYC" );
point = Backendless.Geo.savePoint( point );

List<GeoPoint> previousDropOffs = new ArrayList<GeoPoint>();

GeoPoint dropOff1 = new GeoPoint( 40.757977, -73.98557 );
dropOff1.addMetadata( "name", "Times Square" );
dropOff1.addCategory( "DropOffs" );
dropOff1 = Backendless.Geo.savePoint( dropOff1 );
previousDropOffs.add( dropOff1 );

GeoPoint dropOff2 = new GeoPoint( 40.748379, -73.985565 );
dropOff2.addMetadata( "name", "Empire State Building" );
dropOff2.addCategory( "DropOffs" );
dropOff2 = Backendless.Geo.savePoint( dropOff2 );
previousDropOffs.add( dropOff2 );

Map taxi = new HashMap();
taxi.put( "carmake", "Toyota" );
taxi.put( "carmodel", "Prius" );
taxi = Backendless.Data.of( "TaxiCab" ).save( taxi );

ArrayList<GeoPoint> location = new ArrayList<GeoPoint>();
location.add( point );
// create a one to one relation
Backendless.Data.of( "TaxiCab" ).setRelation( taxi, "location:GeoPoint:1", location );

// link several points to data object
Backendless.Data.of( "TaxiCab" ).setRelation( taxi, "previousDropOffs:GeoPoint:n", previousDropOffs );

After you run the code, you will see the following data object and geopoints created in the application:

Data object:

taxi-data-object.zoom70

Geopoints:

taxi-geo-points.zoom70

Data Security

Data Service supports a very flexible security mechanism for restricting access to objects stored in Backendless. Security permissions apply to users and roles. A permission can either grant or reject an operation for a particular asset. In the context of Data Service, the asset is an object which your app can retrieve, update or delete. Permissions can be granted or rejected globally, where they apply to all tables and all objects in the data store. Additionally, every table may have its own permission matrix and owner policy - a special instruction whether object owners can or cannot retrieve/update/delete the objects they 'own'. Finally, every object has its own Access Control List (ACL) which is a matrix of permissions for the operations applicable specifically to the object:

backendless-security-components

The security system is multi-layered. For any API call the system goes through several layers where each can trim the scope of the operations. The layered order of the decision making is important and consists of the following points of validation:

1. ObjectACL for the user who makes the call
2. ObjectACL for user-defined roles assigned to the user who makes the call.
3. Table permissions for the User account
4. Table permissions for the user-defined roles
5. Owner Policy
6. ObjectACL for system roles
7. Table permissions for system-level roles
8. Global user-defined roles
9. Global system roles

Where:

"User-defined roles" - roles created by the application developer.
"System roles" - roles built into Backendless (AuthenticatedUser, NonAuthenticatedUser, SocialUser, ServerCodeUser, etc).

 

Consider the following guide which illustrates the decision making process:

Backend receives an API request to load data from a table (the Find operation). For the sake of the example, user's identity is "bob@backendless.com" and the user belongs to a custom role called "MyRole".
 
All objects in the table become candidates for the retrieval. Backendless goes through the security permissions layers to determine which objects must be included into the response.
 

1. [LAYER 1] ObjectACL for the user who makes the call.
Backendless checks if there are any restrictions for the user account at the object level. Any object in the collection with ACL which rejects access to the user is excluded from the result. To see or modify the permissions for a particular object, click the 'lock' icon in the ACL column in the data browser in management console to see and manage Object ACL permissions for a specific object.
object-acl-access.zoom80
 
2. [LAYER 2] ObjectACL for user-defined roles assigned to the user who makes the call.
This is the same check as the one above, except Backendless looks into the permissions for the roles defined by the application developer. If the user who made the call belongs to any of the custom roles, Backendless checks if these roles are allowed to perform the current operation for every object in the collection. In the screenshot below, only the "MyRole" role will be checked in this step, since this is the only custom role in the application:
object-acl-custom-role.zoom80
 
3. [LAYER 3] Table permissions for the User account.
Every table in Backendless may have its own set of permissions for users and roles. At this point Backendless checks if the currently logged in user is allowed to run the current operation. For example, if the Find operation is denied for the user, no objects would be returned. To see the permissions for a user account in Backendless Console, select a table and click PERMISSIONS, then USER PERMISSIONS menu:
table-permissions-for-users.zoom80

 

4. [LAYER 4] Table permissions for the user-defined roles.
This step is identical to the one described above with the exception that is checks custom roles for the table. Since this guide reviews the decision making process for the Find operation, Backendless checks the column for Find. If any of the custom roles which the user belongs to deny access, the operation is rejected and no data is returned.
custom-user-roles-for-table.zoom80
 
5. [LAYER 5] Owner Policy.
When a new object is created in Backendless, the system automatically links it with the account of the user that made the call to save the object. You can see that information in the 'ownerId' column in any of your tables in the data browser.  With the association between objects and users (owners), Backendless provides a way to control whether users can get access to the data they created. This is done through a concept we call 'Owner Policy'. To navigate to Owner Policy, select a table in the data browser and click the PERMISSIONS menu. Click the OWNER POLICY menu item.
Granting a permission for an operation in Owner Policy, guarantees that the objects owned by the current user will be allowed access for the specified operations. Denying a permission, takes out the 'owned' objects from the collection of candidate objects to return.
 
6. [LAYER 6] Object ACL for system roles.
This check is identical to step 2 ([LAYER 2] Object ACL for custom roles). The difference is the system roles cover larger groups of users. For example, this step would make possible to restrict access to specific objects for all authenticated (or not authenticated) users, yet the object would be returned with a query made by the object's owner if the Owner Policy (previous step) grants access.  An exception to this rule are API calls from business logic. In that case, if there is no authenticated user in the context of the call, Backendless assigns only ServerCodeUser role. The NotAuthenticatedUser role is not assigned and thus is not checked.
 
7. [LAYER 7] Table permissions for system roles.
Identical to step 4 (table permissions for custom roles), this checks if any of the system roles reject the operation at the table level.
 
8. [LAYER 8] Global custom roles.
Global policy applies to all tables and objects. By default all table level permissions inherit from the global policy. You can configure in the console at: Users > Security Roles. Create a new role and click it to configure the permission matrix:
custom-role-global-matrix.zoom70

Permissions API

Every data object in Backendless has its own access control list (ACL) - a matrix of operations and principals (application's users or roles). An intersection of an operation and a principal contains a permission which determines whether the principal has the right to execute the operation. These permission could be either grant or deny. Backendless console provides an easy to understand way to see and manage these permissions. For example, the screenshot below demonstrates an ACL matrix for an object. Notice the intersection of a column for the Create operation and the AuthenticatedUser role. The cell contains a green checkmark icon representing that the permission is granted:

permission-matrix

In addition to managing the ACL permissions with Backendless Console there is also Permissions API:

To grant access for a user

Backendless.Data.Permissions.FIND.grantForUser( userid, dataObject, responder );
Backendless.Data.Permissions.REMOVE.grantForUser( userid, dataObject, responder );
Backendless.Data.Permissions.UPDATE.grantForUser( userid, dataObject, responder );

where:

userid - a user ID, for which you want to grant a permission.
dataObject - a data object for which you want to grant the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To grant access for a user role

Backendless.Data.Permissions.FIND.grantForRole( rolename, dataObject, responder );
Backendless.Data.Permissions.REMOVE.grantForRole( rolename, dataObject, responder );
Backendless.Data.Permissions.UPDATE.grantForRole( rolename, dataObject, responder );

where:

rolename -  a role name, for which you want to grant a permission.
dataObject - a data object for which you want to grant the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To grant access for all users

Backendless.Data.Permissions.FIND.grantForAllUsers( dataObject, responder );
Backendless.Data.Permissions.REMOVE.grantForAllUsers( dataObject, responder );
Backendless.Data.Permissions.UPDATE.grantForAllUsers( dataObject, responder );

where:

dataObject - a data object for which you want to grant the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To deny access for a user

Backendless.Data.Permissions.FIND.denyForUser( userid, dataObject, responder );
Backendless.Data.Permissions.REMOVE.denyForUser( userid, dataObject, responder );
Backendless.Data.Permissions.UPDATE.denyForUser( userid, dataObject, responder );

where:

userid - a user ID, for which you want to deny a permission.
dataObject - a data object for which you want to deny a permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To deny access for a user role

Backendless.Data.Permissions.FIND.denyForRole( rolename, dataObject, responder );
Backendless.Data.Permissions.REMOVE.denyForRole( rolename, dataObject, responder );
Backendless.Data.Permissions.UPDATE.denyForRole( rolename, dataObject, responder );

where:

rolename - a role name, for which you want to deny a permission.
dataObject - a data object for which you want to grant the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To deny access for all users

Backendless.Data.Permissions.FIND.denyForAllUsers( dataObject, responder );
Backendless.Data.Permissions.REMOVE.denyForAllUsers( dataObject, responder );
Backendless.Data.Permissions.UPDATE.denyForAllUsers( dataObject, responder );

where:

dataObject - a data object for which you want to deny the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

 

Messaging API

Overview

Data Messaging is an essential function of mobile and desktop applications. It can be used for a multitude of functions including chat or private messaging, system update broadcast, maintaining game scores, etc. The Backendless Messaging Service provides API and software infrastructure enabling publish-subscribe message exchange pattern and mobile push notifications. The service consists of the following core concepts: channels, publishers, subscribers and registered devices:

channel        -  a logical medium "transporting" the messages.
publisher        - a program using the Publishing API to send messages to a channel.
subscriber        - a program using the Subscription API to receive messages from a channel.
registered device        - a mobile device registered with a Backendless channel to receive push notifications.

 

Publish-Subscribe Messaging

With the publish-subscribe pattern, one part of the code (or an entire application) can subscribe to receive messages and another publishes messages. A message can be any data - Backendless supports messages of primitive or complex data types. To enable publish-subscribe messaging, Backendless supports the concept of a channel. Subscriptions are "attached" to a channel (or multiple channels) and messages are published into a channel. By default Backendless sends all messages published into a channel to all the channel's subscribers. However, a subscription can include message filters, in this case Backendless delivers only the messages matching the filter.

 

Push Notifications

A message published to a channel can be tagged as a push notification, thus triggering the logic for distributing it to the registered devices. Backendless supports push notifications for iOS, Android and Windows Phone devices. Messages published as push notifications can target either a specific subscriber (as a device) or a group of subscribers. Subscribers can be grouped by operating system (for example, a message sent to all registered iOS devices) or as a collection of individual registrations. The Backendless messaging API supports different types of push notifications - badge updates, alerts, etc.

Sending Email

Backendless provides API for email delivery on behalf of your application. Before the API can be used, the Backendless backend must be configured with your own SMTP server information. This is an important requirement as the API will not work if the Manage > App Settings > Email Settings section in Backendless Console contains default values.

 

Configuration


To configure the backend:

 

1. Login to Backendless Console.
2. Select an app.
3. Click Manage, then scroll down to Email Settings on the App Settings screen.
4. Fill out the form with the SMTP server information
email-settings.zoom50

 
where:

SMTP Server - Hostname or public IP address of the server where the SMTP server is running.
Port - The port number the SMTP server accepts requests on.
From - The Name which will appear in the From field of the sent out emails.
User ID - The user id or email address for the SMTP server connection authentication
Password - The password for the SMTP server connection authentication.
Security - Choose between SSL or TLS connection.

 

Make sure to click Test before saving any configuration changes. The Reset button discards any unsaved changes.

 

Sending Email API


Delivers an email message using current server-side email settings to the recipient specified in the API call.

 

// *******************************************
// synchronous methods
// *******************************************
Backendless.Messaging.sendTextEmail( String subject, 
                                     String messageBody, 
                                     List<String> recipients )
Backendless.Messaging.sendTextEmail( String subject, 
                                     String messageBody, 
                                     String recipient )
Backendless.Messaging.sendHTMLEmail( String subject, 
                                     String messageBody, 
                                     List<String> recipients )
Backendless.Messaging.sendHTMLEmail( String subject, 
                                     String messageBody, 
                                     String recipient )
Backendless.Messaging.sendEmail( String subject, 
                                 BodyParts bodyParts, 
                                 String recipient, 
                                 List<String> attachments )
Backendless.Messaging.sendEmail( String subject, 
                                 BodyParts bodyParts, 
                                 String recipient )
Backendless.Messaging.sendEmail( String subject, 
                                 BodyParts bodyParts, 
                                 List<String> recipients, 
                                 List<String> attachments )

// *******************************************
// asynchronous methods
// *******************************************
Backendless.Messaging.sendTextEmail( String subject, 
                                     String messageBody, 
                                     List<String> recipients, 
                                     AsyncCallback<Void> responder );
Backendless.Messaging.sendTextEmail( String subject, 
                                     String messageBody, 
                                     String recipient, 
                                     AsyncCallback<Void> responder );
Backendless.Messaging.sendHTMLEmail( String subject, 
                                     String messageBody, 
                                     List<String> recipients, 
                                     AsyncCallback<Void> responder );
Backendless.Messaging.sendHTMLEmail( String subject, 
                                     String messageBody, 
                                     String recipient, 
                                     AsyncCallback<Void> responder );
Backendless.Messaging.sendEmail( String subject, 
                                 BodyParts bodyParts, 
                                 String recipient, 
                                 List<String> attachments, 
                                 AsyncCallback<Void> responder );
Backendless.Messaging.sendEmail( String subject, 
                                 BodyParts bodyParts, 
                                 String recipient, 
                                 AsyncCallback<Void> responder );
Backendless.Messaging.sendEmail( String subject, 
                                 BodyParts bodyParts, 
                                 List<String> recipients, 
                                 List<String> attachments, 
                                 AsyncCallback<Void> responder );

where:

subject - email message subject.
messageBody - plain text or HTML body of the email message.
bodyParts - an instance of com.backendless.messaging.BodyParts class which contains either plain text and/or HTML version of the message body.
recipient - email address to deliver the email message to.
recipients - a collection of email addressed to deliver the email message to.
attachments - an array of file paths for the file entries from the Backendless File Service. Referenced files will be attached to the email message. The path is calculated from the root of the file system (as it is seen in File Service browser in Backendless console) without the leading slash. For example, if file agreement.txt is located at /documents/legal/, then the path in the API call must be "documents/legal/agreement.txt".
responder - the callback used for asynchronous calls to indicate that the operation has either successfully completed or resulted in error.

 

Example:

// **************************************************************
// Async API request
// **************************************************************
AsyncCallback<Void> responder = new AsyncCallback<Void>()
{
  @Override
  public void handleResponse( Void response )
  {
    Log.i( "MYAPP", "[ASYNC] email has been sent" );
  }

  @Override
  public void handleFault( BackendlessFault fault )
  {
    Log.e( "MYAPP", "error sending email - " + fault.getMessage() );
  }
};

Backendless.Messaging.sendTextEmail( "Reminder", 
                                     "Hey JB! Your car will be ready by 5pm", 
                                     "james.bond@mi6.co.uk", 
                                     responder );

// **************************************************************
// Sync API request. HTML messahe to multiple recipients
// **************************************************************
ArrayList<String> recipients = new ArrayList<String>();
recipients.add( "mom@gmail.com" );
recipients.add( "dad@gmail.com" );

String mailBody = "Guys, the dinner last night was <b>awesome</b>";

Backendless.Messaging.sendHTMLEmail( "Dinner", mailBody, recipients );

Log.i( "MYAPP", "[SYNC] email has been sent" );

 

Push Notifications

Push Notification Setup (Android)

Backendless can deliver published messages as push notifications to Android devices. Additionally, Backendless Console can be used to publish push notifications. In order to deliver a push notification to Android, the backend must be configured with Google API Key:

2. Click the Get started. Pick a platform button.
server-key-step1
3. Click Enable services for my Android App:
server-key-step2
4. Select your Android app from the list. if the app is not in the list, enter the name of the app, it will be automatically registered in the Google Developer Console.
5. Enter Android package name of your app.
6. Click Continue to Choose and configure services.
server-key-step3
7. Click Cloud Messaging and then click Enable Google Cloud Messaging:
server-key-step4
8. Google generates API key and sender ID. Copy Server API Key:
generated-ids
9. Open Backendless Console and select your application.
10. Click Manage and scroll down to Mobile Settings.
11. Paste the Google API Key into corresponding field located under the Android Push Notifications label:
adding-android-key.zoom50
12. Click Save. At this point the backend is configured and is ready to publish push notifications to Android devices.

 

Google Project Number (Sender ID)

In your project you should register the device in order to receive or send push notifications. To accomplish this, do the following:

1. Return to Step 8 in the instructions above.
2. Copy Sender ID.
3. Use the Sender ID value in Backendless.Messaging.registerDevice(...) method as GCMSenderID argument. For example:
google-project-number-in-code

Manifest Configuration

Android applications must also include special configuration into the application manifest file to enable support for Backendless Push Notifications:

1. Add the following permissions:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="[APP PACKAGE NAME].permission.C2D_MESSAGE"/>
<permission android:name="[APP PACKAGE NAME].permission.C2D_MESSAGE" 
               android:protectionLevel="signature"/>

where [APP PACKAGE NAME] is the top level package name, it must be the same value as the package attribute in the manifest root element.

2. Add receiver declaration into the <application> element in the manifest file:
<receiver android:name="com.backendless.push.BackendlessBroadcastReceiver" 
             android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
    <action android:name="com.google.android.c2dm.intent.REGISTRATION"/>
    <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
    <category android:name="[APP PACKAGE NAME]"/>
  </intent-filter>
</receiver>
<service android:name="com.backendless.push.BackendlessPushService" />

where [APP PACKAGE NAME] is the top level package name, it must be the same value as the package attribute in the manifest root element.

Receiver is responsible for processing incoming push notifications.

 

Notice the value for the android:name attribute in the <receiver> and <service> elements - The receiver class is com.backendless.push.BackendlessBroadcastReceiver. While the service is com.backendless.push.BackendlessPushService. These are the default implementations. They contain the logic for processing push notifications and displaying them in the device's Notification Center. The implementation can be extended in order to get access to the published messages and/or handle push notifications in a custom way.

 

Custom handling of push notifications requires an implementation of two classes. First create a class which extends com.backendless.push.BackendlessPushService. Your implementation can override each of the four methods which correspond to various GCM actions.

Notice the comment in the onMessage method. The implementation of the method in the BackendlessPushService class contains the logic of displaying push notifications in the Android Notification Center. To cancel the default handling of the push notification, make sure your code returns false.

package com.myPackage;

import com.backendless.push.BackendlessPushService;

public class MyPushService extends BackendlessPushService
{

  @Override
  public void onRegistered( Context context, String registrationId )
  {
    Toast.makeText( context, 
                    "device registered" + registrationId, 
                    Toast.LENGTH_SHORT).show();
  }

  @Override
  public void onUnregistered( Context context, Boolean unregistered )
  {
    Toast.makeText( context, 
                    "device unregistered", 
                    Toast.LENGTH_SHORT).show();
  }

  @Override
  public boolean onMessage( Context context, Intent intent )
  {
    String message = intent.getStringExtra( "message" );
    Toast.makeText( context, 
                    "Push message received. Message: " + message, 
                    Toast.LENGTH_LONG ).show();
    // When returning 'true', default Backendless onMessage implementation 
    // will be executed. The default implementation displays the notification 
    // in the Android Notification Center. Returning false, cancels the 
    // execution of the default implementation.
    return false;
  }

  @Override
  public void onError( Context context, String message )
  {
    Toast.makeText( context, message, Toast.LENGTH_SHORT).show();
  }
}

Once a custom service class is defined, it has to be registered with GCM. Backendless handles it through a custom implementation of broadcast receiver. Create a class that extends com.backendless.push.BackendlessBroadcastReceiver. Your implementation must override the getServiceClass() method which returns the Class object of the service implementation:

package com.myPackage;

import com.backendless.push.BackendlessBroadcastReceiver;
import com.backendless.push.BackendlessPushService;

public class MyPushReceiver extends BackendlessBroadcastReceiver
{
  @Override
  public Class<? extends BackendlessPushService> getServiceClass()
  {
    return MyPushService.class;
  }
}
. Make sure to modify the manifest file to reflect your class implementations:
<receiver android:name=".MyPushReceiver" 
             android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
    <action android:name="com.google.android.c2dm.intent.RECEIVE"/>
    <category android:name="com.myPackage"/>
  </intent-filter>
</receiver>
<service android:name=".MyPushService" />

 

Push Notification Setup (iOS)

Setting up your backend to support Push Notifications for iOS requires a few steps, most of which are in Apple Developer Member Center and Keychain Access.  The process consists of the following steps:

 

 

Creating App ID

1. First we are going to create an App ID for the mobile application which will receive Push Notifications. Login to Apple Developer Member Center. Click on "App IDs" in the "Identifiers" section. Use the plus sign "+" button to create a new ID:
create-app-id.zoom60
2. When prompted enter App ID Prefix. Make sure it is descriptive enough so you recognize it later when you return to the Member Center.
3. Select Explicit App ID in the "App ID Suffix" section and enter the same bundle ID which you will be using in the application:
appidsuffix.zoom95
4. In App Services select the services which the application will use and click "continue":
appservices.zoom95
5. Make sure that Push Notifications are enabled and click "submit". This will conclude the App ID creation for the app:
confirm-app-id-creation.zoom90

 

Creating Certificate Request

Push Notifications require a certificate which will be used on a device by the means of a provisioning profile. Also the same certificate (transformed to the Personal Information Exchange - .p12 format) will be used by Backendless to publish Push Notifications. If this makes little sense, do not worry, you will need to perform these steps only ones and then can move on to code and using the APIs.

1. In order to create a certificate a Certificate Signing Request (CSR) must be issued. To create a CSR, open Keychain Access and select Keychain Access >> Certificate Assistant >> Request a Certificate from the main menu:
certificaterequest.zoom80
2. Enter your email address and Common Name (leave the CA Email Address field empty), select "Saved to disk" and click "Continue":
CertificateRequestData
3. Select a directory where to save the file and click Save.

 

Generating an SSL Certificate

The CSR file created in the section above will be used to create an SSL Certificate. That certificate will then be used by Backendless to publish push notifications.

1. Return to Apple Developer Member Center and select "All" under "Certificates". Click the plus button "+" to add a new certificate:
membercenteraddcertificate.zoom60
2. Select certificate type - there are two options Development and Production. For now select "Apple Push Notification service SSL (Sandbox)":
push-cert-sandbox.zoom90
3. Select the App ID created earlier in these instructions:
SelectAppId
4. Next you will see the instructions for generating a CSR which you have already created by now. Click Continue to proceed to the next step.
5. Select the CSR file created and saved to the disk earlier and click Generate:
select-csr-file
6. The certificate is ready now, click "Download" to download it:
download-push-cert.zoom90
7. Add the certificate file to Keychain Access.
8. Open Keychain Access and locate the certificate in the "My Certificates" section:
locate-push-cert-keychain-access.zoom60
9. Right click on the certificate and select the Export option:
export-push-cert.zoom70
10. Save the certificate in the p12 format:
save-cert-p12-format
11. Enter a password for the certificate. Make sure to make a record of the password - you will need to use it later in the instructions when you submit the certificate to Backendless:
enter-password
12. Enter your Mac OS X account password to confirm the action. At this point you have a certificate for Push Notifications.

 

Configuring Backendless App/Backend with the Certificate

Since Backendless provides the actual server-side integration for delivering Push Notifications for your application, it needs to have access to the certificate you created above. The steps below provide the instructions for uploading the certificate into Backendless:

1. Login to Backendless Console and select an application which you will use on the server-side.
2. Click Manage > App Settings. Locate the Mobile Settings section and click the IOS menu.
3. Click the Add Certificate button and upload the .p12 certificate created earlier. Make sure to enter the same password you used when created the certificate. The Channels control lets you select the messaging channels which the certificate will be used with. If you intend to use only one certificate, click the Add channels checkbox.
add-ios-cert
4. Now your Backendless server is ready to publish Push Notifications.

Device Registration

In order to receive push notifications from Backendless, a device must be registered using the API documented below. Device registration may optionally include a list of messaging channels and/or an expiration date/time when the registration should be canceled.

 

Push notifications can be published using either API or Backendless Console. When a push notification is published, it goes through a messaging channel. Channels provide a way to establish a level of filtering - devices registered with a channel will receive notifications published to the channel. However, there are other way to narrow down push notification delivery. For example, a push notification may be sent to a specific device or a group of devices.

 

If no channels are specified in the device registration call, Backendless registers the device with the default channel. If the device registration call references a non-existing channel, Backendless creates the channel and registers the device with it. Registration expiration is a point in time (expressed as a timestamp) when the device registration must expire. Backendless removes the device registration at the specified time and the device no longer receives published push notifications.

 

In order to receive push notifications on a mobile device, application running on the device must register with Backendless using the API call below:

 

// register device with the "default" channel
public void registerDevice( String GCMSenderID )

// register device with the specified channel
public void registerDevice( String GCMSenderID, String channel )

// register device with the channels and assign expiration timestamp for the registration
public void registerDevice( String GCMSenderID, List<String> channels, Date expiration )

where:

gcmSenderID -  Google application project number. See the See the Google Project Number section in the Push Notification Setup section of the guide for information on how to obtain the GCM Sender ID
channel(s) - channel (or a collection of channels) to receive messages from. For the method without the argument, the "Default" channel is used.
expiration -  a timestamp when the device registration should expire.

Errors:

 

The following errors may occur during the message publishing API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5004

Invalid expiration date. The expiration date must be after the current time.

8000

Property value exceeds the length limit. Error message should contain additional details about the violating property.

Example:

// do not forget to call Backendless.initApp in the app initialization section 
Backendless.Messaging.registerDevice( gcmSenderID );

 

Retrieve Device Registration

Backendless server returns information about device registration with the API documented below. The input parameter of the API is deviceId - the value returned by the device registration API.

Synchronous Method:

public DeviceRegistration getDeviceRegistration() throws BackendlessException;

Asynchronous Method:

public void getDeviceRegistration( AsyncCallback<DeviceRegistration> responder );

Return value:

DeviceRegistration - the AsyncCallback argument of the asynchronous method receives an instance of DeviceRegistration in the handleResponse method.

Errors:

The following errors may occur during the device registration API callThe following errors may occur during the device registration API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5000

Unable to retrieve device registration - unknown device ID.

Examples:

// do not forget to call Backendless.initApp in the app initialization code

DeviceRegistration devReg = Backendless.Messaging.getDeviceRegistration();

 

Managing Registrations

Application developers can manage device registrations using the Backendless Console:

1. Login to Backendless Console and select an app where you would like to see the device registrations.
2. Click the Data icon and select the DeviceRegistration table.
devicereg-table.zoom50

 

The table provides a single, unified view of all devices registered to receive push notifications. The table contains important information such as deviceId, deviceToken, a name of the messaging channel (channelName). Since the data about registered devices is stored directly in the Backendless database, it automatically supports the following functions:

Registered devices can be exported/imported into Backendless apps using the export/import functionality of the Data Service.
Data relations between registered devices and other data objects and geopoints (and vice versa)
Devices can be registered using the Data Service API or directly via the console.
SQL (whereClause-based) searches of the devices using the console or the Data Service API.

 

Backendless automatically links registered device with the currently logged in user. Suppose your app registers the device for push notifications. If there is a logged in user at the time when the device is being registered, Backendless creates a relation between the device and the user in the database (via a 1:1 relation):

device-to-user-mapping.zoom70

Publish Push Notifications

Backendless supports push notification delivery to iOS, Android and Windows Phone devices. A device must be registered (see the Device Registration API) in order to receive a push notification. A push notification can be published via API or using the Backendless Console. The content of a push notification consists of a message body and the specialized message headers.

 

Publish a Push Notification from Console

To publish a push notification from Backendless console, navigate to the Messaging screen and click the Devices tab. Make sure there are registered devices, otherwise a push notification has nowhere to go.

1. Select the check boxes for the device(s) you would like to send a notification to. Select Android or iOS tab to enter push notification parameters.
2. Enter parameter values into the corresponding fields.
3. Click the Publish Message button to send a push notification.
messaging-screen-v4.zoom50

 

Publish Push Notification with API

Message publishing API supports the following scenarios:

 

Basic push notification - In the simplest form a push notification is sent to a channel and this to all the devices registered with the channel. The notification message must contain appropriate headers which control the appearance of the notification on the devices. Example.

 

Push notification delivered to devices grouped by the OS - With this type of message delivery push notifications can be sent to all devices of a specific operating system (or a group of).  This includes delivery to Android, iOS or Windows Phone devices or a any combination of. Example.
 
Push notification delivered to specific devices - This delivery type allows push notification delivery to individual devices. Example
 
Delayed publishing - For all scenarios described above a push notification request can be configured to be executed at a later time (thus "delayed publishing"). Messages published with this delivery option can be canceled at any time using the message cancellation API. Example.

 

Push Notification Headers

The headers described below must be added to the "publish options" object or in case of REST clients, they are plain message headers:

Operating System

Headers

Description

 

iOS

"ios-alert-title":value

Sets the title of the push notification message.

"ios-alert-subtitle":value

Sets the sub-title of the push notification message (iOS 10+)

"ios-alert-body":value

Sets the text of the alert message. If the header is not present and the published notification targets the iOS devices, Backendless sets the header to the value of the "message" argument.

"ios-badge":value

Sets the value to update the badge with

"ios-sound":URL string or array of bytes

Sets either a URL for the sound notification to play on the device or an array of bytes for the sound to play.

"ios-content-available":value

Configures a silent-notification. When this key is present, the system wakes up your app in the background and delivers the notification to its app delegate.

Android

"android-ticker-text":value

Sets the text of the ticker showing up at the top of a device's screen when the device receives the notification.

"android-content-title":value

Sets the title of the notification as it is visible in the Android Notification Center

"android-content-text":value

Sets the message of the notification which appears under android-content-title in the Android Notification Center.

Windows Phone

"wp-title":value,
"wp-content":value

Sets the title and the content of a toast notification.

"wp-type":"TILE":
"wp-title" : value,
"wp-backgroundImage" : URL string,
"wp-badge" : number value,
"wp-backTitle" : value,
"wp-backImage" : URL string,
"wp-backContent" : value

Sets the properties for a tile notification.

"wp-type":"RAW",
"wp-raw":XMLString

Sets the properties for a raw notification

Synchronous Methods:

Publishes push notification to the "Default" channel. Headers must be added to the publishOptions argument.

public MessageStatus Backendless.Messaging.publish( Object message, 
                                                    PublishOptions publishOptions ) 
                                                        throws BackendlessException

 

Same as above, but the notification is published into the specified channel. Headers must be added to the publishOptions argument.

public MessageStatus Backendless.Messaging.publish( String channelName, 
                              Object message, 
                              PublishOptions publishOptions ) 
                                       throws BackendlessException

Publishes push notification to the "Default" channel. Headers must be added to the publishOptions argument. Delivery options can be used to:

1. Designate the message as "push notification only" or both a push notification and a pub/sub message.
2. Singlecast publishing - set a list of devices which should be receiving the notification.
3. Broadcast publishing - identify operating systems of the registered devices which should be receiving the notification.
4. Publish the message at a later time (delayed publishing).
5. Specify that the message should be republished multiple times (repeated publishing).

public MessageStatus Backendless.Messaging.publish( Object message, 
                              PublishOptions publishOptions, 
                              DeliveryOptions deliveryOptions ) 
                                     throws BackendlessException

Same as above, but published into the specified channel.

public MessageStatus Backendless.Messaging.publish( String channelName, 
                              Object message, 
                              PublishOptions publishOptions, 
                              DeliveryOptions deliveryOptions )  
                                     throws BackendlessException

Asynchronous Methods:

Same set of methods as above, but executed asynchronously. The difference in the method signatures is the AsyncCallback<MessageStatus> argument:

public void Backendless.Messaging.publish( Object message, 
                                           PublishOptions publishOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( String channelName, 
                                           Object message, 
                                           PublishOptions publishOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( Object message, 
                                           PublishOptions publishOptions, 
                                           DeliveryOptions deliveryOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( String channelName, 
                                           Object message, 
                                           PublishOptions publishOptions, 
                                           DeliveryOptions deliveryOptions, 
                                           AsyncCallback<MessageStatus> responder )

where:

channelName - name of the channel to publish the notification to. If the channel does not exist, Backendless automatically creates it.
message - object to publish. The object can be of any data type - a primitive value, String, Date, a  user-defined complex type, a collection or an array of these types.
publishOptions - an instance of PublishOptions. When provided may contain publisher ID (an arbitrary, application-specific string value identifying the publisher), and/or a collection of headers.
deliveryOptions - an instance of DeliveryOptions. When provided may specify options for push notification delivery such as: deliver to specific devices (or devices grouped by the operating system), delayed delivery or repeated delivery.

 

Return value:

MessageStatus - a data structure which contains ID of the published message. Use the getMessageId() method to get the ID assigned to the message by the backend (the ID is needed to check the message publishing  status or cancel the message).

Errors:

The following errors may occur during the message publishing API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5003

Invalid repeatExpiresAt date in delivery options.

5007

User does not have the permission to publish messages

5030

Invalid publishAt date in the delivery options.

Examples:

Basic push notification

Targeting a group of devices (grouped by OS)

Targeting specific devices

Delayed publishing

Basic push notification

// do not forget to call Backendless.initApp in the app initialization code

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "android-ticker-text", "You just got a push notification!" );
publishOptions.putHeader( "android-content-title", "This is a notification title" );
publishOptions.putHeader( "android-content-text", "Push Notifications are cool" );
MessageStatus status = Backendless.Messaging.publish( (Object) "Hi Devices!", 
                                                                publishOptions );

Targetting a group of devices

// do not forget to call Backendless.initApp in the app initialization code

DeliveryOptions deliveryOptions = new DeliveryOptions();

deliveryOptions.setPushBroadcast( PushBroadcastMask.ANDROID | PushBroadcastMask.IOS );

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "android-ticker-text", "You just got a push notification!" );
publishOptions.putHeader( "android-content-title", "This is a notification title" );
publishOptions.putHeader( "android-content-text", "Push Notifications are cool" );

MessageStatus status = Backendless.Messaging.publish( "Hi Devices!", 
                                                      publishOptions, 
                                                      deliveryOptions );

Targeting specific devices

// do not forget to call Backendless.initApp in the app initialization code 
DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.addPushSinglecast( receiver-device-id );

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "android-ticker-text", "You just got a private push notification!" );
publishOptions.putHeader( "android-content-title", "This is a notification title" );
publishOptions.putHeader( "android-content-text", "Push Notifications are cool" );

MessageStatus status = Backendless.Messaging.publish( "this is a private message!", 
                                                      publishOptions, 
                                                      deliveryOptions );

Delayed publishing

// do not forget to call Backendless.initApp in the app initialization code

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "android-ticker-text", "You just got a private push notification!" );
publishOptions.putHeader( "android-content-title", "This is a notification title" );
publishOptions.putHeader( "android-content-text", "Push Notifications are cool" );

DeliveryOptions deliveryOptions = new DeliveryOptions();
Date publishDate = new Date( System.currentTimeMillis() + 20000 ); // add 20 seconds
deliveryOptions.setPublishAt( publishDate );

MessageStatus status = Backendless.Messaging.publish( "This message was scheduled 20 sec ago", 
                                                      publishOptions, 
                                                      deliveryOptions );

 

Get Message Status

Backendless processes push notification delivery asynchronously. Even when the client app uses blocking/synchronous API, the server creates a push notification delivery task and adds to a queue. Server assigns an ID to the task and returns it to the client as messageId. As a result, push notification delivery status is not available right away. To retrieve the status, the client application must make a separate API call documented below:

Synchronous API:

Backendless.Messaging.getMessageStatus( String messageId );

Asynchronous API:

Backendless.Messaging.getMessageStatus( String messageId, AsyncCallback<MessageStatus> responder );

where:

messageId        -  ID of the message assigned to the message and returned by the Message Publishing API request.

Return Value:

Method returns an instance of com.backendless.messaging.MessageStatus. The object contains the following properties:

messageId        - ID of the message for which the publishing status is retrieved.

status                - an instance of an enum type com.backendless.messaging.PublishStatusEnum.

errorMessage        - contains detailed error message when status is PublishStatusEnum.FAILED.

 

Cancel Device Registration

To cancel the registration of a device with Backendless, an application can use the API described below:

 

public void unregisterDevice() throws BackendlessException;

Errors:

The following errors may occur during the device registration cancellation API callThe following errors may occur during the device registration API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5001

Unable to cancel device registration - unknown device ID.

Example:

String applicationID = "YOUR-APP-ID";
String secretKey = "ANDROID-KEY";
String version = "v1";
// this call must be somewhere early on in the application initialization
Backendless.initApp( appContextOrActivity, applicationId, secretKey, version );

try
{
  Backendless.Messaging.unregisterDevice();
}
catch( BackendlessException exception )
{
  // either device is not registered or an error occurred during de-registration
}

 

Publish/Subscribe Messaging

Message Publishing

Application can publish messages to Backendless for subsequent distribution to subscribers. A message must be published to a channel (or a group of channels). Backendless supports unlimited number of channels per application. Channels can be used as a filtering mechanism - channel subscribers see messages published only to the channel. Message publishing supports the following scenarios:

 

Publishing with message headers - headers is a collection of name/value pairs. A subscriber can set filters expressed as SQL "where clause" queries which Backendless uses to determine if a message should be delivered to the subscriber. When the query matches the published data in message headers, message is delivered to the corresponding subscriber. See example.
 
Publishing to a subtopic - Subtopics provide an additional level of message filtering.

Multiple subtopics can be defined within a channel. Both publishers and subscribers can specify a subtopic within a channel. Subtopic names can be defined using a multi-tiered format:

maintoken[.secondaryToken][.additionalToken]

 

To receive messages from more than one subtopic, subscribers can use the wildcard character (*) in place of any tokens in the subtopic name. For instance, a subscriber could subscribe to the following subtopic: "news..business.*", and the publisher sends messages to "news.business.newyork" and "news.business.tokyo". In this case the messages published to either subtopic will be delivered to the consumer.

 

The wildcard character in the last position will match any token in that position as well as tokens after it. For instance, subtopic com.foo.* will match all of the following: com.foo.bar, com.foo.abc.def, etc. However, the wildcard character in any position other than the last will match only one token. For example, subtopic com.*.foo will match com.abc.foo and com.123.foo, but will not match com.foo.

See example.
 

Delayed publishing - Backendless immediately processes any published messages and delivers them to subscribers without any delay. However, publishers can specify the time when the message should be processed. This is applicable to all the publishing options listed above. Message processing can be canceled at any time using the message cancellation API. See example.
 
Scheduled (repeated) publishing - Backendless supports repeated message processing - a message is published once, but delivered to subscribers with the specified frequency. Repeated delivery can stop either at the specified time or they can be canceled using the message cancellation API. For instance, this could be used for reminders or scheduled tasks. See example.

 

Method Signatures

Synchronous Methods:

Publishes message to "Default" channel.

public MessageStatus Backendless.Messaging.publish( Object message ) 
                                             throws BackendlessException

Same as above, but published into the specified channel.

public MessageStatus Backendless.Messaging.publish( String channelName, 
                                                    Object message ) 
                                                        throws BackendlessException

Publishes message to "Default" channel. May have headers and/or subtopic defined in the publishOptions argument.

public MessageStatus Backendless.Messaging.publish( Object message, 
                                                    PublishOptions publishOptions ) 
                                                            throws BackendlessException

Same as above, but published into the specified channel.

public MessageStatus Backendless.Messaging.publish( String channelName, 
                              Object message, 
                              PublishOptions publishOptions ) 
                                    throws BackendlessException

Publishes message to "Default" channel. May have headers and/or subtopic defined in the publishOptions argument. The deliveryOptions argument can be used to:

1. Designate the message as "push notification only" or both a push notification and a pub/sub message.
2. Publish the message at a later time (delayed publishing).
3. Specify that the message should be republished multiple times (repeated publishing).

public MessageStatus Backendless.Messaging.publish( Object message, 
                              PublishOptions publishOptions, 
                              DeliveryOptions deliveryOptions )
                                         throws BackendlessException

Same as above, but published into the specified channel.

public MessageStatus Backendless.Messaging.publish( String channelName, 
                              Object message, 
                              PublishOptions publishOptions, 
                              DeliveryOptions deliveryOptions ) 
                                   throws BackendlessException

Asynchronous Methods:

The same set of methods is available for the asynchronous invocation. The difference in the method signatures is the AsyncCallback<MessageStatus> argument:

public void Backendless.Messaging.publish( Object message, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( String channelName, 
                                           Object message, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( Object message, 
                                           PublishOptions publishOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( String channelName, 
                                           Object message, 
                                           PublishOptions publishOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( Object message, 
                                           PublishOptions publishOptions, 
                                           DeliveryOptions deliveryOptions, 
                                           AsyncCallback<MessageStatus> responder )

public void Backendless.Messaging.publish( String channelName, 
                                           Object message, 
                                           PublishOptions publishOptions, 
                                           DeliveryOptions deliveryOptions, 
                                           AsyncCallback<MessageStatus> responder )

where:

channelName - name of the channel to publish the message to. If the channel does not exist, Backendless automatically creates it.
message - object to publish. The object can be of any data type - a primitive value, String, Date, a  user-defined complex type, a collection or an array of these types.
publishOptions - an instance of PublishOptions. When provided may contain publisher ID (an arbitrary, application-specific string value identifying the publisher), subtopic value and/or a collection of headers.
deliveryOptions - an instance of DeliveryOptions. When provides may specify options for message delivery such as: deliver as a push notification, delayed delivery or repeated delivery.

 

Return value:

MessageStatus - a data structure which contains ID of the published message. Use the getMessageId() method to get the ID assigned to the message by the backend (the ID is needed to check the message publishing  status or cancel the message).

Errors:

The following errors may occur during the message publishing API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

 

Error Code

Description

5003

Invalid repeatExpiresAt date in delivery options.

5007

User does not have the permission to publish messages

5030

Invalid publishAt date in the delivery options.

Examples:

Basic message publishing

Publishing with message headers

Publishing to a subtopic

Delayed publishing

Repeated publishing

 

Basic message publishing

// do not forget to call Backendless.initApp in the app initialization logic

MessageStatus status = Backendless.Messaging.publish( "hello world! );
// message has been published. Message status is available via- response.getStatus()

Publishing with message headers

// do not forget to call Backendless.initApp in the app initializatio logic

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "city", "Tokyo" );
Weather weather = new Weather();
weather.setHumidity( 70 );
weather.setTempurature( 80 );
MessageStatus status = Backendless.Messaging.publish( weather, publishOptions );

Publishing to a subtopic

// do not forget to call Backendless.initApp in the app initialization logic

PublishOptions publishOptions = new PublishOptions();
publishOptions.setSubtopic( "news.business.newyork" );
MessageStatus status = Backendless.Messaging.publish( 
                                   (Object) "get free coffee at Moonbucks today", 
                                   publishOptions );

Delayed publishing

// do not forget to call Backendless.initApp in the app initialization logic

DeliveryOptions deliveryOptions = new DeliveryOptions();
Date publishDate = new Date( System.currentTimeMillis() + 20000 ); // add 20 seconds
deliveryOptions.setPublishAt( publishDate );

MessageStatus status = Backendless.Messaging.publish( 
                                               "This message was scheduled 20 sec ago", 
                                               null, 
                                               deliveryOptions );

Repeated publishing

// do not forget to call Backendless.initApp in the app initialization logic

DeliveryOptions deliveryOptions = new DeliveryOptions();
deliveryOptions.setRepeatEvery( 20 ); // the message will be delivered every 20 seconds
deliveryOptions.setRepeatExpiresAt( new Date( System.currentTimeMillis() + 60000 ) );

MessageStatus status = Backendless.Messaging.publish( 
                                    "This message was scheduled 20 sec ago", 
                                    null, 
                                    deliveryOptions );

 

Get Message Status

Backendless server handles message publishing as an autonomous task. As a result, the status of the message publishing operation is not available right away. To retrieve publishing status, the client application must make a separate API call documented below:

Synchronous API:

Backendless.Messaging.getMessageStatus( String messageId );

Asynchronous API:

Backendless.Messaging.getMessageStatus( String messageId, AsyncCallback<MessageStatus> responder );

where:

messageId        -  ID of the message assigned to the message and returned by the Message Publishing API request.

Return Value:

Method returns an instance of com.backendless.messaging.MessageStatus. The object contains the following properties:

messageId        - ID of the message for which the publishing status is retrieved.

status                - an instance of an enum type com.backendless.messaging.PublishStatusEnum.

errorMessage        - contains detailed error message when status is PublishStatusEnum.FAILED.

Message Filtering

Backendless message filtering is a powerful mechanism enabling conditional message delivery, interest-based subscriptions and private messaging. A subscription request may include filters in the form of subtopics and selectors. Backendless applies subscriber's filters to every message published into the channel and they match, the message is delivered to the subscriber.

 

Subtopics

Multiple subtopics can be defined within a channel. Both publishers and subscribers can specify a subtopic within a channel. Subtopic names can be defined using a multi-tiered format:

maintoken[.secondaryToken][.additionalToken]

 

To receive messages from more than one subtopic, subscribers can use the wildcard character (*) in place of any tokens in the subtopic name. For instance, a subscriber could subscribe to the following subtopic: "news..business.*", and the publisher sends messages to "news.business.newyork" and "news.business.tokyo". In this case the messages published to either subtopic will be delivered to the consumer.

 

The wildcard character in the last position will match any token in that position as well as tokens after it. For instance, subtopic com.foo.* will match all of the following: com.foo.bar, com.foo.abc.def, etc. However, the wildcard character in any position other than the last will match only one token. For example, subtopic com.*.foo will match com.abc.foo and com.123.foo, but will not match com.foo.

 

Selectors

A selector is a query expressed using the SQL-92 syntax and formatted as the condition part of the SQL's WHERE clause. A query condition must reference the headers of the published messages. When a message is published and a subscriber has a selector query, Backendless executes the query on the headers of the published message. If the result of the query is true, the message is delivered to the subscriber. Consider the following example where the subscriber will receive only messages containing the "city" header with the value of "Tokyo":

 

Publisher:

// do not forget to call Backendless.initApp in the app initialization code

PublishOptions publishOptions = new PublishOptions();
publishOptions.putHeader( "city", "Tokyo" );
Weather weather = new Weather();
weather.setHumidity( 70 );
weather.setTempurature( 80 );
MessageStatus status = Backendless.Messaging.publish( weather, publishOptions );

Subscriber:

// do not forget to call Backendless.initApp in the app initialization code

AsyncCallback<List<Message>> subscriptionResponder = new AsyncCallback<List<Message>>()
  {
    public void handleResponse( List<Message> response )
    {
    }
    public void handleFault( BackendlessFault fault )
    {
    }
  };

SubscriptionOptions subscriptionOptions = new SubscriptionOptions();
subscriptionOptions.setSelector( "city='Tokyo'" );

Backendless.Messaging.subscribe( subscriptionResponder, subscriptionOptions );

 

Message Subscription

In order to receive published messages, application must subscribe to a channel using the API described below. Using the API, an application becomes an "API subscriber".

    Synchronous Methods

Subscribing to "Default" channel:

public Subscription Backendless.Messaging.subscribe( 
                      AsyncCallback<List<Message>> subscriptionResponder ) 
                                                     throws BackendlessException

public Subscription Backendless.Messaging.subscribe( int pollingInterval, 
                               AsyncCallback<List<Message>> subscriptionResponder ) 
                                                     throws BackendlessException

public Subscription Backendless.Messaging.subscribe( 
                            AsyncCallback<List<Message>> subscriptionResponder, 
                            SubscriptionOptions subscriptionOptions ) 
                                                    throws BackendlessException

Subscribing to a specific channel:

public Subscription Backendless.Messaging.subscribe( 
                              String channelName, 
                              AsyncCallback<List<Message>> subscriptionResponder ) 
                                           throws BackendlessException

public Subscription Backendless.Messaging.subscribe( 
                               String channelName, 
                               int pollingInterval, 
                               AsyncCallback<List<Message>> subscriptionResponder ) 
                                           throws BackendlessException

public Subscription Backendless.Messaging.subscribe( 
                               String channelName, 
                               AsyncCallback<List<Message>> subscriptionResponder, 
                               SubscriptionOptions subscriptionOptions ) 
                                                  throws BackendlessException

public Subscription Backendless.Messaging.subscribe( 
                            String channelName, 
                            AsyncCallback<List<Message>> subscriptionResponder, 
                            SubscriptionOptions subscriptionOptions, 
                            int pollingInterval ) throws BackendlessException

where:

channelName - name of the channel to subscribe to.

subscriptionResponder- responder object where Backendless delivers messages for the subscriber.

subscriptionOptions - contains settings for filtered message delivery. Available options include subtopic and selector. See the Message Filtering section below.
pollingInterval - the interval in milliseconds between subsequent polling requests. Each polling request checks for messages for the subscription.

Return value:

Subscription - an object representing the subscription. A Subscription object provides access to a unique identifier which can be used for subscription cancellation.

Asynchronous Methods:

All asynchronous methods have exactly the same signatures as the synchronous counterparts except for the AsyncCallback argument which receives a notification about the successful or failed completion of the method:

Subscribing to "Default" channel:

public void Backendless.Messaging.subscribe( 
                    AsyncCallback<List<Message>> subscriptionResponder,
                    AsyncCallback<Subscription> methodCallback ) 
                                  throws BackendlessException

public void Backendless.Messaging.subscribe( 
                       int pollingInterval, 
                       AsyncCallback<List<Message>> subscriptionResponder,
                       AsyncCallback<Subscription> methodCallback ) 
                                  throws BackendlessException

public void Backendless.Messaging.subscribe( 
                 AsyncCallback<List<Message>> subscriptionResponder, 
                 SubscriptionOptions subscriptionOptions,
                 AsyncCallback<Subscription> methodCallback ) 
                              throws BackendlessException

Subscribing to a specific channel:

public void Backendless.Messaging.subscribe( String channelName, 
                       AsyncCallback<List<Message>> subscriptionResponder,
                       AsyncCallback<Subscription> methodCallback ) 
                                     throws BackendlessException

public void Backendless.Messaging.subscribe( String channelName, 
                       int pollingInterval, 
                       AsyncCallback<List<Message>> subscriptionResponder,
                       AsyncCallback<Subscription> methodCallback ) 
                                       throws BackendlessException

public void Backendless.Messaging.subscribe( String channelName, 
                       AsyncCallback<List<Message>> subscriptionResponder, 
                       SubscriptionOptions subscriptionOptions,
                       AsyncCallback<Subscription> methodCallback ) 
                                  throws BackendlessException

public void Backendless.Messaging.subscribe( String channelName, 
                       AsyncCallback<List<Message>> subscriptionResponder, 
                       SubscriptionOptions subscriptionOptions, 
                       int pollingInterval,
                       AsyncCallback<Subscription> methodCallback ) 
                                  throws BackendlessException

where:

channelName - name of the channel to subscribe to.

subscriptionResponder- responder object where Backendless delivers messages for the subscriber.

subscriptionOptions - contains settings for filtered message delivery. Available options include subtopic and selector. See the Message Filtering section below.
pollingInterval - the interval in milliseconds between subsequent polling requests. Each polling request checks for messages for the subscription
methodCallback - a responder object notified of the successful or failed completion of the subscription operation. An invocation the handleResponse callback method represents a successful message subscription. The argument of the callback is an instance of the Subscription class - an object  representing the established subscription. A Subscription object provides access to a unique identifier which can be used for subscription cancellation.

Errors:

The following errors may occur during the message cancellation API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5008

User does not have permission to create a subscription.

5009

General subscription error. See error message for additional details.

5010

Unknown messaging channel.

Message Object

The Java/Android client retrieves messages through an implementation of the AsyncCallback interface. All subscribe methods described above receive the following argument:AsyncCallback<List<Message>> subscriptionResponder. Backendless delivers new messages published to the subscribed channel through the receivedResponse method of the subscriptionResponder object:

new AsyncCallback<List<Message>>()
{
  public void handleResponse( List<Message> response )
  {
    for( Message message : response )
    {
      String publisherId = message.getPublisherId();
      Object data = message.getData();
    }
  }
  public void handleFault( BackendlessFault fault )
  {
    // handle error here
  }
}

The response argument in the handleResponse method is a collection of the com.backendless.messaging.Message objects:

package com.backendless.messaging;

public class Message
{
  public String getMessageId();
  public Map<String, String> getHeaders();
  public Object getData();
  public String getPublisherId();
  public long getTimestamp();
  public String toString();
}

where:

messageId - unique message ID. The ID is assigned at the time of message publishing.
headers - an associative array which is a collection of key/value pairs. Includes all the headers included with the message publishing. Additionally, Backendless adds the following headers: BL_APPLICATION_ID - contains the ID of the application and BL_VERSION_URL_PREFIX - contains the name of the version of the application.
data - message payload. It is the object sent by a publisher.
publisherID - the property contains sender (publisher) ID if it is provided by the publisher.
timestamp - a timestamp indicating when the message was received by Backendless from the publisher.

 

Examples:

 

// do not forget to call Backendless.initApp in the app initialization code

Backendless.Messaging.subscribe( channelName, 
  new AsyncCallback<List<Message>>()
  {
    public void handleResponse( List<Message> response )
    {
      for( Message message : response )
      {
        String publisherId = message.getPublisherId();
        Object data = message.getData();
      }
    }
    public void handleFault( BackendlessFault fault )
    {
      Toast.makeText( ChatActivity.this, 
                      fault.getMessage(), 
                      Toast.LENGTH_SHORT ).show();                                       }
    }
  }, 
  new AsyncCallback<Subscription>()
  {
    public void handleResponse( Subscription response )
    {
      subscription = response;
    }
    public void handleFault( BackendlessFault fault )
    {
      Toast.makeText( ChatActivity.this, 
                      fault.getMessage(), 
                      Toast.LENGTH_SHORT ).show();
    }
  }
);

 

Cancel Subscription

In order to stop a client from polling for messages, it must issue subscription cancellation request using the API method described below:

 

Method Signatures:
 

subscriptionObject.cancelSubscription() throws BackendlessException;

where:

subscriptionObject - an instance of the Subscription class. The object is returned as a result of the Message Subscription call.

Return value:

boolean - true if the scheduled message has been successfully canceled, False otherwise.

 

Example:

// do not forget to call Backendless.initApp in the app initialization code

Backendless.Messaging.subscribe( channelName, new AsyncCallback<List<Message>>()
  {
    public void handleResponse( List<Message> response )
    {
    }
    public void handleFault( BackendlessFault fault )
    {
    }
 }, 
  new AsyncCallback<Subscription>()
  {
    public void handleResponse( Subscription response )
    {
      Subscription subscription = response;
      subscription.cancelSubscription();
    }
    public void handleFault( BackendlessFault fault )
    {
    }
  }
);

 

Cancel Scheduled Message

Delayed or scheduled messages can be canceled using the API documented below. Backendless processes delayed messages at the time specified by the publisher. Scheduled messages are processed and delivered with a specified interval.

 

Method Signatures

Synchronous Method:

public boolean Backendless.Messaging.cancel( String messageId ) throws BackendlessException;

Asynchronous Method:

public void Backendless.Messaging.cancel( String messageId, 
                                          AsyncCallback<MessageStatus> responder );

where:

messageId - ID of the message to cancel. Message ID must be obtained from the MessageStatus object obtained as the result of the Publish methods.
responder - contains the ID of a message and its status. If the cancellation is successful, the getStatus() method on the MessageStatus object returns the value of com.backendless.messaging.PublishStatusEnum.CANCELED).

 

Return value:

boolean - true if the scheduled message has been successfully canceled, false otherwise.

Errors:

The following errors may occur during the message cancellation API call. See the Error Handling section for details on how to retrieve the error code when the server returns an error:

Error Code

Description

5040

Message has already been canceled or does not exist.

Example:

String applicationID = "YOUR-APP-ID";
String secretKey = "ANDROID-KEY";
String version = "v1";
// this call must be somewhere early on in the application initialization
Backendless.initApp( appContextOrActivity, applicationId, secretKey, version );


DeliveryOptions deliveryOptions = new DeliveryOptions();
Date publishDate = new Date( System.currentTimeMillis() + 20000 ); // add 20 seconds
deliveryOptions.setPublishAt( PushPolicy.ONLY );

MessageStatus status = Backendless.Messaging.publish( "Test Message", null, deliveryOptions );
boolean result = Backendless.Messaging.cancel( status.getMessageId() );

 

Files API

Overview

Every Backendless backend/app is allocated a dedicated file storage space. The file storage is located remotely on the Backendless servers. The file storage can be used to store application's files and on-demand video streams.  Backendless File Service provides the API to work with the file storage. The API supports the following operations:

 

File Upload - upload files to the applications's file storage. The operation creates directories up the hierarchy if necessary. Returns file URL which can be used to download or share the file with others.
File Download - download file using file's URL. The download operation is subject to the permissions from the File access control list (ACL).
File Deletion - delete a file from the file storage. The delete operation is subject to the permissions from the File access control list (ACL).
Directory Deletion - same as file deletion, but applies to the directories.
File/Directory Security (File ACL) - assign/unassign user and roles permissions to upload, download and delete files and directories. This API is used to modify file or directory ACL.

 

In addition to the API implementation, the File Service enables the following capabilities:

 

Git Integration - application developers can interact with the file storage as with a git repository.
Web Hosting - file storage can be used to host static web content.
Custom Domain Name - a custom domain name can be mapped to the file storage in a Backendless backend. This feature in combination with the Web Hosting provides a way to host websites on Backendless.
Custom Web Templates Hosting - includes HTML files and JS scripts for special pages used in various workflows such as user email confirmation, password change and session expiration.

Handling Files in Console

Backendless Console includes a graphical file browser which supports the following operations:

Creating a New File

To create a file:

1. Log in to Backendless Console and select an application. Click the Files icon:
files-screen1.zoom50
2. Select a directory where a new file should created (or create a new directory). Click the New File link from the toolbar.
3. Enter a name in the File name field. Additionally, you can select syntax highlighter from the menu located in the lower-right corner. The highlighter selection is available only for new files. When an existing file is opened for editing, a highlighter is automatically selected based on the file extension.
files-screen2.zoom50
4. To save the file click the Save or the Save and Close button.

Editing a File

To edit a file:

1. Select a directory containing the file on the Files screen of the console.
2. Click the Edit file icon in the Actions column for the file to open it for editing:

files-screen3

3. Once the changes in the file are made click the Save or the Save and Close button.

Getting Public URL for a File

A file in the Backendless File Storage has two URLs:

a public URL which can be used to download the file outside of Backendless console. This URL is subject for any permissions assigned to the file or the directory where it resides.
a private URL which makes the file accessible to the developer of the application.

In order to obtain the public URL:

1. Select a directory containing a file on the Files screen of Backendless console.
2. Click the Get Public URL icon next to the file. Backendless console copies the file's public URL to the computer's clipboard:

files-screen4

To get file's private URL:

Right click file name and select "Copy Link Address" (text of the menu may vary depending on the browser):
files-screen5

File's private URL works only if the request to download the file contains the auth-key HTTP header. The value of the header is a token assigned by Backendless to the developer login to console.

Archiving Files

Backendless Console includes a feature enabling to compress directories into a single ZIP file. The feature applies specifically to directories, meaning an individual file cannot be compressed - it must be placed into a directory first.

Notice: archiving of directories with total content size greater than 100 Mb may take longer time; Backendless sends an email to the application developer upon successful completion of the operation.

To archive a directory:

1. Log in to Backendless Console, select an application and click the Files icon.
2. Navigate to a directory which should be compressed.
3. Click the ZIP Directory button:
files-screen6
4. Once the directory is compressed into an archive, it will appear in the parent directory:
files-screen7

File Upload

The file upload operation delivers and saves a local file in the remote Backendless file storage. The return value of the operation is the file URL which has the following structure:

https://api.backendless.com/<application id>/<REST-api-key>/files/<path>/<file name>

where:

<application id> - ID of the application which can be obtained from the Manage > App Settings screen of the Backendless Console
<REST-api-key> - REST API key of your application. You can get the value from the Manage > App Settings section of the Backendless Console.
<path> - directory path where the file is saved
<file name> - name of the file

The URL assigned to a file and returned as a result of the upload operation accounts for any security permissions assigned to the file (or the folder it is located in).

Java and Android

Asynchronous Methods:

Upload a file to a remote path in the Backendless storage.

// uploads a file to a remote directory in Backendless Hosting
public void Backendless.Files.upload( File file,
                                      String remotePath,
                                      AsyncCallback<BackendlessFile> responder )
                                      
// same as the method above, but with an option to overwrite a file if one exists with the same name
public void Backendless.Files.upload( File file,
                                      String remotePath,
                                      boolean overwrite,
                                      AsyncCallback<BackendlessFile> responder )

Upload a file to a remote path and receive updates for the status of the upload. Updates are represented as percentages and are delivered via the uploadCallback argument.

// uploads a file to a remote directory in Backendless Hosting.
public void Backendless.Files.upload( File file,
                                      String remotePath,
                                      UploadCallback uploadCallback,
                                      AsyncCallback<BackendlessFile> responder )
                                      
// same as the method above, but with an option to overwrite a file if one exists with the same name
public void Backendless.Files.upload( File file,
                                      String remotePath,
                                      boolean overwrite,
                                      UploadCallback uploadCallback,
                                      AsyncCallback<BackendlessFile> responder )

Synchronous Methods:

Upload a file to a remote path in the Backendless storage.

// uploads file to the specified path in the Backendless Hosting storage
public BackendlessFile Backendless.Files.upload( File file, 
                                                 String remotePath ) 
                                                          throws Exception

// same as the above, but provides an option to overwrite if 
// another file already exists with the same name
public BackendlessFile Backendless.Files.upload( File file, 
                                                 String remotePath, 
                                                 boolean overwrite ) 
                                                           throws Exception

Upload a file to a remote path and receive updates for the status of the upload. Updates are represented as percentages and are delivered via the uploadCallback argument.

// uploads file to a directory in Backendless Hosting. 
public BackendlessFile Backendless.Files.upload( File file,
                                                 String remotePath,
                                                 UploadCallback uploadCallback ) 
                                                          throws Exception
                                                 
// same as the method above, but with an option to overwrite 
// the file if one exists with the same name
public BackendlessFile Backendless.Files.upload( File file,
                                                 String remotePath,
                                                 boolean overwrite,
                                                 UploadCallback uploadCallback ) 
                                                           throws Exception

Create a file in the remote Backendless file storage from the bytes provided by OutputStream.

// uploads data from output stream provided by the outputStreamRouter object to a remote
// directory in Backendless Hosting.
public BackendlessFile Backendless.Files.uploadFromStream( 
                                             IOutputStreamRouter outputStreamRouter,
                                             String remoteName,
                                             String remotePath ) throws Exception
                                                           
// same as the method above, but with an option to overwrite 
// the file if one exists with the same name
public BackendlessFile Backendless.Files.uploadFromStream( 
                                             IOutputStreamRouter outputStreamRouter,
                                             String remoteName,
                                             String remotePath,
                                             boolean overwrite ) throws Exception

 

where:

file - local file to upload to Backendless file storage. Remote file name is the same as the name of the local file.
remotePath - path (without file name) in the remote Backendless file storage where the file should be saved.
uploadCallback - receives updates through the upload process. Updates expressed as percentage of file uploaded to the server.
outputStreamRouter - a wrapper around a java.io.ObjectStream object which provides the bytes for the upload.
remoteName - name of the file to save in the remote Backendless file storage.
responder - a responder object which receives a callback when the method successfully uploads the file or if an error occurs. Applies  to the asynchronous methods only.

Android Only

Synchronous Method:

Upload a bitmap image and save as a file in the remote Backendless storage.

// uploads an android bitmap to a remote directory in Backendless Hosting
public BackendlessFile Backendless.Files.upload( 
                               android.graphics.Bitmap bitmap,
                               android.graphics.Bitmap.CompressFormat compressFormat,
                               int quality,
                               String remoteName,
                               String remotePath ) throws Exception

// same as the method above, but with an option to overwrite 
// the file if one exists with the same name
public BackendlessFile Backendless.Files.upload( 
                               android.graphics.Bitmap bitmap,
                               android.graphics.Bitmap.CompressFormat compressFormat,
                               int quality,
                               String remoteName,
                               String remotePath,
                               boolean overwrite ) throws Exception

Asynchronous Method:

Same as the method above, but the upload is handled asynchronously.

// uploads a bitmap and stores it in a file in remote directory in Backendless Hosting
public void Backendless.Files.upload( 
                          android.graphics.Bitmap bitmap,
                          android.graphics.Bitmap.CompressFormat compressFormat,
                          int quality,
                          String remoteName,
                          String remotePath,
                          AsyncCallback<BackendlessFile> responder ) throws Exception
                                      
// same as the method above, but with an option to overwrite 
// the file if one exists with the same name
public void Backendless.Files.upload( 
                          android.graphics.Bitmap bitmap,
                          android.graphics.Bitmap.CompressFormat compressFormat,
                          int quality,
                          String remoteName,
                          String remotePath,
                          AsyncCallback<BackendlessFile> responder ) throws Exception

where:

bitmap - bitmap to upload to the remote Backendless storage and save as a file.
compressFormat - compress format to use when compressing the bitmap. The compression is done by the Bitmap.compress method.
quality - quality of the compression. The value is passed to the Bitmap.compress method, which handles the compression.
remoteName - name of the file to save in the remote Backendless file storage.
remotePath - path in the remote Backendless file storage where the file should be saved.
responder - a responder object which receives a callback when the method successfully uploads the file or if an error occurs. Applies  to the asynchronous methods only

Return Value (sync methods only):

BackendlessFile - a wrapper around the URL assigned to the uploaded file.

Example:

Bitmap photo = (Bitmap) getIntent().getExtras().get( "data" );
 
Backendless.Files.Android.upload( photo, 
                                  Bitmap.CompressFormat.PNG, 
                                  100, 
                                  "myphoto.png", 
                                  "mypics", 
                                  new AsyncCallback<BackendlessFile>()
{
 @Override
 public void handleResponse( final BackendlessFile backendlessFile )
 {
 }
 
 @Override
 public void handleFault( BackendlessFault backendlessFault )
 {
   Toast.makeText( UploadingActivity.this, 
                   backendlessFault.toString(), 
                   Toast.LENGTH_SHORT ).show();
 }
}

 

Save Files From Byte Arrays

In addition to the classic file upload, files can be saved by uploading a byte array which becomes the content of the saved file.

Asynchronous Methods:

// Save file in a remote path in the Backendless file storage. The path includes both the directory 
// where the file should be saved and the file name. If the file with the same name already exists, 
// it is not overwritten and error is returned.
public void saveFile( String filePathName, byte[] fileContent, 
                                                 AsyncCallback<String> responder )

// Save a file in a remote path in the Backendless file storage. The path includes both the directory
// where the file should be saved and the file name. The overwrite argument determines if the file 
// should be overwritten if it already exists.
public void saveFile( String filePathName, byte[] fileContent, boolean overwrite, 
                                                 AsyncCallback<String> responder )

// Save a file in a remote path in the Backendless file storage. If the file with the same name 
// already exists, it is not overwritten and error is returned.
public void saveFile( String path, String fileName, byte[] fileContent, 
                                                 AsyncCallback<String> responder )

// Save a file in a remote path in the Backendless file storage. The overwrite argument determines 
// if the file should be overwritten if it already exists.
public void saveFile( String path, String fileName, byte[] fileContent, boolean overwrite, 
                                                 AsyncCallback<String> responder )

Synchronous Methods:

// Save a file in a remote path in the Backendless file storage. The path includes 
// both the directory where the file should be saved and the file name. If another 
// file with the same name already exists, it is not overwritten and an error is returned.
public String saveFile( String filePathName, byte[] fileContent )

// Save a file in a remote path in the Backendless file storage. The path includes 
// both the directory where the file should be saved and the file name. The 
// overwrite argument determines if the file should be overwritten, if it already exists.
public String saveFile( String filePathName, byte[] fileContent, boolean overwrite )

// Save a file in a remote path in the Backendless file storage. If the file with 
// the same name already exists, it is not overwritten and error is returned.
public String saveFile( String path, String fileName, byte[] fileContent )

// Save a file in a remote path in the Backendless file storage. The overwrite 
// argument determines if the file should be overwritten if it already exists.
public String saveFile( String path, String fileName, byte[] fileContent, boolean overwrite )

 

where:

filePathName - identifies both the directory where the file should be saved and the file name. Must start with "/" which represents the root directory of the remote file storage.
path - path of the directory where the file should be stored. Must start with "/" which represents the root directory of the remote file storage.
fileName - name of the file where the byte content should be written to.
fileContent - an array of bytes to save.
overwrite - the file is overwritten if the argument value is true and the file already exists. Otherwise, if the value is false and another file with the same name already exists, an error  is returned.
responder - a responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous methods only.

 

Example:

The example below describes how to save a file called "fox.txt" from the string "The quick brown fox jumps over the lazy dog." You will need to specify:

content of a new file ("The quick brown fox jumps over the lazy dog")
where to save the new file ("testfolder")
a name of the newly created file ("fox.txt")
whether a new file should overwrite the existing file, if any (true)

public static void saveFile() 
{ 
  byte[] bytes = "The quick brown fox jumps over the lazy dog".getBytes(); 
  String savedFileURL = Backendless.Files.saveFile( "tempfolder", 
                                                    "fox.txt", 
                                                    bytes, 
                                                    true ); 
  System.out.println( "File saved. File URL - " + savedFileURL ); 
}

The server will return notification and link to the newly added file ("File saved. File URL - ") or an error.

Errors:

Error codes returned on attempt to save a file from the byte array.

Error Code

Description

6016

When saving a new file from the byte array, the payload exceeds 2,800,000 bytes.

6003

A file you are trying to save already exists in the system and cannot overwrite since overwrite argument is ether set to false or omitted.

 

File Download

Downloading a File via the Backendless Console

To download a file:

1. Log in to Backendless Console and select the application containing the file.
2. Click the Files tab on the left menu.
3. Locate a file you want to download. Click the Download file icon.

files-screen8

Downloading a File via API

Downloading a file from the Backendless file storage is the basic HTTP GET operation. The operation should use the same URL which Backendless returned as the result of the file upload operation. Alternatively, if the file was uploaded manually using the console, the URL can be composed as:

https://api.backendless.com/<application id>/<REST API key>/files/<path>/<file name>

where:

<application id> - ID of the application which can be obtained from the Manage > App Settings screen of the Backendless Console.
<REST API key> - REST API key assigned to the application by Backendless. The key is available from the Manage > App Settings screen of the Backendless Console.
<path> - Directory path where the file is saved.
<file name> - Name of the file.

Files fetched with the URL scheme defined above are subject to the security constraints and permissions established by the application developer. See the Files Security section for additional details on how to secure file storage. Fetching a file secured by an access control list (ACL) policy requires an additional HTTP header in the request:

user-token:<value>

where:

<value> - a value identifying currently logged in user (if any). This is the value received in the response for the login API request.  The token uniquely identifies the user and the roles associated with him. It is used by Backendless to establish user's identity for all operations where the token is present. It is necessary in order to determine permissions applicable to the user and the roles associated with the account. This header is optional.

Renaming a File/Directory

 
Renames a file or a directory in the Backendless file storage:

Synchronous Method:

public String renameFile( String oldPathName, String newName )

Asynchronous Method:

public void renameFile( String oldPathName, String newName, AsyncCallback<String> responder )

where:

oldPathName - a path identifying file or directory to be renamed. The must start with the root directory for the file storage allocated to the application.
newName - new name for the file or directory.
responder - a responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous method only.

Return value:

Absolute path of the renamed file or directory.

Example:

The example below describes how to rename a file called readme.txt located in the /documentation directory to readme-first.txt:

Backendless.Files.renameFile( "/documentation/readme.txt", "readme-first.txt" );

Errors:

The server may return the following errors:

Error Code

Error message

Notes

4000

User has no permissions to specified resource

Occurs when the user (anonymous or logged in) has no permission to modify the file

6000

File or directory cannot be found.

Occurs when the source file/directory cannot be found.

6006

Path contains prohibited symbols: {path name}


6007

The specified resource was not found

Occurs when the API requests to rename a non-existent file or a directory.

6018

Unable to modify file/folder: file/folder already exists: {file/directory name}

Occurs when the target file/directory already exists.

6028

Parameter {param name} cannot be null

Occurs when one of the parameters is null.

8010

Missing field {field name}

Occurs in REST APIs when one of the required fields is missing in the body.

Copying a File/Directory

Renames a file or a directory in the Backendless file storage:

Asynchronous Method:

public void copyFile( String sourcePathName, String targetPath, AsyncCallback<String> responder )

Synchronous Method:

public String copyFile( String sourcePathName, String targetPath )

where:

sourcePathName - a path identifying file or directory to be copied. The path must start with the root directory of the remote file storage.
targetPath - a path to a directory where the source file or directory should be copied to. If the directory does not exist, it is created.
responder - a responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous method only.

Return value:

Absolute path to the copied file or directory.

Example:

The example below describes how to copy a file called readme.txt located in the /documentation directory to the /updated-docs directory:

Backendless.Files.copyFile( "/documentation/readme.txt", "/updated-docs" );

Errors:

The server may return the following errors:

Error Code

Error message

Notes

4000

User has no permissions to specified resource

Occurs when the user (anonymous or logged in) has no permission to modify the file

6000

File or directory cannot be found.

Occurs when the source file/directory cannot be found.

6006

Path contains prohibited symbols: {path name}


6007

The specified resource was not found

Occurs when the API requests to rename a non-existent file or a directory.

6018

Unable to modify file/folder: file/folder already exists: {file/directory name}

Occurs when the target file/directory already exists.

6028

Parameter {param name} cannot be null

Occurs when one of the parameters is null.

8010

Missing field {field name}

Occurs in REST APIs when one of the required fields is missing in the body.

8011

Content type should be 'application/json'

Occurs in REST API when the request's content type is not application/json

Moving a File/Directory

Renames a file or a directory in the Backendless file storage:

Asynchronous Method:

public void moveFile( String sourcePathName, String targetPath, AsyncCallback<String> responder )

Synchronous Method:

public String moveFile( String sourcePathName, String targetPath )

where:

sourcePathName - a path identifying file or directory to move. The path must start with the root directory of the remote file storage.
targetPath - a path to a directory where the source file or directory should be moved to. If the directory does not exist, it is created.
responder - a responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous method only.

Return value:

Absolute path to the copied file or directory.

Example:

The example below describes how to move a file called readme.txt located in the /documentation directory to the /updated-docs directory:

Backendless.Files.moveFile( "/documentation/readme.txt", "/updated-docs" );

Errors:

The server may return the following errors:

Error Code

Error message

Notes

4000

User has no permissions to specified resource

Occurs when the user (anonymous or logged in) has no permission to modify the file

6000

File or directory cannot be found.

Occurs when the source file/directory cannot be found.

6006

Path contains prohibited symbols: {path name}


6007

The specified resource was not found

Occurs when the API requests to rename a non-existent file or a directory.

6018

Unable to modify file/folder: file/folder already exists: {file/directory name}

Occurs when the target file/directory already exists.

6021

Unable to move file to file


6023

File already exists in target directory


6028

Parameter {param name} cannot be null

Occurs when one of the parameters is null.

8010

Missing field {field name}

Occurs in REST APIs when one of the required fields is missing in the body.

8011

Content type should be 'application/json'

Occurs in REST API when the request's content type is not application/json

Directory Listing

Retrieves a listing of a directory in the Backendless file storage:

Synchronous Methods:

Get a directory listing for the specified path with a pattern matching all files excluding all subdirectories.

public List<FileInfo> listing( String path )

Get a directory listing for the specified path and pattern. Subdirectory inclusion is controlled by the recursive argument.

public List<FileInfo> listing( String path, String pattern, boolean recursive)
public List<FileInfo> listing( String path, 
                                  String pattern, 
                                  boolean recursive, 
                                  int pagesize, 
                                  int offset)

Asynchronous Method:

Get a directory listing for the specified path with a pattern matching all files excluding all subdirectories.

public void listing( String path, 
                     AsyncCallback<List<FileInfo>> responder)

Get a directory listing for the specified path and pattern. Subdirectory inclusion is controlled by the recursive argument.

public void listing( String path, 
                     String pattern, 
                     boolean recursive, 
                     int pagesize,
                     int offset,
                     AsyncCallback<List<FileInfo>> responder)

where:

path - path of a directory to get a listing of. The path must start with the root directory identified by forward slash, for example: "/web".
pattern - A pattern which the returned files and directories must match. The pattern can include wildcard characters. Asterisk (*) matches a substring of any length. Question mark (?) matches only one character in the file/directory name.
recursive - an optional parameter. If present and is true, requests that the listing must be retrieved recursively for all directories contained within <path>.
pagesize - an optional parameter. If present, identifies how many items should be returned in the response.
offset - an optional parameter. If present, indicates the index of item in the response from which to get the <pagesize> items.
responder - a responder object which receives a callback when the method successfully completes or if an error occurs. Applies to the asynchronous method only.

Return value:

A collection of files and directories matching the specified pattern. The returned collection may not include all files and directories as the size of the number of returned entries is limited for paging purposes. To get the next set of file/directory entries, an additional request must be made with a different offset value. The total count of all files/directories in the listing use the Get File Count API.

Each element in the collection contains the following properties:

name - name of the file or directory without any path information
public URL - absolute URL of the file or directory
URL - relative URL of the file or directory starting from the root of the file storage
created on - a timestamp indicating when the file or directory were created

Example:

The example below describes how to get a listing of all HTML files from the /web directory and all subdirectories.

Backendless.Files.listing( "/web", "*.html", true, 
                  new AsyncCallback<List<FileInfo>>()
{
  @Override
  public void handleResponse( List<FileInfo> response )
  {
    Iterator<FileInfo> filesIterator = response.iterator();
    while( filesIterator.hasNext() )
    {
      FileInfo file = filesIterator.next();
      String URL = file.getURL();
      String publicURL = file.getPublicUrl();
      Date createdOn = new Date( file.getCreatedOn() );
      String name = file.getName();
     }
   }

   @Override
   public void handleFault( BackendlessFault fault )
   {
   }
 });

Errors:

The server may return the following errors:

Error Code

Error message

Notes

6000

File or directory cannot be found.

Occurs when the source file/directory cannot be found.

6006

Path contains prohibited symbols: {path name}


6007

The specified resource was not found

Occurs when the API requests to rename a non-existent file or a directory.

6029

Specified resource must be a directory

Occurs when the path to get a listing for is not a directory

Get File Count

This operation returns the number of files and optionally directories located in the specified path. Additional options include:

 

pattern filtering - counts files and directories which match the pattern.
recursive counting - when enabled, counts all matching files and directories while recursively traversing the directory structure.

 

Deleting a File

To delete a file from the Backendless file storage, it must be identified by the file path/name. Files in the Backendless storage have the following URL structure:

https://api.backendless.com/<application id>/<REST API key>/files/<path>/<file name>

The API to delete a file uses the <path>/<filename> part to identify the file which must be deleted.

Asynchronous Method:

public void Backendless.Files.remove( String filePath, AsyncCallback<Void> responder )

Synchronous Method:

public void Backendless.Files.remove( String filePath ) throws Exception

where:

filePath - Path of the file to delete. The path must consist of the file path and file name.
responder - A responder object which receives a callback when the method successfully deleted the file or if an error occurs. Applies to the asynchronous methods only.

Example:

Backendless.Files.remove( "pictures/myphoto.png",
                           new AsyncCallback<Void>()
{
  @Override
  public void handleResponse()
  {
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
   
  }
}

 

Deleting a Directory

To delete a directory from the Backendless file storage, it must be identified by the its path. Directories in the Backendless storage have the following URL structure:

https://api.backendless.com/<application id>/<REST API Key>/files/<path>

The API to delete a directory uses the <path> element from the URL above.

 

Asynchronous Method:

public void Backendless.Files.removeDirectory( String path, 
                                               AsyncCallback<Void> responder )

Synchronous Method:

public void Backendless.Files.removeDirectory( String path ) throws Exception

where:

path - path of the directory to delete.
responder - a responder object which receives a callback when the method successfully deleted the directory or if an error occurs. Applies to the asynchronous methods only.

Example:

Backendless.Files.remove( "pictures", new AsyncCallback<Void>()
{
  @Override
  public void handleResponse()
  {
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
  }

 

Git Integration

Backendless file storage can also function as a git repository. This could be very convenient for deploying multiple files from the developer's computer with a single command. Git integration is disabled by default. To enable git for the file storage:

1. Open Backendless Console
2. Select your app/backend
3. Click Manage and scroll down to the Enable .git support section
4. Use the toggle to turn git integration on or off:
git-support

When the git integration is turned on, all files present in or uploaded to the file storage are immediately committed to the repository. This integration is bi-directional. It means that any files committed into the git repository by the means of git, will also be copied into the file storage. When git integration is being turned off, the git repository is deleted with all the associated history (the files remain in the file storage).

With the git integration enabled, a new folder (.git) appears in the File Browser on the Files screen. The folder contains the files from the git repository. When a file is uploaded to file storage either via the Upload API or using the File Browser, it is automatically committed to the repository. Likewise, when a file is pushed into the repository, it becomes available and visible in the file storage. The same applies to editing and deleting files either in the Backendless Console or in git repository.

When git is enabled, the repository is available at the following address:

https://git.backendless.com/<application id>/.git

where:

<application id> - application ID available in Backendless Console at Manage > App Settings.

When the Backendless backend is configured with a custom domain name, the repository URL is:

http://<custom domain name>/.git

The repository uses the same authentication as Backendless Console. That means all git commands must use the same developer email address and password as for logging in to Backendless Console.

It is important to note that any system level files created by git are also placed into the file storage (the .git directory). These files are accounted for when calculating the file space used by the app/backend.

Configuring Local Environment

You have no local files and there is a remote GIT repository:

There are files in the Backendless storage and there are no files locally:

Clone existing repository:

mkdir /path/to/your/project
cd /path/to/your/project
git clone http://git.backendless.com/<your application id>/.git
cd <your application id>

Adding a file locally and pushing to Backendless git:

> echo "First file" >> file.txt
> git add file.txt
> git commit -m 'Initial commit with new file'
> git push -u origin master

You have with an existing GIT project in your local environment:

This applies when you already have a local git project. You also enabled git integration in Backendless and need to "integrate" your local git project with the git repository in Backendless.

> cd /path/to/my/repo
> git remote add origin http://git.backendless.com/<your application id>/.git
 
# pushes the repo and its refs for the first time to Backendless git
> git push -u origin --all
 
# pushes any tags to Backendless git
> git push -u origin --tags

You have an existing FILE project in your local environment.

This applies when you have existing files locally and need to add them to the git repository you initialized in Backendless.

> cd /path/to/my/repo
> git init
> git remote add origin http://git.backendless.com/<your application id>/.git
> git pull -u origin master
> git add *
> git commit -m 'merge with existing project'
> git push -u origin master

Web Hosting

Backendless file storage includes a special directory which facilitates web hosting for the app/backend. The directory name is /web:

web-directory

The /web folder serves as the web server root. The web server is available at the following URLs:

With custom domain name enabled for the account:

http://custom domain name

Without custom domain name:

https://api.backendless.com/<application id>/<REST API key>/files/web

where:

<application id> - ID of the application which can be obtained from the Manage > App Settings screen of the Backendless Console
<REST API Key> - API Key assigned to the application by Backendless. It is available from the Manage > App Settings screen of the Backendless Console.

 

Custom Domain Name

Backendless File Service supports mapping of a custom domain name to the application's backend. As a result, once a domain name is mapped, the following backend's resources become available via the custom URL:

Service API endpoint. The default endpoint for all Backendless services is:

https://api.backendless.com

With a custom domain name, the endpoint is also available at:

http://<custom domain name>/api

Web Hosting. Backendless file storage contains a special directory - /web, which serves as the web site root. When a custom domain name is mapped to a Backendless application/backend, the contents of the /web directory are served for the HTTP requests with the domain name. See the Web Hosting section for additional details.
git endpoint. When the Backendless git integration is enabled, the git endpoint with a custom domain name is:

http://<custom domain name>/.git

Before a custom domain name is assigned to a Backendless application:

1. Create a CNAME record in DNS for your custom domain name
2. Map the CNAME record to develop.backendless.com
3. Open Backendless Console and select your application/backend.
4. Click Manage and scroll down to the "Custom Domain" section.
5. Enter the domain name into the text field and click Save
custom-domain.zoom50

Custom Web Template Hosting

A client-to-backend workflow may include interaction with web pages presented to the users of the application. Consider the following scenarios:

User registration. When a user registers with an application, he receives an email with a link to a page. Clicking the link acknowledges that the email address is valid and the user account is confirmed.
Password change. When a user requests password change (or password recovery), an email is sent to the user with a link to a web page where they can reset the password.
Session expiration. When a user session with the application expires, he is redirected to a webpage.

All these use cases have something in common - they all force the user to interact with a web page. The templates for these pages are available in the /web/templates path of the backend's file storage:

files-web-templates-dirs

The look and feel as well as the logic in the pages can be customized by modifying the HTML/CSS /JS files provided for each template. For example, the contents of the change_password folder is:

change-password-template

The "out-of-the-box" rendering of the pages is shown below:

Registration confirmation page:

backendless-registration-conf

Password change page:

backendless-password-change

Session expiration page:

session-expiration

Files Security

Access to files and directories can be restricted using permissions. Backendless supports the following permissions for files and directories:

Read - permission to download a file. This permission can be applied to a directory, in that case it applies recursively to all files contained therein.
Write - permission to upload a file or modify a directory by uploading files into it.
Remove - permission to delete a file or a directory.

To modify the permission matrix for a file or a directory, click the "Permissions" icon in file browser in Backendless Console:

files-directory-permissions

 

The permission assignment popup contains two sections: USER PERMISSIONS and ROLES PERMISSIONS which allow permission management either for a specific user account or for application roles.  To modify a permission for an operation for a user or a role, click the icon in the corresponding column. The icon has 3 states:

ico-checkmark-gray - inherit GRANT permission from the global permission matrix. This is the default permission.

ico-checkmark-green - explicit GRANT of the permission for the operation. Allows the user to perform the operation.

icon-deny - DENY permission for the operation. Restricts the user from performing the operation.

files-role-permissions.zoom70

Permissions API

Backendless security mechanism assigns an Access Control List (ACL) to every file in the file storage. An ACL defines users and user roles that have permissions to read, write, or delete a file. The Permissions API allows programmatic control over file ACL by granting or denying permissions to a file for a user or a user role. Using the API permissions can be applies for a user or user role individually or in "bulk" - for all users or user roles in a single call.

The path or the url argument in the APIs below must identify a file or directory for which the permission is modified. The value must be the short version of the path. For example, suppose the full URL of a file in the file storage system is:

https://api.backendless.com/31CB9FED-F34C-5541-FF26-6C2B6719F200/23432-A6B2-FF6B-31CB9FED/files/movies/vacation.mp4

The path to the file in the API call must contain only the directory and the file name (without the leading slash):

movies/vacation.mp4

 

The user account (or the role) on which behalf the API is executed must contain the Permission permission. For example, if the call is made by an authenticated user, the role for the user account would be AuthenticatedUser. The role must have the Permission permission in order for the API call to go through.

 

To grant access for a user

FilePermission.READ.grantForUser( userid, url, responder );
FilePermission.DELETE.grantForUser( userid, url, responder );
FilePermission.WRITE.grantForUser( userid, url, responder );

where:

userid - ID of a user, for which you want to grant the read, delete, or write permission.
url - path to a file, for which you want to specify the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To grant access for a user role

FilePermission.READ.grantForRole( rolename, url, responder );
FilePermission.DELETE.grantForRole( rolename, url, responder );
FilePermission.WRITE.grantForRole( rolename, url, responder );

where:

rolename - name of a user role, for which you want to grant the read, delete, or write permission.
url - path to a file, for which you want to specify the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To grant access for all users

FilePermission.READ.grantForAllUsers( url, responder );
FilePermission.DELETE.grantForAllUsers( url, responder );
FilePermission.WRITE.grantForAllUsers( url, responder );

where:

url - path to a file, for which you want to specify the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To deny access for a user

FilePermission.READ.denyForUser( userid, url, responder );
FilePermission.DELETE.denyForUser( userid, url, responder );
FilePermission.WRITE.denyForUser( userid, url, responder );

where:

userid - ID of a user, for which you want to deny the read, delete, or write permission.
url - path to a file, for which you want to specify the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To deny access for a user role

FilePermission.READ.denyForRole( rolename, url, responder );
FilePermission.DELETE.denyForRole( rolename, url, responder );
FilePermission.WRITE.denyForRole( rolename, url, responder );

where:

rolename - name of a user role, for which you want to deny the read, delete, or write permission.
url - path to a file, for which you want to specify the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

To deny access for all users

FilePermission.READ.denyForAllUsers( url, responder );
FilePermission.DELETE.denyForAllUsers( url, responder );
FilePermission.WRITE.denyForAllUsers( url, responder );

where:

url - path to a file, for which you want to specify the permission.
responder - a responder object which will receive a callback when the method successfully saves the object or if an error occurs. Applies  to the asynchronous method only.

 

Geolocation API

Overview

Backendless Geolocation Service is a system supporting management and search of geo points. A geo point in the most primitive format consists of a pair of coordinates: latitude and longitude. Optionally a geo point may contain metadata, which is a collection of arbitrary key/value pairs. A geo point belongs to a category, which is a logical grouping of geo points. The diagram bellow illustrates these concepts:

backendless-geo-point-concepts.jpeg

 

Backendless allows infinite number of geo points managed for an application. Geo points can be added via an API call or the import functionality in Backendless console. Once the backend is populated with geo points, the search API can be used to run the following types of geo queries:

Radius-based search - Searches for geo points in a circular map area defined by the coordinates of the central point and a radius. Backendless returns all geo points within the area.
backendless-geo-radius-search
Search in a rectangular map area - Searches for geo points in a rectangular map area identified by the coordinates of two corners defining the area (North West and South East):
backendless-geo-rectangle-search

 

Additionally, the geo search API supports the following search options available in the APIs:

Filtering by categories - Both types of search (radius-based and rectangular) can specify the categories in which the backend should search for the geo points.
Query-based search - The metadata associated with the geo points can be used in queries which should be formatted using the SQL-92 syntax. For example, the geo point shown in the image above can be discovered with the following queries:

cuisine = 'French'

cuisine LIKE 'Fr%' and Atmosphere = 'Casual'

cuisine = 'French' and (Price = '$$$$' or Price = '$$$')

Relative search - Runs a search for a subset of metadata key/value pairs to match up to the specified threshold value. The threshold must be expressed as a percentage of matches.

 

Adding a GeoPoint

This API adds a geo point to the backend geo location storage. Once a geo point is added, it becomes searchable through all geopoint search mechanisms supported by Backendless (search in radius, search in a rectangular area, search in a category). At the present moment there are two ways to add geo points: (1) using the API documented below or (2) using the Backendless console's import function.

 

 

Synchronous Methods:

public GeoPoint Backendless.Geo.savePoint( GeoPoint geoPoint ) throws BackendlessException

public GeoPoint Backendless.Geo.savePoint( double latitude, 
                                           double longitude, 
                                           Map<String, Object> metadata ) 
                                                  throws BackendlessException

public GeoPoint Backendless.Geo.savePoint( double latitude, 
                                           double longitude, 
                                           List<String> categories, 
                                           Map<String, Object> metadata ) 
                                                  throws BackendlessException

Asynchronous Method:

public GeoPoint Backendless.Geo.savePoint( GeoPoint geoPoint, 
                                           AsyncCallback<GeoPoint> responder ) 
                                                           throws BackendlessException

public GeoPoint Backendless.Geo.savePoint( double latitude, 
                                           double longitude, 
                                           Map<String, Object> metadata, 
                                           AsyncCallback<GeoPoint> responder ) 
                                                         throws BackendlessException

public GeoPoint Backendless.Geo.savePoint( double latitude, 
                                           double longitude, 
                                           List<String> categories, 
                                           Map<String, Object> metadata, 
                                           AsyncCallback<GeoPoint> responder ) 
                                                            throws BackendlessException

where:

geoPoint - a strongly typed object defining the properties of a geo point to add. See the class definition in the "Return Value" section.
latitude - latitude of the point to add.
longitude - longitude of the point to add.
categories - list of categories the point is added to. If a category does not exist at the time when a point is added, Backendless creates the category and adds the point to it. For the methods without this argument, the point is added to the "Default" category.
metadata - metadata associated with the geo point. Must be an arbitrary collection (map) of key/value pairs. Accepted values for this parameter are: String, Number (integer and double), and Data Service objects. Date values must be represented as number in the Unix timestamp format (number of milliseconds since January 1, 1970 at UTC). Learn more about using date in search queries for category, radius, or rectangular area search.
responder - a responder object which receives a callback when the method successfully adds the geo point. Applies to the asynchronous methods only.

 

Return Value:

An instance of the GeoPoint class representing the new geo point added to the application's geo location storage. Below is a brief version of the class. The complete source code listing is available in the Backendless github repository.

Example:

List<String> categories = new ArrayList<String>()
categories.add( "restaurants" );
categories.add( "cool_places" );

Map<String, Object> meta = new HashMap<String, Object>();
meta.put( "name", "Eatzi's" );

Backendless.Geo.savePoint( 32.81, -96.80, categories, meta, 
                               new AsyncCallback<GeoPoint>()
  {
    @Override
    public void handleResponse( GeoPoint geoPoint )
    {
      Log.i( "MYAPP", geoPoint.getObjectId() );
    }

    @Override
    public void handleFault( BackendlessFault backendlessFault )
    {
    
    }
  });
}

 

Updating a GeoPoint

Geo update API relies on the same methods used for Adding a Geo Point. The primary difference is in order to update a geo point it must have the objectId property assigned by Backendless (when the geopoint is created or imported). The semantics of the properties in an update request is as follows:

objectId is a required property.
All other properties (latitude, longitude, categories, metadata) are optional, but at least one must contain a value.
If latitude or longitude contain values, the new values replace the existing ones.
If categories contains a value, the geo point is moved to the specified categories (with coordinates and metadata).
If categories is null, the geo point stays in the current category.
If metadata is null, the geo point keeps the current metadata.
If metadata contains any key/value pairs, the new metadata replaces the existing one.
If metadata is an empty object/dictionary, the existing metadata is removed.

Deleting a GeoPoint

There are two ways to delete a geopoint from the Geolocation storage:

 

Deleting a GeoPoint using the Backendless Console

To delete a geo point using the Backendless Console:

1. Log in to the Backendless Console, select your app and click the Geolocation icon.
2. Select a geo category containing the geopoint to be deleted.
3. Click the checkboxes next to the geopoint(s) which should be deleted.
4. Click Delete Selected from the button bar as shown below:
geo-delete-geopoints.zoom50
5. Click Delete in the confirmation popup to confirm the deletion.
6. A confirmation notification will appear in the top right corner.

 

Deleting a GeoPoint with the API

Synchronous Method:

public void Backendless.Geo.removePoint( GeoPoint geoPoint )

Asynchronous Method:

public void Backendless.Geo.removePoint( GeoPoint geoPoint, AsyncCallback<Void> responder )

where:

geoPoint - a geopoint object to delete from the Geolocation storage.
responder - a responder object which receives a callback when the method successfully delete the geopoint. Applies to the asynchronous method only.

Return Value:

None

 

Example:

The code below demonstrates how to delete a geopoint. A geopoint is added first, then subsequently deleted.
Synchronous API sample:

GeoPoint geoPoint = new GeoPoint( -31.96, 115.84 ); 
// save a geopoint (so there is something to delete)
GeoPoint savedGeoPoint = Backendless.Geo.savePoint( geoPoint );
 
// now delete the saved geopoint
Backendless.Geo.removePoint( savedGeoPoint ); 

Asynchronous API sample:

GeoPoint geoPoint = new GeoPoint( -31.96, 115.84 );

// save a geopoint, so there is something to delete 
Backendless.Geo.savePoint( geoPoint, new AsyncCallback<GeoPoint>() 
{ 
  @Override 
  public void handleResponse( GeoPoint savedGeoPoint ) 
  { 
    // now delete the saved geopoint
    Backendless.Geo.removePoint( savedGeoPoint, new AsyncCallback<Void>() 
    { 
      @Override 
      public void handleResponse( Void response ) 
      { 
        // GeoPoint has been deleted 
      } 

      @Override 
      public void handleFault( BackendlessFault fault ) 
      { 
        // an error has occurred, the error code can be retrieved with fault.getCode() 
      } 
    } ); 
  } 
  @Override 
  public void handleFault( BackendlessFault fault ) 
  { 
    // an error has occurred, the error code can be retrieved with fault.getCode() 
  } 
});

 

Adding a Geo Category

This API creates a geo category. A geo category is a logical grouping of geo points. Category name may contain the following literals: a-z, A-Z, numbers 0-9 and the underscore (_ ) character. The name must start with a literal. Category names can be inspected using Backendless Console (see the image below) or using the API call retrieving a list of categories.

 

Adding Categories in Console

Backendless Console supports adding a category via the graphical interface. To create a category:

1. Login to Backendless Console and select your app/backend.
2. Click the Geolocation icon in the menu on the left.
3. Use the "plus" icon located above the section containing the list of categories:
create-new-geo-cat
4. Enter the category name in the popup and click "Save".

 
Adding Categories with the API

Synchronous Method:

public GeoCategory Backedless.Geo.addCategory( String categoryName ) throws BackendlessException

Asynchronous Method:

public void Backedless.Geo.addCategory( String categoryName, AsyncCallback<GeoCategory> responder )

where:

categoryName - name of the category to create.
responder - a responder object which receives a callback when the method successfully creates the category or if an error occurs. Applies to the asynchronous methods only.

Return Value:

An instance of GeoCategory representing the new category. The object contains properties with category's objectId, name and number of geo points in it:
 

Example:

Backendless.Geo.addCategory( "mycategory", new AsyncCallback<GeoCategory>()
{
  @Override
  public void handleResponse( GeoCategory category )
  {
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
   
  }
}

 

Deleting a Geo Category

This API deletes a geo category. If the category does not exist, the service returns an error.
 

Removing Categories in Console

Backendless Console supports category deletion via the graphical interface. To delete a category:

1. Login to Backendless Console
2. Select your app/backend.
3. Click the Geolocation icon in the menu on the left.
4. Use the "delete" icon in the section above the list of categories:
delete-geo-category
5. Select DELETE in the confirmation popup.

 
Deleting Categories with the API
Synchronous Method:

public boolean Backedless.Geo.deleteCategory( String categoryName ) throws BackendlessException

Asynchronous Method:

public void Backedless.Geo.deleteCategory( String categoryName, AsyncCallback<Boolean> responder )

where:

categoryName - name of the category to delete.
responder - a responder object which receives a callback when the method successfully deletes the category or if an error occurs. Applies to the asynchronous methods only.

Return Value:

true if the category is deleted, false otherwise.

Example:

Backendless.Geo.deleteCategory( "mycategory", new AsyncCallback<Boolean>()
{
  @Override
  public void handleResponse( Boolean result )
  {
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
   
  }
}

 

Retrieving Geo Categories

This API retrieves a list of all the application's geo categories.

 

Synchronous Method:

public List<GeoCategory> Backedless.Geo.getCategories() throws BackendlessException

Asynchronous Method:

public void Backedless.Geo.addCategory( AsyncCallback<List<GeoCategory>> responder )

where:

responder - a responder object which receives a callback when the method successfully retrieves the categories contained in the application or if an error occurs. Applies to the asynchronous methods only.

Return Value:

A list of the GeoCategory objects. Each GeoCategory instance represents a category with the properties of objectId, name and number of geo points in it.

package com.backendless.geo;

public class GeoCategory implements Comparable<GeoCategory>
{
  private String objectId;
  private String name;
  private int size;

  // returns objectId assigned to the category
  public String getId()
  {
    return objectId;
  }

  // returns category name
  public String getName()
  {
    return name;
  }

  // returns number of geo points in the category
  public int getSize()
  {
    return size;
  }

  public void setName( String name )
  {
    this.name = name;
  }

  @Override
  public boolean equals( Object o )
  {
    if( this == o )
      return true;

    if( o == null || getClass() != o.getClass() )
      return false;

    GeoCategory that = (GeoCategory) o;

    if( !name.equals( that.name ) )
      return false;

    if( objectId != null ? !objectId.equals( that.objectId ) : that.objectId != null )
      return false;

    return true;
  }

  @Override
  public int hashCode()
  {
    int result = objectId != null ? objectId.hashCode() : 0;
    result = 31 * result + name.hashCode();
    return result;
  }

  @Override
  public String toString()
  {
    final StringBuffer sb = new StringBuffer();
    sb.append( "GeoCategory" );
    sb.append( "{name='" ).append( name ).append( '\'' );
    sb.append( ", size=" ).append( size );
    sb.append( '}' );
    return sb.toString();
  }

  @Override
  public int compareTo( GeoCategory arg )
  {
    if( this == arg )
      return 0;

    int nameDiff = this.name == null ? 
                   (arg.getName() == null ? 0 : -1) : 
                   this.name.compareTo( arg.getName() );

    if( nameDiff != 0 )
      return nameDiff;

    return size - arg.getSize();
  }
}

 

Example:

Backendless.Geo.getCategories(new AsyncCallback<List<GeoCategory>>()
{
  @Override
  public void handleResponse( List<GeoCategory> category )
  {
  }

  @Override
  public void handleFault( BackendlessFault backendlessFault )
  {
   
  }
}

 

Get Geopoint Count

// TODO

Importing Geo Data

Backendless console supports bulk import of geo points with metadata. The import procedure automatically places the geo points into the specified categories. The raw data must be in a comma separated values (CSV) format. For more details about geopoint import see the Importing for Geo Service section of the guide.

Search in Category

This API supports two types of geo searches:

Search in one or more geo categories.
Search based on metadata properties in one or more categories .

 

Methods:

 

Synchronous Method:

public List<GeoPoint> Backedless.Geo.getPoints( BackendlessGeoQuery geoQuery ) 
                                                       throws BackendlessException

Asynchronous Method:

public void Backendless.Geo.getPoints( BackendlessGeoQuery geoQuery,
                                       AsyncCallback<List<GeoPoint>> responder )

where:

geoQuery - a query object encapsulating the search parameters. See the Running Search Queries section below for details.
responder - a responder object which receives a callback when the method successfully returns search result or if an error occurs. Applies to the asynchronous method only.

Return Value:

A collection of GeoPoint objects matching the search query. Each GeoPoint instance contains:

objectId - an ID assigned to geo point by Backendless when it is saved in the backend geo location storage.
latitude - latitude of the geo point.
longitude - longitude of the geo point.
categories - an array of geo categories the point belong to.
metadata - metadata associated with the geo point. Accepted values for this parameter are: String, Number (integer and double), and Data Service objects. Date values must be represented as number in the Unix timestamp format (number of milliseconds since January 1, 1970 at UTC). Learn more about using date in search queries for category, radius, or rectangular area search.

 

Since the search query may produce a large number of geo points, not all of them are returned at once. Instead, all found geo points are divided into 'pages'. The size of each page is determined by the pageSize parameter in the query object. The first response returns the first page. The query object includes methods for adjusting page size and offset. Subsequent search requests with the query object will return additional pages of data.

 

Calculating offset

All geo points in the entire search result are indexed. The index of the first geo point is 0. The offset parameter in the query object specifies the index from which to load the next page of geo points. For example, suppose the entire search result is 200 points. If the initial pageSize is 20, then only 20 geo points are returned in the first response. To get the second page of geo points, they should be loaded from offset 20, third from 40 and so on. The formula for calculating the offset is:
 [value of offset in the current response] + [size of current page ].

 

Running Search Queries

The geo query object includes multiple parameters. Depending on which parameters contain values, the semantics of the search would change. Any search must be performed within at least one category. If no category names are provided, the search is performed in the Default category.

 

Search in categories

To search in one or more categories without any constraints on metadata or proximity to a center point, simply set the names of the categories in the query object. The request returns all geo points divided into pages of data, one page at a time.

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "Restaurants" );
List<GeoPoint> geoPoints = Backendless.Geo.getPoints( geoQuery );

Search in categories with metadata

Metadata-based search finds all geo points which match all specified metadata properties in the given categories. The example below searches for the geo points in the Restaurants category with metadata containing "Cuisine = French" and "Atmosphere = Romantic".

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "Restaurants" );
HashMap<String, String> metaSearch = new HashMap<String, String>();
metaSearch.put( "Cuisine", "French"  );
metaSearch.put( "Athmosphere", "Romantic"  );
geoQuery.setMetadata( metaSearch );
List<GeoPoint> geoPoints = Backendless.Geo.getPoints( geoQuery );

 
 

Using dates in where clause when searching in categories

The search query used to retrieve geo points may reference date values. These values must be stored as a number of milliseconds since January 1st, 1970 at UTC. The example below demonstrates the usage of a date/time timestamp in a search query:
 

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "Restaurants" );
HashMap<String, String> metaSearch = new HashMap<String, String>();
metaSearch.put( "Cuisine", "French"  );
metaSearch.put( "Athmosphere", "Romantic"  );
geoQuery.setMetadata( metaSearch );
List<GeoPoint> geoPoints = Backendless.Geo.getPoints( geoQuery );

 

Search in category by date by using synchronous call:

public void searchByDateInCategorySync() throws ParseException
{
  try
  {
    // date
    SimpleDateFormat dateFormat = new SimpleDateFormat( "dd.MM.yyyy 'at' HH:mm" );
    Date updated = dateFormat.parse( "17.01.2015 at 12:00" );

    // create
    GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
    geoPoint.addCategory( "Coffee" );
    geoPoint.addMetadata( "Name", "Starbucks" );
    geoPoint.addMetadata( "Parking", true );
    geoPoint.addMetadata( "updated", new Date().getTime() );

    geoPoint = Backendless.Geo.savePoint( geoPoint );
    System.out.println( String.format( "searchByDateInCategory -> point: %s", 
                                       geoPoint ) );

    // search
    BackendlessGeoQuery query = new BackendlessGeoQuery( Arrays.asList( "Coffee" ) );
    query.setWhereClause( String.format( "updated > %d", updated.getTime() ) );
    query.setIncludeMeta( true );
    BackendlessCollection<GeoPoint> points = Backendless.Geo.getPoints( query );

    System.out.println( String.format( "searchByDateInCategory GETPOINTS: %s", 
                                       points.getCurrentPage() ) );
  }
  catch( BackendlessException e )
  {
    Log.e( "MYAPP", String.format( "searchByDateInCategory FAULT = %s", e ) );
  }
}

Search in category by date by using asynchronous call:

public void searchByDateInCategoryAsync() throws ParseException
{
  // date
  SimpleDateFormat dateFormat = new SimpleDateFormat( "dd.MM.yyyy 'at' HH:mm" );
  final Date updated = dateFormat.parse( "17.01.2015 at 12:00" );

  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.addCategory( "Coffee" );
  geoPoint.addMetadata( "Name", "Starbucks" );
  geoPoint.addMetadata( "Parking", true );
  geoPoint.addMetadata( "updated", new Date().getTime() );

  Backendless.Geo.savePoint( geoPoint, new AsyncCallback<GeoPoint>()
  {
    @Override
    public void handleResponse( GeoPoint geoPoint )
    {
      Log.i( "MYAPP", String.format( "searchByDateInCategory -> point: %s", 
                                     geoPoint ) );

      // search
      BackendlessGeoQuery query = new BackendlessGeoQuery( Arrays.asList( "Coffee" ) );
      query.setWhereClause( String.format( "updated > %d", updated.getTime() ) );
      query.setIncludeMeta( true );
      Backendless.Geo.getPoints( query, new AsyncCallback<List<GeoPoint>>()
      {
        @Override
        public void handleResponse( List<GeoPoint> points )
        {
          Log.i( "MYAPP", String.format( "searchByDateInCategory GETPOINTS: %s", 
                                          points.getCurrentPage() ) );
        }

        @Override
        public void handleFault( BackendlessFault fault )
        {
          Log.i( "MYAPP", String.format( "searchByDateInCategory FAULT = %s", 
                                         fault ) );
        }
      } );
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
      Log.i( "MYAPP", String.format( "searchByDateInCategory SAVEPOINT: %s", 
                                     fault ) );
    }
  } );
}

Requesting meta in response

Geo points returned in the search results do not include their metadata properties by default. The search query object includes a property which can be used to request the metadata to be included. This property can be used with any search options described above. For example, the following code runs a search in a category and requests the metadata to be included:

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "Restaurants" );
geoQuery.setIncludeMeta( true );
List<GeoPoint> geoPoints = Backendless.Geo.getPoints( geoQuery );

 

Search in Radius

This API supports multiple types of geo searches:

Search for geo points located within specified distance (radius) from a given point.
Search in radius based on metadata.

 

 

Synchronous Method:

public List<GeoPoint> Backedless.Geo.getPoints( BackendlessGeoQuery geoQuery ) 
                                                       throws BackendlessException

Asynchronous Method:

public void getPoints( BackendlessGeoQuery geoQuery,
                       AsyncCallback<List<GeoPoint>> responder )

where:

geoQuery - a query object encapsulating the search parameters. See the Running Search Queries section below for details.
responder - a responder object which receives a callback when the method successfully returns search result or if an error occurs. Applies to the asynchronous method only.

 

Return Value:

A collection of GeoPoint objects matching the search query. Each GeoPoint object contains the following properties:

objectId - ID assigned to geo point by Backendless when it is saved in the backend geo location storage.
latitude - latitude of the geo point.
longitude - longitude of the geo point.
categories - an array of geo categories the point belong to.
metadata - metadata associated with the geo point. Accepted values for this parameter are: String, Number (integer and double), and Data Service objects. Date values must be represented as number in the Unix timestamp format (number of milliseconds since January 1, 1970 at UTC). Learn more about using date in search queries for category, radius, or rectangular area search.
distance - distance between the center point referenced in the search query and the geo point. The distance is always in units specified in the search query.

 

Since the search query may produce a large number of geo points, not all of them are returned at once. Instead, all found geo points are divided into 'pages'. The size of each page is determined by the pageSize parameter in the query object. The first response returns the first page. The query object includes methods for adjusting page size and offset. Subsequent search requests with the query object will return additional pages of data.

 

Calculating offset

All geo points in the entire search result are indexed. The index of the first geo point is 0. The offset parameter in the query object specifies the index from which to load the next page of geo points. For example, suppose the entire search result is 200 points. If the initial pageSize is 20, then only 20 geo points are returned in the first response. To get the second page of geo points, they should be loaded from offset 20, third from 40 and so on. The formula for calculating the offset is:
 [value of offset in the current response] + [size of current page ].

 

The geo points in the search results will be sorted by their proximity to the central point (center of the radius): the geo points that are closest to the central point will be listed first.

 

Running Search Queries

The geo query object includes multiple parameters, none of them are required. As a result, depending on which parameters contain values, the semantics of the search would change. Any search must be performed within at least one category. If no category names are provided, the search is performed in the Default category.

 

Search in categories with radius

Radius-based search establishes a circular area by setting the coordinates of a central point and a distance (radius). Backendless searches for geo points in the specified distance from the coordinates in the center and includes them into the search result. The value of the distance is interpreted based in the units parameter, which can be METERS, KILOMETERS, MILES, YARDS, FEET:

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "Restaurants" );
geoQuery.setLatitude( 41.38 );
geoQuery.setLongitude( 2.15 );
geoQuery.setRadius( 10000d );
geoQuery.setUnits( Units.METERS );
List<GeoPoint> geoPoints = Backendless.Geo.getPoints( geoQuery );

Search in categories with radius and metadata

This is the same as above, with the difference that the search result includes only geo points with the matching metadata:

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "Restaurants" );
geoQuery.setLatitude( 41.38 );
geoQuery.setLongitude( 2.15 );
geoQuery.setRadius( 10000d );
geoQuery.setUnits( Units.METERS );

HashMap<String, String> metaSearch = new HashMap<String, String>();
metaSearch.put( "Cuisine", "French"  );
metaSearch.put( "Athmosphere", "Romantic"  );
geoQuery.setMetadata( metaSearch );

List<GeoPoint> geoPoints = Backendless.Geo.getPoints( geoQuery );

 

Using dates in where clause when searching in radius

The search query used to retrieve geo points may reference date values. These values must be stored as a number of milliseconds since January 1st, 1970 at UTC. The example below demonstrates the usage of a date/time timestamp in a search condition:

 

Search in radius by date by using synchronous call:

public void searchByDateInRadiusSync() throws ParseException
{
  try
  {
    // date
    SimpleDateFormat dateFormat = new SimpleDateFormat( "dd.MM.yyyy 'at' HH:mm" );
    Date updated = dateFormat.parse( "17.01.2015 at 12:00" );

    // create
    GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
    geoPoint.setCategories( Arrays.asList( "City", "Coffee" ) );
    geoPoint.addMetadata( "Name", "Starbucks" );
    geoPoint.addMetadata( "City", "Honolulu" );
    geoPoint.addMetadata( "Parking", true );
    geoPoint.addMetadata( "updated", new Date().getTime() );

    geoPoint = Backendless.Geo.savePoint( geoPoint );
    Log.i( "MYAPP", String.format( "searchByDateInRadius -> point: %s", geoPoint ) );

    // search
    BackendlessGeoQuery query = new BackendlessGeoQuery( 21.30, -157.85, 50, 
                                                         Units.KILOMETERS );
    query.addCategory( "City" );
    query.setWhereClause( String.format( "updated > %d", updated.getTime() ) );
    query.setIncludeMeta( true );

    List<GeoPoint> points = Backendless.Geo.getPoints( query );
    Log.i( "MYAPP", String.format( "searchByDateInRadius GETPOINTS: %s", 
                                   points.getCurrentPage() ) );
  }
  catch( BackendlessException e )
  {
    Log.e( "MYAPP", String.format( "searchByDateInRadius FAULT = %s", e ) );
  }
}

Search in radius by date by using asynchronous call:

public void searchByDateInRadiusAsync() throws ParseException
{
  // date
  SimpleDateFormat dateFormat = new SimpleDateFormat( "dd.MM.yyyy 'at' HH:mm" );
  final Date updated = dateFormat.parse( "17.01.2015 at 12:00" );

  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.setCategories( Arrays.asList( "City", "Coffee" ) );
  geoPoint.addMetadata( "Name", "Starbucks" );
  geoPoint.addMetadata( "City", "Honolulu" );
  geoPoint.addMetadata( "Parking", true );
  geoPoint.addMetadata( "updated", new Date().getTime() );

  Backendless.Geo.savePoint( geoPoint, new AsyncCallback<GeoPoint>()
  {
    @Override
    public void handleResponse( GeoPoint geoPoint )
    {
      Log.i( "MYAPP", String.format( "searchByDateInRadius -> point: %s", geoPoint ) );

      // search
      BackendlessGeoQuery query = new BackendlessGeoQuery( 21.30, -157.85, 50, 
                                                           Units.KILOMETERS );
      query.addCategory( "City" );
      query.setWhereClause( String.format( "updated > %d", updated.getTime() ) );
      query.setIncludeMeta( true );

      Backendless.Geo.getPoints( query, new AsyncCallback<List<GeoPoint>>()
      {
        @Override
        public void handleResponse( List<GeoPoint> points )
        {
          Log.i( "MYAPP", String.format( "searchByDateInRadius GETPOINTS: %s", 
                                         points.getCurrentPage() ) );
        }

        @Override
        public void handleFault( BackendlessFault fault )
        {
          Log.e( "MYAPP", String.format( "searchByDateInRadius FAULT = %s", fault ) );
        }
      } );
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
      Log.e( "MYAPP", String.format( "searchByDateInRadius SAVEPOINT: %s", fault ) );
    }
  } );
}

 

Requesting meta in response

Geo points returned in the search results do not include their metadata properties by default. The search query object includes a property which can be used to request the metadata to be included. This property can be used with any search options described above. The syntax for requesting metadata in response is described in the Search in Category section.

Search in Rectangular Area

This API runs a search within a rectangular area of the map. The area is defined with the coordinates of the North West and South East corners of the map rectangle.

Synchronous Method:

public List<GeoPoint> Backedless.Geo.getPoints( BackendlessGeoQuery geoQuery ) 
                                                       throws BackendlessException

Asynchronous Method:

public void getPoints( BackendlessGeoQuery geoQuery,
                       AsyncCallback<BackendlessCollection<GeoPoint>> responder )

where:

geoQuery - a query object encapsulating the search parameters. See the Running Search Queries section below for details.
responder - a responder object which receives a callback when the method successfully returns search result or if an error occurs. Applies to the asynchronous method only.

 

Return Value:

A collection of GeoPoint objects matching the search query. Each GeoPoint instance contains the following properties:

objectId - ID assigned to geo point by Backendless when it is saved in the backend geo location storage.
latitude - latitude of the geo point.
longitude - longitude of the geo point.
categories - an array of geo categories the point belong to.
metadata - metadata associated with the geo point. Accepted values for this parameter are: String, Number (integer and double), and Data Service objects. Date values must be represented as number in the Unix timestamp format (number of milliseconds since January 1, 1970 at UTC). Learn more about using date in search queries for category, radius, or rectangular area search.
distance - distance between the center point referenced in the search query and the geo point. The distance is always in units specified in the search query.

 

Geo points returned from the search query are contained inside of BackendlessCollection. Since the search query may produce a large number of geo points, not all of them are returned at once. Instead, all found geo points are divided into 'pages'. The size of each page is determined by the pageSize parameter in the query object. The first response returns the first page. The collection class includes methods for loading additional pages. BackendlessCollection also includes the total number of all geo points found by the search operation (the totalObjects value).

All geo points in the entire search result are indexed. The index of the first geo point is 0. The offset parameter in the query object and in some method of BackendlessCollection specifies the index from which to load the next page of geo points. For example, suppose the entire search result is 200 points (the totalObjects value returned in the collection is 200). If the initial pageSize is 20, then only 20 geo points are returned in the first response. To get the second page of geo points, they should be loaded from offset 20, third from 40 and so on. The formula for calculating the offset is:
 [value of offset in the current response] + [size of current page ].

Running Search Queries

The geo query object includes multiple parameters, however, only the coordinates defining the rectangular area are required.  A search query must be performed within at least one category. If no category names are provided, the search is performed in the Default category.

 

Search in a rectangle in categories

Rectangle-based search establishes a geographic area by setting the coordinates of the North West and South East corners of the area. Backendless searches for geo points in the specified area and includes them into the search result:

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "Restaurants" );
geoQuery.setSearchRectangle( new double[32.78, -96.8, 25.79, -80.22] );
BackendlessCollection<GeoPoint> geoPoints = Backendless.Geo.getPoints( geoQuery );

Search in categories in a rectangular area and metadata

This is the same as above, with the difference that the search result includes only geo points with the matching metadata:

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "Restaurants" );

HashMap<String, String> metaSearch = new HashMap<String, String>();
metaSearch.put( "Cuisine", "French"  );
metaSearch.put( "Athmosphere", "Romantic"  );
geoQuery.setMetadata( metaSearch );

geoQuery.setSearchRectangle( new double[32.78, -96.8, 25.79, -80.22] );
BackendlessCollection<GeoPoint> geoPoints = Backendless.Geo.getPoints( geoQuery );

Using dates in where clause

The search query used to retrieve geo points may reference date values. These values must be stored as a number of milliseconds since January 1st, 1970 at UTC. The example below demonstrates the usage of a date/time timestamp in a search query:

 

Search in rectangular area by date by using synchronous call:

public void searchByDateInRectangularAreaSync() throws ParseException
{
  // date
  SimpleDateFormat dateFormat = new SimpleDateFormat( "dd.MM.yyyy 'at' HH:mm" );
  Date opened = dateFormat.parse( "17.01.2015 at 07:00" );
  Date closed = dateFormat.parse( "17.01.2015 at 23:00" );
  Date now = dateFormat.parse( "17.01.2015 at 15:20" );

  try
  {
    // create
    GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
    geoPoint.addCategory( "Coffee" );
    geoPoint.addMetadata( "Name", "Starbucks" );
    geoPoint.addMetadata( "opened", opened.getTime() );
    geoPoint.addMetadata( "closed", closed.getTime() );

    geoPoint = Backendless.Geo.savePoint( geoPoint );
    Log.i( "MYAPP", String.format( "searchByDateInRectangularArea -> point: %s", 
                                   geoPoint ) );

    // search
    GeoPoint nortWestCorner = new GeoPoint( 21.306944 + 0.5, -157.858333 - 0.5 );
    GeoPoint southEastCorner = new GeoPoint( 21.306944 - 0.5, -157.858333 + 0.5 );
    BackendlessGeoQuery query = new BackendlessGeoQuery( nortWestCorner, 
                                                         southEastCorner );
    query.addCategory( "Coffee" );
    query.setWhereClause( String.format( "opened < %d AND closed > %d", 
                          now.getTime(), now.getTime() ) );
    query.setIncludeMeta( true );

    List<GeoPoint> points = Backendless.Geo.getPoints( query );
    Log.i( "MYAPP", String.format( "searchByDateInRectangularArea GETPOINTS: %s", 
                                    points.getCurrentPage() ) );
  }
  catch( BackendlessException e )
  {
    Log.e( "MYAPP", String.format( "searchByDateInRectangularArea FAULT = %s", e ) );
  }
}

Search in rectangular area by date by using asynchronous call:

public void searchByDateInRectangularAreaAsync() throws ParseException
{
  // date
  SimpleDateFormat dateFormat = new SimpleDateFormat( "dd.MM.yyyy 'at' HH:mm" );
  Date opened = dateFormat.parse( "17.01.2015 at 07:00" );
  Date closed = dateFormat.parse( "17.01.2015 at 23:00" );
  final Date now = dateFormat.parse( "17.01.2015 at 15:20" );

  // create
  GeoPoint geoPoint = new GeoPoint( 21.306944, -157.858333 );
  geoPoint.addCategory( "Coffee" );
  geoPoint.addMetadata( "Name", "Starbucks" );
  geoPoint.addMetadata( "opened", opened.getTime() );
  geoPoint.addMetadata( "closed", closed.getTime() );

  Backendless.Geo.savePoint( geoPoint, new AsyncCallback<GeoPoint>()
  {
    @Override
    public void handleResponse( GeoPoint geoPoint )
    {
      Log.i( "MYAPP", String.format( "searchByDateInRectangularArea -> point: %s", 
                                     geoPoint ) );

      // search
      GeoPoint nortWestCorner = new GeoPoint( 21.306944 + 0.5, -157.858333 - 0.5 );
      GeoPoint southEastCorner = new GeoPoint( 21.306944 - 0.5, -157.858333 + 0.5 );
      BackendlessGeoQuery query = new BackendlessGeoQuery( nortWestCorner, 
                                                           southEastCorner );
      query.addCategory( "Coffee" );
      query.setWhereClause( String.format( "opened < %d AND closed > %d", 
                                           now.getTime(), now.getTime() ) );
      query.setIncludeMeta( true );

      Backendless.Geo.getPoints( query, new AsyncCallback<List<GeoPoint>>()
      {
        @Override
        public void handleResponse( List<GeoPoint> points )
        {
          Log.i( "MYAPP", String.format( "searchByDateInRectangularArea GETPOINTS: %s", 
                                         points.getCurrentPage() ) );
        }

        @Override
        public void handleFault( BackendlessFault fault )
        {
          Log.e( "MYAPP", String.format( "searchByDateInRectangularArea FAULT = %s", fault ) );
        }
      } );
    }

    @Override
    public void handleFault( BackendlessFault fault )
    {
      System.err.println( String.format( "searchByDateInRectangularArea SAVEPOINT: %s", fault ) );
    }
  } );
}

Requesting meta in response

Geo points returned in the search results do not include their metadata properties by default. The search query object includes a property which can be used to request the metadata to be included. This property can be used with any search options described above. The syntax for requesting metadata in response is described in the Search in Category section.

GeoPoint Clustering

Geo point search in a category, radius or a rectangular area may return too many geo points within close geographic proximity from each other. This might be difficult to view and process in a client application. To address this problem, Backendless supports the geo clustering feature. A geo cluster is a group of several geo points located close to each other. The two screenshots below demonstrate the advantages of clustering: the picture on the top displays search results in the Backendless Console with clustering turned off and the one on the bottom displays search results as clusters when clustering has been enabled:

 

Geo Points View:

clustering-points.zoom50

 

Clusters and Points View:

clustering-clusters.zoom50

 

Backendless creates clusters by splitting the map into a grid of squares. Geo points which belong to a square are placed into the same cluster. When a square contains only one point, it remains non-clustered.

 

Testing Geo Clustering in Backendless Console

The Geolocation page displays non-clustered geo points by default. To see how geoclustering works and test geoclustering search results:

1. Log in to Backendless Console, select an application and click the Geolocation icon.
2. Click the Map-driven navigation toggle. The toggle changes how the geo points are loaded from the backend. In the map-driven mode console loads the geo points for the visible area of the map.
3. Click the Geo Clustering toggle to enable clustering.
4. Console reloads geo points and clusters for the current viewport of the map and displays the results. A cluster is visualized as a blue marker on the map with a number indicating how many geo points it represents.
geofence-screen7.zoom50

 

Geo clustering is also available with the "Search in Radius" option, which searches for geo points in a circular area. To enable this functionality, click the Search in radius toggle:

geofence-screen8.zoom50

 

If you want to see the geo points in a cluster, zoom in the map or double-click a cluster's marker. Zooming the map in too much (when using the clustering along with the search in radius) may result that the search radius will be much bigger than the visible part of the map on the screen. In this case, zoom out so you can adjust the circle position and/or radius.

 

Clicking a cluster's marker will reveal the coordinates and combined metadata of all geopoints in the cluster.

geofence-screen9.zoom50

Retrieving Clustered Geo Points

Geo point clustering can be requested by setting clustering parameters in BackendlessGeoQuery, which is used in the calls to retrieve geo points from a category, radius or rectangular area.

 

public void setClusteringParams(double westLongitude, 
                                double eastLongitude, 
                                int mapWidth, 
                                int clusterGridSize )  

// same as above, but uses the default clusterGridSize of 100
public void setClusteringParams(double westLongitude, 
                                double eastLongitude, 
                                int mapWidth ).

where

westLongitude - the longitude of any point on the western boundary of the map in degrees.
eastLongitude - the longitude of any point on the eastern boundary of the map in degrees.
mapWidth - the size of the viewing area of the map in pixels.
clusterGridSize - the size in pixels of the grid's squares used to group points into clusters. The default value is 100 pixels

Example:

Once the clustering parameters are set, the geo point search API will return clustered geo points. The return value is a collection of GeoCluster and/or GeoPoint objects. Instances of the latter may be returned when a geo point does not have any neighboring points within the grid's square it belongs to. The GeoCluster class extends from GeoPoint and supports all the inherited properties: latitude, longitude, categories and metadata. Additionally, geo cluster has its own property representing the number of points the cluster consists of. Since a GeoCluster object an instance of the GeoPoint class, the processing of search responses may look like this:

int mapWidth = 500;
double westLongitude = 23.123;
double eastLongitude = -80.238;

BackendlessGeoQuery geoQuery = new BackendlessGeoQuery();
geoQuery.addCategory( "geoservice_sample" );
geoQuery.initClustering( westLongitude, eastLongitude, mapWidth );
List<GeoPoint> points = Backendless.Geo.getPoints( geoQuery );

Iterator<GeoPoint> iterator=points.iterator();

while( iterator.hasNext() )
{
    GeoPoint geoPointOrCluster =iterator.next();

    if( geoPointOrCluster instanceof GeoCluster )
    {
        GeoCluster geoCluster = (GeoCluster) geoPointOrCluster;
        Log.i( "MYAPP", "Number of points in the cluster: " + geoCluster.getTotalPoints() );
    }
    else
    {
        Log.i( "MYAPP", "Loaded geo point" );
    }

    Log.i( "MYAPP", "latitude - " + geoPointOrCluster.getLatitude() + 
                    ", longitude - " + geoPointOrCluster.getLongitude() );
}

Loading Geo Points from a Cluster

With the geo clustering feature enabled, you may need to reveal the geo points gathered in a cluster. In the Backendless Console, you can click the cluster to reveal its details as described above. By using the calls described below, you will be able to reveal the geo points in a cluster via API.

Synchronous method

List<GeoPoint> Backendless.Geo.loadGeoPoints( GeoCluster geoCluster )

Asynchronous method

Backendless.Geo.loadGeoPoints( final GeoCluster geoCluster, 
                               final AsyncCallback<List<GeoPoint>> responder )

Return Value:

A collection of GeoPoint objects that are included into a cluster.

 

Relations with Data Objects

Backendless Data Service manages application's data objects and provides APIs to work with data objects. Backendless provides integration between data objects managed by Data Service and geo points managed by Geo Service for the scenarios when a logical connection between the two types must exist in an application. For instance, in a taxi ordering app a data object may represent a taxi car, while a geo point represents its location on the map. It is logical to link the two together so they can be retrieved and managed at once.

The Geo-to-Data integration is implemented through geo point metadata. A me