Java tutorial
/* * 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 de.elepferd.web.pushnotifier.server; import java.io.IOException; import java.text.ParseException; import java.util.ArrayList; 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.google.android.c2dm.server.C2DMessaging; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.users.User; import de.elepferd.web.pushnotifier.allshared.AllConfig; import de.elepferd.web.pushnotifier.allshared.JsonRpcException; import de.elepferd.web.pushnotifier.allshared.JsonRpcMethod; import de.elepferd.web.pushnotifier.allshared.PushNotifierProtocol; import de.elepferd.web.pushnotifier.javashared.Util; import de.elepferd.web.pushnotifier.jsonrpc.server.JsonRpcServlet; import de.elepferd.web.pushnotifier.server.ModelImpl.DeviceRegistration; import de.elepferd.web.pushnotifier.server.ModelImpl.Note; import de.elepferd.web.pushnotifier.server.ModelImpl.UserInfo; /** * The server side implementation of the PushNotifier JSON-RPC service. */ @SuppressWarnings("serial") public class PushNotifierServlet 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(PushNotifierServlet.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 = PushNotifierProtocol.UserInfo.METHOD) public JSONObject userInfo(final CallContext context) throws JSONException, JsonRpcException { String continueUrl = context.getParams().optString(PushNotifierProtocol.UserInfo.ARG_LOGIN_CONTINUE, "/"); JSONObject data = new JSONObject(); if (context.getUserService().isUserLoggedIn()) { UserInfo userInfo = new UserInfo(context.getUserService().getCurrentUser()); data.put(PushNotifierProtocol.UserInfo.RET_USER, userInfo.toJSON()); data.put(PushNotifierProtocol.UserInfo.RET_LOGOUT_URL, context.getUserService().createLogoutURL(continueUrl)); } else { data.put(PushNotifierProtocol.UserInfo.RET_LOGIN_URL, context.getUserService().createLoginURL(continueUrl)); } return data; } @JsonRpcMethod(method = PushNotifierProtocol.ServerInfo.METHOD) public JSONObject serverInfo(final CallContext context) throws JSONException, JsonRpcException { JSONObject responseJson = new JSONObject(); responseJson.put(PushNotifierProtocol.ServerInfo.RET_PROTOCOL_VERSION, PROTOCOL_VERSION); return responseJson; } @JsonRpcMethod(method = PushNotifierProtocol.NotesList.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(Note.class); query.setFilter("ownerKey == ownerKeyParam && pendingDelete == false"); query.declareParameters(Key.class.getName() + " ownerKeyParam"); @SuppressWarnings("unchecked") List<Note> notes = (List<Note>) query.execute(userInfo.getKey()); JSONObject responseJson = new JSONObject(); try { JSONArray notesJson = new JSONArray(); for (Note note : notes) { notesJson.put(note.toJSON()); } responseJson.put(PushNotifierProtocol.NotesList.RET_NOTES, notesJson); } catch (JSONException e) { throw new JsonRpcException(500, "Error serializing response.", e); } return responseJson; } @JsonRpcMethod(method = PushNotifierProtocol.NotesGet.METHOD, requires_login = true) public JSONObject notesGet(final CallContext context) throws JSONException, JsonRpcException { UserInfo userInfo = getCurrentUserInfo(context); String noteId = context.getParams().getString(PushNotifierProtocol.NotesGet.ARG_ID); Key noteKey = Note.makeKey(userInfo.getId(), noteId); try { Note note = context.getPersistenceManager().getObjectById(Note.class, noteKey); 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, "Note with ID " + noteId + " does not exist."); } } @JsonRpcMethod(method = PushNotifierProtocol.NotesCreate.METHOD, requires_login = true) public JSONObject notesCreate(final CallContext context) throws JSONException, JsonRpcException { UserInfo userInfo = getCurrentUserInfo(context); String clientDeviceId = null; JSONObject noteJson; Note note; try { clientDeviceId = context.getParams().optString(PushNotifierProtocol.ARG_CLIENT_DEVICE_ID); noteJson = context.getParams().getJSONObject(PushNotifierProtocol.NotesCreate.ARG_NOTE); noteJson.put("owner_id", userInfo.getId()); note = new Note(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(PushNotifierProtocol.NotesCreate.RET_NOTE, 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 = PushNotifierProtocol.NotesEdit.METHOD, requires_login = true) public JSONObject notesEdit(final CallContext context) throws JSONException, JsonRpcException { UserInfo userInfo = getCurrentUserInfo(context); String clientDeviceId; JSONObject noteJson; Note note; String noteId = "n/a"; Transaction tx = context.getPersistenceManager().currentTransaction(); try { clientDeviceId = context.getParams().optString(PushNotifierProtocol.ARG_CLIENT_DEVICE_ID); noteJson = context.getParams().getJSONObject(PushNotifierProtocol.NotesEdit.ARG_NOTE); noteId = noteJson.getString("id"); Key noteKey = Note.makeKey(userInfo.getId(), noteId); tx.begin(); note = context.getPersistenceManager().getObjectById(Note.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, "Note 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(PushNotifierProtocol.NotesEdit.RET_NOTE, noteJson); return responseJson; } @JsonRpcMethod(method = PushNotifierProtocol.NotesDelete.METHOD, requires_login = true) public JSONObject notesDelete(final CallContext context) throws JSONException, JsonRpcException { UserInfo userInfo = getCurrentUserInfo(context); String clientDeviceId = null; Note note; String noteId; try { clientDeviceId = context.getParams().optString(PushNotifierProtocol.ARG_CLIENT_DEVICE_ID); noteId = context.getParams().getString(PushNotifierProtocol.NotesDelete.ARG_ID); } catch (JSONException e) { throw new JsonRpcException(400, "Invalid note ID.", e); } Transaction tx = context.getPersistenceManager().currentTransaction(); try { tx.begin(); note = context.getPersistenceManager().getObjectById(Note.class, Note.makeKey(userInfo.getId(), noteId)); if (note.isPendingDelete()) { throw new JDOObjectNotFoundException(); } if (!note.getOwnerId().equals(userInfo.getId())) { throw new JsonRpcException(403, "You do not have permission to modify this note."); } note.markForDeletion(); tx.commit(); } catch (JDOObjectNotFoundException e) { throw new JsonRpcException(404, "Note with ID " + noteId + " does not exist."); } finally { if (tx.isActive()) { tx.rollback(); } } enqueueDeviceMessage(context.getPersistenceManager(), userInfo, clientDeviceId); return null; } @JsonRpcMethod(method = PushNotifierProtocol.NotesSync.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(PushNotifierProtocol.ARG_CLIENT_DEVICE_ID); sinceDate = Util .parseDateISO8601(context.getParams().getString(PushNotifierProtocol.NotesSync.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<Note> localNotes = new ArrayList<Note>(); if (context.getParams().has(PushNotifierProtocol.NotesSync.ARG_LOCAL_NOTES)) { JSONArray localChangesJson = context.getParams() .getJSONArray(PushNotifierProtocol.NotesSync.ARG_LOCAL_NOTES); for (int i = 0; i < localChangesJson.length(); i++) { try { JSONObject noteJson = localChangesJson.getJSONObject(i); if (noteJson.has("id")) { Key existingNoteKey = Note.makeKey(userInfo.getId(), noteJson.get("id").toString()); try { Note existingNote = (Note) context.getPersistenceManager().getObjectById(Note.class, existingNoteKey); if (!existingNote.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) { // Note doesn't exist, instead of throwing an error, // just re-create the note on the server side (unassign its ID). //throw new JsonRpcException(404, "Note with ID " // + noteJson.get("id").toString() + " does not exist."); noteJson.remove("id"); } } noteJson.put("owner_id", userInfo.getId()); Note localNote = new Note(noteJson); localNotes.add(localNote); } catch (JSONException e) { throw new JsonRpcException(400, "Invalid local note content.", e); } } } // Query server-side note changes. Query query = context.getPersistenceManager().newQuery(Note.class); query.setFilter("ownerKey == ownerKeyParam && modifiedDate > sinceDate"); query.setOrdering("modifiedDate desc"); query.declareParameters(Key.class.getName() + " ownerKeyParam, java.util.Date sinceDate"); @SuppressWarnings("unchecked") List<Note> notes = (List<Note>) query.execute(userInfo.getKey(), sinceDate); // Now merge the lists and conflicting objects. /*Reconciler<Note> reconciler = new Reconciler<Note>() { @Override public Note reconcile(Note o1, Note 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<Note> reconciledNotes = reconciler.reconcileLists(notes, localNotes); for (Note note : reconciledNotes) { // Save the note. context.getPersistenceManager().makePersistent(note); // Put it in the response output. notesJson.put(note.toJSON()); } tx.commit();*/ } finally { if (tx.isActive()) { tx.rollback(); } } enqueueDeviceMessage(context.getPersistenceManager(), userInfo, clientDeviceId); responseJson.put(PushNotifierProtocol.NotesSync.RET_NOTES, notesJson); responseJson.put(PushNotifierProtocol.NotesSync.RET_NEW_SINCE_DATE, Util.formatDateISO8601(newSinceDate)); return responseJson; } @JsonRpcMethod(method = PushNotifierProtocol.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(PushNotifierProtocol.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(PushNotifierProtocol.DevicesRegister.RET_DEVICE, registrationJson); return responseJson; } @JsonRpcMethod(method = PushNotifierProtocol.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(PushNotifierProtocol.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 = PushNotifierProtocol.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; } } }