Home | History | Annotate | Download | only in samplesyncadapter_server
      1 #!/usr/bin/python2.5
      2 
      3 # Copyright (C) 2010 The Android Open Source Project
      4 #
      5 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
      6 # use this file except in compliance with the License. You may obtain a copy of
      7 # the License at
      8 #
      9 # http://www.apache.org/licenses/LICENSE-2.0
     10 #
     11 # Unless required by applicable law or agreed to in writing, software
     12 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     13 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     14 # License for the specific language governing permissions and limitations under
     15 # the License.
     16 
     17 """
     18 Handlers for Sample SyncAdapter services.
     19 
     20 Contains several RequestHandler subclasses used to handle post operations.
     21 This script is designed to be run directly as a WSGI application.
     22 
     23 """
     24 
     25 import cgi
     26 import logging
     27 import time as _time
     28 from datetime import datetime
     29 from django.utils import simplejson
     30 from google.appengine.api import users
     31 from google.appengine.ext import db
     32 from google.appengine.ext import webapp
     33 from model import datastore
     34 import wsgiref.handlers
     35 
     36 
     37 class BaseWebServiceHandler(webapp.RequestHandler):
     38     """
     39     Base class for our web services. We put some common helper
     40     functions here.
     41     """
     42 
     43     """
     44     Since we're only simulating a single user account, declare our
     45     hard-coded credentials here, so that they're easy to see/find.
     46     We actually accept any and all usernames that start with this
     47     hard-coded values. So if ACCT_USER_NAME is 'user', then we'll
     48     accept 'user', 'user75', 'userbuddy', etc, all as legal account
     49     usernames.
     50     """
     51     ACCT_USER_NAME  = 'user'
     52     ACCT_PASSWORD   = 'test'
     53     ACCT_AUTH_TOKEN = 'xyzzy'
     54 
     55     DATE_TIME_FORMAT = '%Y/%m/%d %H:%M'
     56 
     57     """
     58     Process a request to authenticate a client.  We assume that the username
     59     and password will be included in the request. If successful, we'll return
     60     an authtoken as the only content.  If auth fails, we'll send an "invalid
     61     credentials" error.
     62     We return a boolean indicating whether we were successful (true) or not (false).
     63     In the event that this call fails, we will setup the response, so callers just
     64     need to RETURN in the error case.
     65     """
     66     def authenticate(self):
     67         self.username = self.request.get('username')
     68         self.password = self.request.get('password')
     69 
     70         logging.info('Authenticatng username: ' + self.username)
     71 
     72         if ((self.username != None) and
     73                 (self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and
     74                 (self.password == BaseWebServiceHandler.ACCT_PASSWORD)):
     75             # Authentication was successful - return our hard-coded
     76             # auth-token as the only response.
     77             self.response.set_status(200, 'OK')
     78             self.response.out.write(BaseWebServiceHandler.ACCT_AUTH_TOKEN)
     79             return True
     80         else:
     81             # Authentication failed. Return the standard HTTP auth failure
     82             # response to let the client know.
     83             self.response.set_status(401, 'Invalid Credentials')
     84             return False
     85 
     86     """
     87     Validate the credentials of the client for a web service request.
     88     The request should include username/password parameters that correspond
     89     to our hard-coded single account values.
     90     We return a boolean indicating whether we were successful (true) or not (false).
     91     In the event that this call fails, we will setup the response, so callers just
     92     need to RETURN in the error case.
     93     """
     94     def validate(self):
     95         self.username = self.request.get('username')
     96         self.authtoken = self.request.get('authtoken')
     97 
     98         logging.info('Validating username: ' + self.username)
     99 
    100         if ((self.username != None) and
    101                 (self.username.startswith(BaseWebServiceHandler.ACCT_USER_NAME)) and
    102                 (self.authtoken == BaseWebServiceHandler.ACCT_AUTH_TOKEN)):
    103             return True
    104         else:
    105             self.response.set_status(401, 'Invalid Credentials')
    106             return False
    107 
    108 
    109 class Authenticate(BaseWebServiceHandler):
    110     """
    111     Handles requests for login and authentication.
    112 
    113     UpdateHandler only accepts post events. It expects each
    114     request to include username and password fields. It returns authtoken
    115     after successful authentication and "invalid credentials" error otherwise.
    116     """
    117 
    118     def post(self):
    119         self.authenticate()
    120 
    121     def get(self):
    122         """Used for debugging in a browser..."""
    123         self.post()
    124 
    125 
    126 class SyncContacts(BaseWebServiceHandler):
    127     """Handles requests for fetching user's contacts.
    128 
    129     UpdateHandler only accepts post events. It expects each
    130     request to include username and authtoken. If the authtoken is valid
    131     it returns user's contact info in JSON format.
    132     """
    133 
    134     def get(self):
    135         """Used for debugging in a browser..."""
    136         self.post()
    137 
    138     def post(self):
    139         logging.info('*** Starting contact sync ***')
    140         if (not self.validate()):
    141             return
    142 
    143         updated_contacts = []
    144 
    145         # Process any client-side changes sent up in the request.
    146         # Any new contacts that were added are included in the
    147         # updated_contacts list, so that we return them to the
    148         # client. That way, the client can see the serverId of
    149         # the newly added contact.
    150         client_buffer = self.request.get('contacts')
    151         if ((client_buffer != None) and (client_buffer != '')):
    152             self.process_client_changes(client_buffer, updated_contacts)
    153 
    154         # Add any contacts that have been updated on the server-side
    155         # since the last sync by this client.
    156         client_state = self.request.get('syncstate')
    157         self.get_updated_contacts(client_state, updated_contacts)
    158 
    159         logging.info('Returning ' + str(len(updated_contacts)) + ' contact records')
    160 
    161         # Return the list of updated contacts to the client
    162         self.response.set_status(200)
    163         self.response.out.write(toJSON(updated_contacts))
    164 
    165     def get_updated_contacts(self, client_state, updated_contacts):
    166         logging.info('* Processing server changes')
    167         timestamp = None
    168 
    169         base_url = self.request.host_url
    170 
    171         # The client sends the last high-water-mark that they successfully
    172         # sync'd to in the syncstate parameter.  It's opaque to them, but
    173         # its actually a seconds-in-unix-epoch timestamp that we use
    174         # as a baseline.
    175         if client_state:
    176             logging.info('Client sync state: ' + client_state)
    177             timestamp = datetime.utcfromtimestamp(float(client_state))
    178 
    179         # Keep track of the update/delete counts, so we can log it
    180         # below.  Makes debugging easier...
    181         update_count = 0
    182         delete_count = 0
    183 
    184         contacts = datastore.Contact.all()
    185         if contacts:
    186             # Find the high-water mark for the most recently updated friend.
    187             # We'll return this as the syncstate (x) value for all the friends
    188             # we return from this function.
    189             high_water_date = datetime.min
    190             for contact in contacts:
    191                 if (contact.updated > high_water_date):
    192                     high_water_date = contact.updated
    193             high_water_mark = str(long(_time.mktime(high_water_date.utctimetuple())) + 1)
    194             logging.info('New sync state: ' + high_water_mark)
    195 
    196             # Now build the updated_contacts containing all the friends that have been
    197             # changed since the last sync
    198             for contact in contacts:
    199                 # If our list of contacts we're returning already contains this
    200                 # contact (for example, it's a contact just uploaded from the client)
    201                 # then don't bother processing it any further...
    202                 if (self.list_contains_contact(updated_contacts, contact)):
    203                     continue
    204 
    205                 handle = contact.handle
    206 
    207                 if timestamp is None or contact.updated > timestamp:
    208                     if contact.deleted == True:
    209                         delete_count = delete_count + 1
    210                         DeletedContactData(updated_contacts, handle, high_water_mark)
    211                     else:
    212                         update_count = update_count + 1
    213                         UpdatedContactData(updated_contacts, handle, None, base_url, high_water_mark)
    214 
    215         logging.info('Server-side updates: ' + str(update_count))
    216         logging.info('Server-side deletes: ' + str(delete_count))
    217 
    218     def process_client_changes(self, contacts_buffer, updated_contacts):
    219         logging.info('* Processing client changes: ' + self.username)
    220 
    221         base_url = self.request.host_url
    222 
    223         # Build an array of generic objects containing contact data,
    224         # using the Django built-in JSON parser
    225         logging.info('Uploaded contacts buffer: ' + contacts_buffer)
    226         json_list = simplejson.loads(contacts_buffer)
    227         logging.info('Client-side updates: ' + str(len(json_list)))
    228 
    229         # Keep track of the number of new contacts the client sent to us,
    230         # so that we can log it below.
    231         new_contact_count = 0
    232 
    233         for jcontact in json_list:
    234             new_contact = False
    235             id = self.safe_attr(jcontact, 'i')
    236             if (id != None):
    237                 logging.info('Updating contact: ' + str(id))
    238                 contact = datastore.Contact.get(db.Key.from_path('Contact', id))
    239             else:
    240                 logging.info('Creating new contact record')
    241                 new_contact = True
    242                 contact = datastore.Contact(handle='temp')
    243 
    244             # If the 'change' for this contact is that they were deleted
    245             # on the client-side, all we want to do is set the deleted
    246             # flag here, and we're done.
    247             if (self.safe_attr(jcontact, 'd') == True):
    248                 contact.deleted = True
    249                 contact.put()
    250                 logging.info('Deleted contact: ' + contact.handle)
    251                 continue
    252 
    253             contact.firstname = self.safe_attr(jcontact, 'f')
    254             contact.lastname = self.safe_attr(jcontact, 'l')
    255             contact.phone_home = self.safe_attr(jcontact, 'h')
    256             contact.phone_office = self.safe_attr(jcontact, 'o')
    257             contact.phone_mobile = self.safe_attr(jcontact, 'm')
    258             contact.email = self.safe_attr(jcontact, 'e')
    259             contact.deleted = (self.safe_attr(jcontact, 'd') == 'true')
    260             if (new_contact):
    261                 # New record - add them to db...
    262                 new_contact_count = new_contact_count + 1
    263                 contact.handle = contact.firstname + '_' + contact.lastname
    264                 logging.info('Created new contact handle: ' + contact.handle)
    265             contact.put()
    266             logging.info('Saved contact: ' + contact.handle)
    267 
    268             # We don't save off the client_id value (thus we add it after
    269             # the "put"), but we want it to be in the JSON object we
    270             # serialize out, so that the client can match this contact
    271             # up with the client version.
    272             client_id = self.safe_attr(jcontact, 'c')
    273 
    274             # Create a high-water-mark for sync-state from the 'updated' time
    275             # for this contact, so we return the correct value to the client.
    276             high_water = str(long(_time.mktime(contact.updated.utctimetuple())) + 1)
    277 
    278             # Add new contacts to our updated_contacts, so that we return them
    279             # to the client (so the client gets the serverId for the
    280             # added contact)
    281             if (new_contact):
    282                 UpdatedContactData(updated_contacts, contact.handle, client_id, base_url,
    283                         high_water)
    284 
    285         logging.info('Client-side adds: ' + str(new_contact_count))
    286 
    287     def list_contains_contact(self, contact_list, contact):
    288         if (contact is None):
    289             return False
    290         contact_id = str(contact.key().id())
    291         for next in contact_list:
    292             if ((next != None) and (next['i'] == contact_id)):
    293                 return True
    294         return False
    295 
    296     def safe_attr(self, obj, attr_name):
    297         if attr_name in obj:
    298             return obj[attr_name]
    299         return None
    300 
    301 class ResetDatabase(BaseWebServiceHandler):
    302     """
    303     Handles cron request to reset the contact database.
    304 
    305     We have a weekly cron task that resets the database back to a
    306     few contacts, so that it doesn't grow to an absurd size.
    307     """
    308 
    309     def get(self):
    310         # Delete all the existing contacts from the database
    311         contacts = datastore.Contact.all()
    312         for contact in contacts:
    313             contact.delete()
    314 
    315         # Now create three sample contacts
    316         contact1 = datastore.Contact(handle = 'juliet',
    317                 firstname = 'Juliet',
    318                 lastname = 'Capulet',
    319                 phone_mobile = '(650) 555-1000',
    320                 phone_home = '(650) 555-1001',
    321                 status = 'Wherefore art thou Romeo?')
    322         contact1.put()
    323 
    324         contact2 = datastore.Contact(handle = 'romeo',
    325                 firstname = 'Romeo',
    326                 lastname = 'Montague',
    327                 phone_mobile = '(650) 555-2000',
    328                 phone_home = '(650) 555-2001',
    329                 status = 'I dream\'d a dream to-night')
    330         contact2.put()
    331 
    332         contact3 = datastore.Contact(handle = 'tybalt',
    333                 firstname = 'Tybalt',
    334                 lastname = 'Capulet',
    335                 phone_mobile = '(650) 555-3000',
    336                 phone_home = '(650) 555-3001',
    337                 status = 'Have at thee, coward')
    338         contact3.put()
    339 
    340 
    341 
    342 
    343 def toJSON(object):
    344     """Dumps the data represented by the object to JSON for wire transfer."""
    345     return simplejson.dumps(object)
    346 
    347 class UpdatedContactData(object):
    348     """Holds data for user's contacts.
    349 
    350     This class knows how to serialize itself to JSON.
    351     """
    352     __FIELD_MAP = {
    353         'handle': 'u',
    354         'firstname': 'f',
    355         'lastname': 'l',
    356         'status': 's',
    357         'phone_home': 'h',
    358         'phone_office': 'o',
    359         'phone_mobile': 'm',
    360         'email': 'e',
    361         'client_id': 'c'
    362     }
    363 
    364     def __init__(self, contact_list, username, client_id, host_url, high_water_mark):
    365         obj = datastore.Contact.get_contact_info(username)
    366         contact = {}
    367         for obj_name, json_name in self.__FIELD_MAP.items():
    368             if hasattr(obj, obj_name):
    369               v = getattr(obj, obj_name)
    370               if (v != None):
    371                   contact[json_name] = str(v)
    372               else:
    373                   contact[json_name] = None
    374         contact['i'] = str(obj.key().id())
    375         contact['a'] = host_url + "/avatar?id=" + str(obj.key().id())
    376         contact['x'] = high_water_mark
    377         if (client_id != None):
    378             contact['c'] = str(client_id)
    379         contact_list.append(contact)
    380 
    381 class DeletedContactData(object):
    382     def __init__(self, contact_list, username, high_water_mark):
    383         obj = datastore.Contact.get_contact_info(username)
    384         contact = {}
    385         contact['d'] = 'true'
    386         contact['i'] = str(obj.key().id())
    387         contact['x'] = high_water_mark
    388         contact_list.append(contact)
    389 
    390 def main():
    391     application = webapp.WSGIApplication(
    392             [('/auth', Authenticate),
    393              ('/sync', SyncContacts),
    394              ('/reset_database', ResetDatabase),
    395             ],
    396             debug=True)
    397     wsgiref.handlers.CGIHandler().run(application)
    398 
    399 if __name__ == "__main__":
    400     main()
    401