/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.apps.iosched.gcm.server;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Transaction;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* Simple implementation of a data store using standard Java collections. This class is neither
* persistent (it will lost the data when the app is restarted) nor thread safe.
*/
public final class Datastore {
private static final Logger logger = Logger.getLogger(Datastore.class.getSimpleName());
static final String MULTICAST_TYPE = "Multicast";
static final String MULTICAST_REG_IDS_PROPERTY = "regIds";
static final String MULTICAST_ANNOUNCEMENT_PROPERTY = "announcement";
static final int MULTICAST_SIZE = 1000;
static final String DEVICE_TYPE = "Device";
static final String DEVICE_REG_ID_PROPERTY = "regid";
private static final FetchOptions DEFAULT_FETCH_OPTIONS = FetchOptions.Builder
.withPrefetchSize(MULTICAST_SIZE)
.chunkSize(MULTICAST_SIZE);
private static final DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
private Datastore() {
throw new UnsupportedOperationException();
}
/**
* Registers a device.
*/
public static void register(String regId) {
logger.info("Registering " + regId);
Transaction txn = ds.beginTransaction();
try {
Entity entity = findDeviceByRegId(regId);
if (entity != null) {
logger.fine(regId + " is already registered; ignoring.");
return;
}
entity = new Entity(DEVICE_TYPE);
entity.setProperty(DEVICE_REG_ID_PROPERTY, regId);
ds.put(entity);
txn.commit();
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
}
/**
* Unregisters a device.
*/
public static void unregister(String regId) {
logger.info("Unregistering " + regId);
Transaction txn = ds.beginTransaction();
try {
Entity entity = findDeviceByRegId(regId);
if (entity == null) {
logger.warning("Device " + regId + " already unregistered");
} else {
Key key = entity.getKey();
ds.delete(key);
}
txn.commit();
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
}
/**
* Updates the registration id of a device.
*/
public static void updateRegistration(String oldId, String newId) {
logger.info("Updating " + oldId + " to " + newId);
Transaction txn = ds.beginTransaction();
try {
Entity entity = findDeviceByRegId(oldId);
if (entity == null) {
logger.warning("No device for registration id " + oldId);
return;
}
entity.setProperty(DEVICE_REG_ID_PROPERTY, newId);
ds.put(entity);
txn.commit();
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
}
/**
* Gets the number of total devices.
*/
public static int getTotalDevices() {
Transaction txn = ds.beginTransaction();
try {
Query query = new Query(DEVICE_TYPE).setKeysOnly();
List<Entity> allKeys =
ds.prepare(query).asList(DEFAULT_FETCH_OPTIONS);
int total = allKeys.size();
logger.fine("Total number of devices: " + total);
txn.commit();
return total;
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
}
/**
* Gets all registered devices.
*/
public static List<String> getDevices() {
List<String> devices;
Transaction txn = ds.beginTransaction();
try {
Query query = new Query(DEVICE_TYPE);
Iterable<Entity> entities =
ds.prepare(query).asIterable(DEFAULT_FETCH_OPTIONS);
devices = new ArrayList<String>();
for (Entity entity : entities) {
String device = (String) entity.getProperty(DEVICE_REG_ID_PROPERTY);
devices.add(device);
}
txn.commit();
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
return devices;
}
/**
* Returns the device object with the given registration ID.
*/
private static Entity findDeviceByRegId(String regId) {
Query query = new Query(DEVICE_TYPE)
.addFilter(DEVICE_REG_ID_PROPERTY, FilterOperator.EQUAL, regId);
PreparedQuery preparedQuery = ds.prepare(query);
List<Entity> entities = preparedQuery.asList(DEFAULT_FETCH_OPTIONS);
Entity entity = null;
if (!entities.isEmpty()) {
entity = entities.get(0);
}
int size = entities.size();
if (size > 0) {
logger.severe(
"Found " + size + " entities for regId " + regId + ": " + entities);
}
return entity;
}
/**
* Creates a persistent record with the devices to be notified using a multicast message.
*
* @param devices registration ids of the devices.
* @param announcement announcement messageage
* @return encoded key for the persistent record.
*/
public static String createMulticast(List<String> devices, String announcement) {
String type = (announcement == null || announcement.trim().length() == 0)
? "sync"
: ("announcement: " + announcement);
logger.info("Storing multicast (" + type + ") for " + devices.size() + " devices");
String encodedKey;
Transaction txn = ds.beginTransaction();
try {
Entity entity = new Entity(MULTICAST_TYPE);
entity.setProperty(MULTICAST_REG_IDS_PROPERTY, devices);
entity.setProperty(MULTICAST_ANNOUNCEMENT_PROPERTY, announcement);
ds.put(entity);
Key key = entity.getKey();
encodedKey = KeyFactory.keyToString(key);
logger.fine("multicast key: " + encodedKey);
txn.commit();
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
return encodedKey;
}
/**
* Gets a persistent record with the devices to be notified using a multicast message.
*
* @param encodedKey encoded key for the persistent record.
*/
public static Entity getMulticast(String encodedKey) {
Key key = KeyFactory.stringToKey(encodedKey);
Entity entity;
Transaction txn = ds.beginTransaction();
try {
entity = ds.get(key);
txn.commit();
return entity;
} catch (EntityNotFoundException e) {
logger.severe("No entity for key " + key);
return null;
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
}
/**
* Updates a persistent record with the devices to be notified using a multicast message.
*
* @param encodedKey encoded key for the persistent record.
* @param devices new list of registration ids of the devices.
*/
public static void updateMulticast(String encodedKey, List<String> devices) {
Key key = KeyFactory.stringToKey(encodedKey);
Entity entity;
Transaction txn = ds.beginTransaction();
try {
try {
entity = ds.get(key);
} catch (EntityNotFoundException e) {
logger.severe("No entity for key " + key);
return;
}
entity.setProperty(MULTICAST_REG_IDS_PROPERTY, devices);
ds.put(entity);
txn.commit();
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
}
/**
* Deletes a persistent record with the devices to be notified using a multicast message.
*
* @param encodedKey encoded key for the persistent record.
*/
public static void deleteMulticast(String encodedKey) {
Transaction txn = ds.beginTransaction();
try {
Key key = KeyFactory.stringToKey(encodedKey);
ds.delete(key);
txn.commit();
} finally {
if (txn.isActive()) {
txn.rollback();
}
}
}
}