/*
* Copyright 2010 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.samsung.appengine.web.server;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import javax.servlet.http.HttpServletRequest;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.samsung.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.users.User;
import com.samsung.appengine.allshared.AllConfig;
import com.samsung.appengine.allshared.JsonRpcException;
import com.samsung.appengine.allshared.JsonRpcMethod;
import com.samsung.appengine.allshared.RemindMeProtocol;
import com.samsung.appengine.javashared.Util;
import com.samsung.appengine.jsonrpc.server.JsonRpcServlet;
import com.samsung.appengine.web.server.ModelImpl.Alert;
import com.samsung.appengine.web.server.ModelImpl.DeviceRegistration;
import com.samsung.appengine.web.server.ModelImpl.UserInfo;
/**
* The server side implementation of the JumpNote JSON-RPC service.
*/
@SuppressWarnings("serial")
public class RemindMeServlet extends JsonRpcServlet {
public static final int DEBUG = -1; // 1 == yes, 0 == no, -1 == decide based on server info
private static final Logger log = Logger.getLogger(RemindMeServlet.class.getName());
private static final String PROTOCOL_VERSION = "1";
public static final String DEVICE_TYPE_ANDROID = "android";
@Override
@SuppressWarnings("all")
protected boolean isDebug(HttpServletRequest req) {
if (DEBUG == 1)
return true;
else if (DEBUG == 0)
return false;
else
return this.getServletContext().getServerInfo().contains("Development");
}
@JsonRpcMethod(method = RemindMeProtocol.UserInfo.METHOD)
public JSONObject userInfo(final CallContext context) throws JSONException, JsonRpcException {
String continueUrl = context.getParams().optString(
RemindMeProtocol.UserInfo.ARG_LOGIN_CONTINUE, "/");
JSONObject data = new JSONObject();
if (context.getUserService().isUserLoggedIn()) {
UserInfo userInfo = new UserInfo(context.getUserService().getCurrentUser());
data.put(RemindMeProtocol.UserInfo.RET_USER, userInfo.toJSON());
data.put(RemindMeProtocol.UserInfo.RET_LOGOUT_URL, context.getUserService().createLogoutURL(continueUrl));
} else {
data.put(RemindMeProtocol.UserInfo.RET_LOGIN_URL, context.getUserService().createLoginURL(continueUrl));
}
return data;
}
@JsonRpcMethod(method = RemindMeProtocol.ServerInfo.METHOD)
public JSONObject serverInfo(final CallContext context) throws JSONException, JsonRpcException {
JSONObject responseJson = new JSONObject();
responseJson.put(RemindMeProtocol.ServerInfo.RET_PROTOCOL_VERSION, PROTOCOL_VERSION);
return responseJson;
}
@JsonRpcMethod(method = RemindMeProtocol.AlertsList.METHOD, requires_login = true)
public JSONObject notesList(final CallContext context) throws JSONException, JsonRpcException {
UserInfo userInfo = getCurrentUserInfo(context);
// Note: this would be inefficient for large note collections
Query query = context.getPersistenceManager().newQuery(Alert.class);
query.setFilter("ownerKey == ownerKeyParam && pendingDelete == false");
query.declareParameters(Key.class.getName() + " ownerKeyParam");
@SuppressWarnings("unchecked")
List<Alert> alerts = (List<Alert>) query.execute(userInfo.getKey());
JSONObject responseJson = new JSONObject();
try {
JSONArray notesJson = new JSONArray();
for (Alert note : alerts) {
notesJson.put(note.toJSON());
}
responseJson.put(RemindMeProtocol.AlertsList.RET_NOTES, notesJson);
} catch (JSONException e) {
throw new JsonRpcException(500, "Error serializing response.", e);
}
return responseJson;
}
@JsonRpcMethod(method = RemindMeProtocol.AlertsGet.METHOD, requires_login = true)
public JSONObject notesGet(final CallContext context) throws JSONException, JsonRpcException {
UserInfo userInfo = getCurrentUserInfo(context);
String noteId = context.getParams().getString(RemindMeProtocol.AlertsGet.ARG_ID);
Key alertKey = Alert.makeKey(userInfo.getId(), noteId);
try {
Alert note = context.getPersistenceManager().getObjectById(Alert.class, alertKey);
if (note.isPendingDelete()) {
throw new JDOObjectNotFoundException();
}
if (!note.getOwnerId().equals(userInfo.getId())) {
throw new JsonRpcException(403, "You do not have permission to access this note.");
}
return (JSONObject) note.toJSON();
} catch (JDOObjectNotFoundException e) {
throw new JsonRpcException(404, "Alert with ID " + noteId + " does not exist.");
}
}
@JsonRpcMethod(method = RemindMeProtocol.AlertsCreate.METHOD, requires_login = true)
public JSONObject notesCreate(final CallContext context) throws JSONException, JsonRpcException {
UserInfo userInfo = getCurrentUserInfo(context);
String clientDeviceId = null;
JSONObject noteJson;
Alert note;
try {
clientDeviceId = context.getParams().optString(RemindMeProtocol.ARG_CLIENT_DEVICE_ID);
noteJson = context.getParams().getJSONObject(RemindMeProtocol.AlertsCreate.ARG_ALERT);
noteJson.put("owner_id", userInfo.getId());
note = new Alert(noteJson);
} catch (JSONException e) {
throw new JsonRpcException(400, "Invalid note parameter.", e);
}
context.getPersistenceManager().makePersistent(note);
noteJson = (JSONObject) note.toJSON(); // get new parameters like ID, creation date, etc.
enqueueDeviceMessage(context.getPersistenceManager(), userInfo, clientDeviceId);
JSONObject responseJson = new JSONObject();
responseJson.put(RemindMeProtocol.AlertsCreate.RET_ALERT, noteJson);
return responseJson;
}
public void enqueueDeviceMessage(PersistenceManager pm,
UserInfo userInfo, String clientDeviceId) {
Query query = pm.newQuery(DeviceRegistration.class);
query.setFilter("ownerKey == ownerKeyParam");
query.declareParameters(Key.class.getName() + " ownerKeyParam");
@SuppressWarnings("unchecked")
List<DeviceRegistration> registrations = (List<DeviceRegistration>)
query.execute(userInfo.getKey());
int numDeviceMessages = 0;
for (DeviceRegistration registration : registrations) {
if (registration.getDeviceId().equals(clientDeviceId) ||
registration.getRegistrationToken() == null)
continue;
if (DEVICE_TYPE_ANDROID.equals(registration.getDeviceType())) {
++numDeviceMessages;
String email = userInfo.getEmail();
String collapseKey = Long.toHexString(email.hashCode());
try {
C2DMessaging.get(getServletContext()).sendWithRetry(
registration.getRegistrationToken(),
collapseKey,
AllConfig.C2DM_MESSAGE_EXTRA,
AllConfig.C2DM_MESSAGE_SYNC,
AllConfig.C2DM_ACCOUNT_EXTRA,
email);
} catch (IOException ex) {
log.severe("Can't send C2DM message, next manual sync " +
"will get the changes.");
}
}
}
log.info("Scheduled " + numDeviceMessages + " C2DM device messages for user " +
userInfo.getEmail() + ".");
}
//
// @JsonRpcMethod(method = RemindMeProtocol.AlertsEdit.METHOD, requires_login = true)
// public JSONObject notesEdit(final CallContext context) throws JSONException, JsonRpcException {
// UserInfo userInfo = getCurrentUserInfo(context);
//
// String clientDeviceId;
// JSONObject noteJson;
// Alert note;
// String noteId = "n/a";
// Transaction tx = context.getPersistenceManager().currentTransaction();
// try {
// clientDeviceId = context.getParams().optString(RemindMeProtocol.ARG_CLIENT_DEVICE_ID);
// noteJson = context.getParams().getJSONObject(RemindMeProtocol.AlertsEdit.ARG_NOTE);
// noteId = noteJson.getString("id");
// Key noteKey = Alert.makeKey(userInfo.getId(), noteId);
//
// tx.begin();
// note = context.getPersistenceManager().getObjectById(Alert.class, noteKey);
// if (!note.getOwnerId().equals(userInfo.getId())) {
// throw new JsonRpcException(403, "You do not have permission to modify this note.");
// }
// noteJson.put("owner_id", userInfo.getId());
// note.fromJSON(noteJson);
// tx.commit();
// } catch (JSONException e) {
// throw new JsonRpcException(400, "Invalid note parameter.", e);
// } catch (JDOObjectNotFoundException e) {
// throw new JsonRpcException(404, "Alert with ID " + noteId + " does not exist.");
// } finally {
// if (tx.isActive()) {
// tx.rollback();
// }
// }
//
// enqueueDeviceMessage(context.getPersistenceManager(), userInfo, clientDeviceId);
//
// noteJson = (JSONObject) note.toJSON(); // get more parameters like ID, creation date, etc.
// JSONObject responseJson = new JSONObject();
// responseJson.put(RemindMeProtocol.AlertsEdit.RET_NOTE, noteJson);
// return responseJson;
// }
@JsonRpcMethod(method = RemindMeProtocol.AlertsDelete.METHOD, requires_login = true)
public JSONObject notesDelete(final CallContext context) throws JSONException, JsonRpcException {
UserInfo userInfo = getCurrentUserInfo(context);
String clientDeviceId = null;
Alert alert;
String noteId;
try {
clientDeviceId = context.getParams().optString(RemindMeProtocol.ARG_CLIENT_DEVICE_ID);
noteId = context.getParams().getString(RemindMeProtocol.AlertsDelete.ARG_ID);
} catch (JSONException e) {
throw new JsonRpcException(400, "Invalid note ID.", e);
}
Transaction tx = context.getPersistenceManager().currentTransaction();
try {
tx.begin();
alert = context.getPersistenceManager().getObjectById(Alert.class,
Alert.makeKey(userInfo.getId(), noteId));
if (alert.isPendingDelete()) {
throw new JDOObjectNotFoundException();
}
if (!alert.getOwnerId().equals(userInfo.getId())) {
throw new JsonRpcException(403, "You do not have permission to modify this note.");
}
alert.markForDeletion();
tx.commit();
} catch (JDOObjectNotFoundException e) {
throw new JsonRpcException(404, "Alert with ID " + noteId + " does not exist.");
} finally {
if (tx.isActive()) {
tx.rollback();
}
}
enqueueDeviceMessage(context.getPersistenceManager(), userInfo, clientDeviceId);
return null;
}
@JsonRpcMethod(method = RemindMeProtocol.AlertsSync.METHOD, requires_login = true)
public JSONObject notesSync(final CallContext context) throws JSONException, JsonRpcException {
// This method should return a list of updated notes since a current
// date, optionally reconciling/merging a set of a local notes.
String clientDeviceId = null;
UserInfo userInfo = getCurrentUserInfo(context);
Date sinceDate;
try {
clientDeviceId = context.getParams().optString(RemindMeProtocol.ARG_CLIENT_DEVICE_ID);
sinceDate = Util.parseDateISO8601(context.getParams().getString(RemindMeProtocol.AlertsSync.ARG_SINCE_DATE));
} catch (ParseException e) {
throw new JsonRpcException(400, "Invalid since_date.", e);
} catch (JSONException e) {
throw new JsonRpcException(400, "Invalid since_date.", e);
}
JSONObject responseJson = new JSONObject();
JSONArray notesJson = new JSONArray();
Transaction tx = context.getPersistenceManager().currentTransaction();
Date newSinceDate = new Date();
try {
tx.begin();
List<Alert> localAlerts = new ArrayList<Alert>();
if (context.getParams().has(RemindMeProtocol.AlertsSync.ARG_LOCAL_ALERTS)) {
JSONArray localChangesJson = context.getParams().getJSONArray(RemindMeProtocol.AlertsSync.ARG_LOCAL_ALERTS);
for (int i = 0; i < localChangesJson.length(); i++) {
try {
JSONObject noteJson = localChangesJson.getJSONObject(i);
if (noteJson.has("id")) {
Key existingAlertKey = Alert.makeKey(userInfo.getId(),
noteJson.get("id").toString());
try {
Alert existingAlert = (Alert) context.getPersistenceManager().getObjectById(
Alert.class, existingAlertKey);
if (!existingAlert.getOwnerId().equals(userInfo.getId())) {
// User doesn't have permission to edit this note. Instead of
// throwing an error, just re-create it on the server side.
//throw new JsonRpcException(403,
// "You do not have permission to modify this note.");
noteJson.remove("id");
}
} catch (JDOObjectNotFoundException e) {
// Alert doesn't exist, instead of throwing an error,
// just re-create the note on the server side (unassign its ID).
//throw new JsonRpcException(404, "Alert with ID "
// + noteJson.get("id").toString() + " does not exist.");
noteJson.remove("id");
}
}
noteJson.put("owner_id", userInfo.getId());
Alert localAlert = new Alert(noteJson);
localAlerts.add(localAlert);
} catch (JSONException e) {
throw new JsonRpcException(400, "Invalid local note content.", e);
}
}
}
// Query server-side note changes.
Query query = context.getPersistenceManager().newQuery(Alert.class);
query.setFilter("ownerKey == ownerKeyParam && modifiedDate > sinceDate");
query.setOrdering("modifiedDate desc");
query.declareParameters(Key.class.getName() + " ownerKeyParam, java.util.Date sinceDate");
@SuppressWarnings("unchecked")
List<Alert> alerts = (List<Alert>) query.execute(userInfo.getKey(), sinceDate);
// Now merge the lists and conflicting objects.
Reconciler<Alert> reconciler = new Reconciler<Alert>() {
@Override
public Alert reconcile(Alert o1, Alert o2) {
boolean pick1 = o1.getModifiedDate().after(o2.getModifiedDate());
// Make sure only the chosen version of the note is persisted
context.getPersistenceManager().makeTransient(pick1 ? o2 : o1);
return pick1 ? o1 : o2;
}
};
Collection<Alert> reconciledAlerts = reconciler.reconcileLists(alerts, localAlerts);
for (Alert alert : reconciledAlerts) {
// Save the note.
context.getPersistenceManager().makePersistent(alert);
// Put it in the response output.
notesJson.put(alert.toJSON());
}
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
}
enqueueDeviceMessage(context.getPersistenceManager(), userInfo, clientDeviceId);
responseJson.put(RemindMeProtocol.AlertsSync.RET_ALERTS, notesJson);
responseJson.put(RemindMeProtocol.AlertsSync.RET_NEW_SINCE_DATE,
Util.formatDateISO8601(newSinceDate));
return responseJson;
}
@JsonRpcMethod(method = RemindMeProtocol.DevicesRegister.METHOD, requires_login = true)
public JSONObject devicesRegister(final CallContext context) throws JSONException, JsonRpcException {
UserInfo userInfo = getCurrentUserInfo(context);
JSONObject registrationJson;
DeviceRegistration registrationParam;
try {
registrationJson = context.getParams().getJSONObject(RemindMeProtocol.DevicesRegister.ARG_DEVICE);
registrationJson.put("owner_id", userInfo.getId());
registrationParam = new DeviceRegistration(registrationJson);
} catch (JSONException e) {
throw new JsonRpcException(400, "Invalid device parameter.", e);
}
Transaction tx = context.getPersistenceManager().currentTransaction();
try {
tx.begin();
Query query = context.getPersistenceManager().newQuery(DeviceRegistration.class);
query.setFilter("ownerKey == ownerKeyParam && deviceId == deviceIdParam");
query.declareParameters(Key.class.getName() + " ownerKeyParam, String deviceIdParam");
@SuppressWarnings("unchecked")
List<DeviceRegistration> registrations = (List<DeviceRegistration>)
query.execute(userInfo.getKey(), registrationParam.getDeviceId());
// Update all existing registration tokens.
boolean registeredForUser = false;
for (DeviceRegistration registration : registrations) {
if (registration.getOwnerId().equals(userInfo.getId()))
registeredForUser = true;
registration.setRegistrationToken(registrationParam.getRegistrationToken());
}
// Register the device for the logged in user if not already registered.
if (!registeredForUser) {
context.getPersistenceManager().makePersistent(registrationParam);
}
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
}
registrationJson = (JSONObject) registrationParam.toJSON();
JSONObject responseJson = new JSONObject();
responseJson.put(RemindMeProtocol.DevicesRegister.RET_DEVICE, registrationJson);
return responseJson;
}
@JsonRpcMethod(method = RemindMeProtocol.DevicesUnregister.METHOD, requires_login = true)
public JSONObject devicesUnregister(final CallContext context) throws JSONException, JsonRpcException {
UserInfo userInfo = getCurrentUserInfo(context);
String deviceId;
try {
deviceId = context.getParams().getString(RemindMeProtocol.DevicesUnregister.ARG_DEVICE_ID);
} catch (JSONException e) {
throw new JsonRpcException(400, "Invalid device ID parameter.", e);
}
Transaction tx = context.getPersistenceManager().currentTransaction();
try {
tx.begin();
Query query = context.getPersistenceManager().newQuery(DeviceRegistration.class);
query.setFilter("ownerKey == ownerKeyParam && deviceId == deviceIdParam");
query.declareParameters(Key.class.getName() + " ownerKeyParam, String deviceIdParam");
@SuppressWarnings("unchecked")
List<DeviceRegistration> registrations = (List<DeviceRegistration>)
query.execute(userInfo.getKey(), deviceId);
if (registrations.size() == 0) {
throw new JsonRpcException(404, "Device with provided ID is not registered.");
}
for (DeviceRegistration registration : registrations) {
context.getPersistenceManager().deletePersistent(registration);
}
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
}
return null;
}
@JsonRpcMethod(method = RemindMeProtocol.DevicesClear.METHOD, requires_login = true)
public JSONObject devicesClear(final CallContext context) throws JSONException, JsonRpcException {
UserInfo userInfo = getCurrentUserInfo(context);
Transaction tx = context.getPersistenceManager().currentTransaction();
try {
tx.begin();
Query query = context.getPersistenceManager().newQuery(DeviceRegistration.class);
query.setFilter("ownerKey == ownerKeyParam");
query.declareParameters(Key.class.getName() + " ownerKeyParam");
@SuppressWarnings("unchecked")
List<DeviceRegistration> registrations = (List<DeviceRegistration>)
query.execute(userInfo.getKey());
for (DeviceRegistration registration : registrations) {
context.getPersistenceManager().deletePersistent(registration);
}
tx.commit();
} finally {
if (tx.isActive()) {
tx.rollback();
}
}
return null;
}
public UserInfo getCurrentUserInfo(final CallContext context) {
if (!context.getUserService().isUserLoggedIn())
return null;
User user = context.getUserService().getCurrentUser();
try {
UserInfo userInfo = context.getPersistenceManager().getObjectById(UserInfo.class,
user.getUserId());
return userInfo;
} catch (JDOObjectNotFoundException e) {
UserInfo userInfo = new UserInfo(user);
context.getPersistenceManager().makePersistent(userInfo);
return userInfo;
}
}
}