| #!/usr/bin/python2.5 |
| |
| # Copyright (C) 2010 The Android Open Source Project |
| # |
| # 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. |
| |
| """ |
| Handlers for Sample SyncAdapter services. |
| |
| Contains several RequestHandler subclasses used to handle post operations. |
| This script is designed to be run directly as a WSGI application. |
| |
| """ |
| |
| import cgi |
| import logging |
| import time as _time |
| from datetime import datetime |
| from django.utils import simplejson |
| from google.appengine.api import users |
| from google.appengine.ext import db |
| from google.appengine.ext import webapp |
| from model import datastore |
| import wsgiref.handlers |
| |
| |
| class BaseWebServiceHandler(webapp.RequestHandler): |
| """ |
| Base class for our web services. We put some common helper |
| functions here. |
| """ |
| |
| """ |
| Since we're only simulating a single user account, declare our |
| hard-coded credentials here, so that they're easy to see/find. |
| We actually accept any and all usernames that start with this |
| hard-coded values. So if ACCT_USER_NAME is 'user', then we'll |
| accept 'user', 'user75', 'userbuddy', etc, all as legal account |
| usernames. |
| """ |
| ACCT_USER_NAME = 'user' |
| ACCT_PASSWORD = 'test' |
| ACCT_AUTH_TOKEN = 'xyzzy' |
| |
| DATE_TIME_FORMAT = '%Y/%m/%d %H:%M' |
| |
| """ |
| Process a request to authenticate a client. We assume that the username |
| and password will be included in the request. If successful, we'll return |
| an authtoken as the only content. If auth fails, we'll send an "invalid |
| credentials" error. |
| We return a boolean indicating whether we were successful (true) or not (false). |
| In the event that this call fails, we will setup the response, so callers just |
| need to RETURN in the error case. |
| """ |
| def authenticate(self): |
| self.username = self.request.get('username') |
| self.password = self.request.get('password') |
| |
| logging.info('Authenticatng username: ' + self.username) |
| |
| if ((self.username != None) and |
| (self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and |
| (self.password == BaseWebServiceHandler.ACCT_PASSWORD)): |
| # Authentication was successful - return our hard-coded |
| # auth-token as the only response. |
| self.response.set_status(200, 'OK') |
| self.response.out.write(BaseWebServiceHandler.ACCT_AUTH_TOKEN) |
| return True |
| else: |
| # Authentication failed. Return the standard HTTP auth failure |
| # response to let the client know. |
| self.response.set_status(401, 'Invalid Credentials') |
| return False |
| |
| """ |
| Validate the credentials of the client for a web service request. |
| The request should include username/password parameters that correspond |
| to our hard-coded single account values. |
| We return a boolean indicating whether we were successful (true) or not (false). |
| In the event that this call fails, we will setup the response, so callers just |
| need to RETURN in the error case. |
| """ |
| def validate(self): |
| self.username = self.request.get('username') |
| self.authtoken = self.request.get('authtoken') |
| |
| logging.info('Validating username: ' + self.username) |
| |
| if ((self.username != None) and |
| (self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and |
| (self.authtoken == BaseWebServiceHandler.ACCT_AUTH_TOKEN)): |
| return True |
| else: |
| self.response.set_status(401, 'Invalid Credentials') |
| return False |
| |
| |
| class Authenticate(BaseWebServiceHandler): |
| """ |
| Handles requests for login and authentication. |
| |
| UpdateHandler only accepts post events. It expects each |
| request to include username and password fields. It returns authtoken |
| after successful authentication and "invalid credentials" error otherwise. |
| """ |
| |
| def post(self): |
| self.authenticate() |
| |
| def get(self): |
| """Used for debugging in a browser...""" |
| self.post() |
| |
| |
| class SyncContacts(BaseWebServiceHandler): |
| """Handles requests for fetching user's contacts. |
| |
| UpdateHandler only accepts post events. It expects each |
| request to include username and authtoken. If the authtoken is valid |
| it returns user's contact info in JSON format. |
| """ |
| |
| def get(self): |
| """Used for debugging in a browser...""" |
| self.post() |
| |
| def post(self): |
| logging.info('*** Starting contact sync ***') |
| if (not self.validate()): |
| return |
| |
| updated_contacts = [] |
| |
| # Process any client-side changes sent up in the request. |
| # Any new contacts that were added are included in the |
| # updated_contacts list, so that we return them to the |
| # client. That way, the client can see the serverId of |
| # the newly added contact. |
| client_buffer = self.request.get('contacts') |
| if ((client_buffer != None) and (client_buffer != '')): |
| self.process_client_changes(client_buffer, updated_contacts) |
| |
| # Add any contacts that have been updated on the server-side |
| # since the last sync by this client. |
| client_state = self.request.get('syncstate') |
| self.get_updated_contacts(client_state, updated_contacts) |
| |
| logging.info('Returning ' + str(len(updated_contacts)) + ' contact records') |
| |
| # Return the list of updated contacts to the client |
| self.response.set_status(200) |
| self.response.out.write(toJSON(updated_contacts)) |
| |
| def get_updated_contacts(self, client_state, updated_contacts): |
| logging.info('* Processing server changes') |
| timestamp = None |
| |
| base_url = self.request.host_url |
| |
| # The client sends the last high-water-mark that they successfully |
| # sync'd to in the syncstate parameter. It's opaque to them, but |
| # its actually a seconds-in-unix-epoch timestamp that we use |
| # as a baseline. |
| if client_state: |
| logging.info('Client sync state: ' + client_state) |
| timestamp = datetime.utcfromtimestamp(float(client_state)) |
| |
| # Keep track of the update/delete counts, so we can log it |
| # below. Makes debugging easier... |
| update_count = 0 |
| delete_count = 0 |
| |
| contacts = datastore.Contact.all() |
| if contacts: |
| # Find the high-water mark for the most recently updated friend. |
| # We'll return this as the syncstate (x) value for all the friends |
| # we return from this function. |
| high_water_date = datetime.min |
| for contact in contacts: |
| if (contact.updated > high_water_date): |
| high_water_date = contact.updated |
| high_water_mark = str(long(_time.mktime(high_water_date.utctimetuple())) + 1) |
| logging.info('New sync state: ' + high_water_mark) |
| |
| # Now build the updated_contacts containing all the friends that have been |
| # changed since the last sync |
| for contact in contacts: |
| # If our list of contacts we're returning already contains this |
| # contact (for example, it's a contact just uploaded from the client) |
| # then don't bother processing it any further... |
| if (self.list_contains_contact(updated_contacts, contact)): |
| continue |
| |
| handle = contact.handle |
| |
| if timestamp is None or contact.updated > timestamp: |
| if contact.deleted == True: |
| delete_count = delete_count + 1 |
| DeletedContactData(updated_contacts, handle, high_water_mark) |
| else: |
| update_count = update_count + 1 |
| UpdatedContactData(updated_contacts, handle, None, base_url, high_water_mark) |
| |
| logging.info('Server-side updates: ' + str(update_count)) |
| logging.info('Server-side deletes: ' + str(delete_count)) |
| |
| def process_client_changes(self, contacts_buffer, updated_contacts): |
| logging.info('* Processing client changes: ' + self.username) |
| |
| base_url = self.request.host_url |
| |
| # Build an array of generic objects containing contact data, |
| # using the Django built-in JSON parser |
| logging.info('Uploaded contacts buffer: ' + contacts_buffer) |
| json_list = simplejson.loads(contacts_buffer) |
| logging.info('Client-side updates: ' + str(len(json_list))) |
| |
| # Keep track of the number of new contacts the client sent to us, |
| # so that we can log it below. |
| new_contact_count = 0 |
| |
| for jcontact in json_list: |
| new_contact = False |
| id = self.safe_attr(jcontact, 'i') |
| if (id != None): |
| logging.info('Updating contact: ' + str(id)) |
| contact = datastore.Contact.get(db.Key.from_path('Contact', id)) |
| else: |
| logging.info('Creating new contact record') |
| new_contact = True |
| contact = datastore.Contact(handle='temp') |
| |
| # If the 'change' for this contact is that they were deleted |
| # on the client-side, all we want to do is set the deleted |
| # flag here, and we're done. |
| if (self.safe_attr(jcontact, 'd') == True): |
| contact.deleted = True |
| contact.put() |
| logging.info('Deleted contact: ' + contact.handle) |
| continue |
| |
| contact.firstname = self.safe_attr(jcontact, 'f') |
| contact.lastname = self.safe_attr(jcontact, 'l') |
| contact.phone_home = self.safe_attr(jcontact, 'h') |
| contact.phone_office = self.safe_attr(jcontact, 'o') |
| contact.phone_mobile = self.safe_attr(jcontact, 'm') |
| contact.email = self.safe_attr(jcontact, 'e') |
| contact.deleted = (self.safe_attr(jcontact, 'd') == 'true') |
| if (new_contact): |
| # New record - add them to db... |
| new_contact_count = new_contact_count + 1 |
| contact.handle = contact.firstname + '_' + contact.lastname |
| logging.info('Created new contact handle: ' + contact.handle) |
| contact.put() |
| logging.info('Saved contact: ' + contact.handle) |
| |
| # We don't save off the client_id value (thus we add it after |
| # the "put"), but we want it to be in the JSON object we |
| # serialize out, so that the client can match this contact |
| # up with the client version. |
| client_id = self.safe_attr(jcontact, 'c') |
| |
| # Create a high-water-mark for sync-state from the 'updated' time |
| # for this contact, so we return the correct value to the client. |
| high_water = str(long(_time.mktime(contact.updated.utctimetuple())) + 1) |
| |
| # Add new contacts to our updated_contacts, so that we return them |
| # to the client (so the client gets the serverId for the |
| # added contact) |
| if (new_contact): |
| UpdatedContactData(updated_contacts, contact.handle, client_id, base_url, |
| high_water) |
| |
| logging.info('Client-side adds: ' + str(new_contact_count)) |
| |
| def list_contains_contact(self, contact_list, contact): |
| if (contact is None): |
| return False |
| contact_id = str(contact.key().id()) |
| for next in contact_list: |
| if ((next != None) and (next['i'] == contact_id)): |
| return True |
| return False |
| |
| def safe_attr(self, obj, attr_name): |
| if attr_name in obj: |
| return obj[attr_name] |
| return None |
| |
| class ResetDatabase(BaseWebServiceHandler): |
| """ |
| Handles cron request to reset the contact database. |
| |
| We have a weekly cron task that resets the database back to a |
| few contacts, so that it doesn't grow to an absurd size. |
| """ |
| |
| def get(self): |
| # Delete all the existing contacts from the database |
| contacts = datastore.Contact.all() |
| for contact in contacts: |
| contact.delete() |
| |
| # Now create three sample contacts |
| contact1 = datastore.Contact(handle = 'juliet', |
| firstname = 'Juliet', |
| lastname = 'Capulet', |
| phone_mobile = '(650) 555-1000', |
| phone_home = '(650) 555-1001', |
| status = 'Wherefore art thou Romeo?') |
| contact1.put() |
| |
| contact2 = datastore.Contact(handle = 'romeo', |
| firstname = 'Romeo', |
| lastname = 'Montague', |
| phone_mobile = '(650) 555-2000', |
| phone_home = '(650) 555-2001', |
| status = 'I dream\'d a dream to-night') |
| contact2.put() |
| |
| contact3 = datastore.Contact(handle = 'tybalt', |
| firstname = 'Tybalt', |
| lastname = 'Capulet', |
| phone_mobile = '(650) 555-3000', |
| phone_home = '(650) 555-3001', |
| status = 'Have at thee, coward') |
| contact3.put() |
| |
| |
| |
| |
| def toJSON(object): |
| """Dumps the data represented by the object to JSON for wire transfer.""" |
| return simplejson.dumps(object) |
| |
| class UpdatedContactData(object): |
| """Holds data for user's contacts. |
| |
| This class knows how to serialize itself to JSON. |
| """ |
| __FIELD_MAP = { |
| 'handle': 'u', |
| 'firstname': 'f', |
| 'lastname': 'l', |
| 'status': 's', |
| 'phone_home': 'h', |
| 'phone_office': 'o', |
| 'phone_mobile': 'm', |
| 'email': 'e', |
| 'client_id': 'c' |
| } |
| |
| def __init__(self, contact_list, username, client_id, host_url, high_water_mark): |
| obj = datastore.Contact.get_contact_info(username) |
| contact = {} |
| for obj_name, json_name in self.__FIELD_MAP.items(): |
| if hasattr(obj, obj_name): |
| v = getattr(obj, obj_name) |
| if (v != None): |
| contact[json_name] = str(v) |
| else: |
| contact[json_name] = None |
| contact['i'] = str(obj.key().id()) |
| contact['a'] = host_url + "/avatar?id=" + str(obj.key().id()) |
| contact['x'] = high_water_mark |
| if (client_id != None): |
| contact['c'] = str(client_id) |
| contact_list.append(contact) |
| |
| class DeletedContactData(object): |
| def __init__(self, contact_list, username, high_water_mark): |
| obj = datastore.Contact.get_contact_info(username) |
| contact = {} |
| contact['d'] = 'true' |
| contact['i'] = str(obj.key().id()) |
| contact['x'] = high_water_mark |
| contact_list.append(contact) |
| |
| def main(): |
| application = webapp.WSGIApplication( |
| [('/auth', Authenticate), |
| ('/sync', SyncContacts), |
| ('/reset_database', ResetDatabase), |
| ], |
| debug=True) |
| wsgiref.handlers.CGIHandler().run(application) |
| |
| if __name__ == "__main__": |
| main() |