//========================================================================
//$Id: ContactsJSONServlet.java 457 2011-08-25 08:03:38Z janb.webtide $
//Copyright 2009 Mort Bay Consulting Pty. Ltd.
//------------------------------------------------------------------------
//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 org.mortbay.ijetty.console;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.URIUtil;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.net.Uri;
import android.provider.Contacts;
import android.util.Log;
/**
* ContactsJSONServlet
*
* A servlet to support restful style requests for Android Contacts.
* Restful request uri structure:
*
* /rest/contacts/photo/[who]
* A request to obtain the photo corresponding to contact [who]
* eg
* /rest/contacts/photo/123
*
* /rest/contacts/[who]/[what][?action=0|1|2|3|4][&id=x]
*
* who = contact db id
*
* eg request to retrieve Contact information about Contact 123:
*
* /rest/contacts/123
*
* eg request to delete Contact 123:
*
* /rest/contacts/123?action=3
*
* eg request to save a new Contact:
* /rest/contacts/?action=4
*
*
* eg request to saved updated Contact:
* /rest/contacts?action=1&id=123
*
* eg request to retrieve 10 contacts starting at Contact 25:
* /rest/contacts?start=25&pg=10
*
* eg request to retrieve all contacts:
* /rest/contacts/
*
*
*/
public class ContactsJSONServlet extends HttpServlet
{
private static final String TAG = "ContactsSrvlt";
private static final long serialVersionUID = 1L;
public static int __VERSION = 0;
public static final int __ACTION_NONE = -1;
public static final int __ACTION_CALL = 0;
public static final int __ACTION_DEL = 3;
public static final int __ACTION_SAVE = 4;
public static final int __DEFAULT_PG_START = 0;
public static final int __DEFAULT_PG_SIZE = 10;
public static final String __ACTION_PARAM = "action";
public static final String __PG_START_PARAM = "pgStart";
public static final String __PG_SIZE_PARAM = "pgSize";
private ContentResolver resolver;
@Override
public void init(ServletConfig config) throws ServletException
{
super.init(config);
resolver = (ContentResolver)getServletContext().getAttribute("org.mortbay.ijetty.contentResolver");
}
public ContentResolver getContentResolver()
{
return resolver;
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
doGet(request,response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
String pathInfo = request.getPathInfo();
String servletPath = request.getServletPath();
String pathInContext = URIUtil.addPaths(servletPath,pathInfo);
if (pathInfo == null)
{
//redirect any requests for /console/contacts to be /console/contacts/
RequestDispatcher dispatcher = request.getRequestDispatcher(pathInContext + "/");
dispatcher.forward(request,response);
}
else
{
StringTokenizer strtok = new StringTokenizer(pathInfo,"/");
String who = null;
boolean isPhoto = false;
int action = __ACTION_NONE;
if (strtok.hasMoreElements())
{
String tmp = strtok.nextToken();
if ("photo".equalsIgnoreCase(tmp))
isPhoto = true;
else
who = tmp;
}
if (who == null && strtok.hasMoreElements())
{
who = strtok.nextToken();
}
String tmp = request.getParameter(__ACTION_PARAM);
action = (tmp == null?__ACTION_NONE:Integer.parseInt(tmp.trim()));
switch (action)
{
case __ACTION_NONE:
{
if (who != null)
{
//a specific contact is being requested
if (isPhoto)
handleGetImage(request,response,who);
else
handleGetContact(request,response,who);
}
else
{
//Get all contacts
String str = request.getParameter(__PG_START_PARAM);
int pgStart = (str == null ? -1 : Integer.parseInt(str.trim()));
str = request.getParameter(__PG_SIZE_PARAM);
int pgSize = (str == null ? -1 : Integer.parseInt(str.trim()));
handleGetContacts(request,response, pgStart, pgSize);
}
break;
}
case __ACTION_CALL:
{
//
// Call a Contact
// TODO not implemented
//
break;
}
case __ACTION_SAVE:
{
//
// Save a Contact, either new or edited
//
who = request.getParameter("id");
handleSaveContact(request,response,who);
break;
}
case __ACTION_DEL:
{
//
// Delete a contact
//
handleDeleteContact(request,response,who);
break;
}
default:
{
handleDefault(request,response);
}
}
}
}
protected void deleteUser(PrintWriter writer, HttpServletRequest request, HttpServletResponse response, String id) throws ServletException, IOException
{
try
{
Contact.delete(getContentResolver(),id);
}
catch (Exception e)
{
// TODO: Better error catching - ie. check for invalid user and just failure at remove
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setStatus(HttpServletResponse.SC_OK);
writer.println("{ \"status\": \"OK\" }");
}
private void getContactMethods(String who, ContactMethod.ContactMethodsCollection contactMethods, PrintWriter writer)
{
writer.print("[");
ContentValues contactMethod;
StringBuffer buff = new StringBuffer();
while (contactMethods != null && (contactMethod = contactMethods.next()) != null)
{
String id = contactMethod.getAsString(android.provider.BaseColumns._ID);
String data = contactMethod.getAsString(Contacts.ContactMethodsColumns.DATA);
String auxData = contactMethod.getAsString(Contacts.ContactMethodsColumns.AUX_DATA);
String label = contactMethod.getAsString(Contacts.ContactMethodsColumns.LABEL);
int isPrimary = contactMethod.getAsInteger(Contacts.ContactMethodsColumns.ISPRIMARY).intValue();
int kind = contactMethod.getAsInteger(Contacts.ContactMethodsColumns.KIND).intValue();
int type = contactMethod.getAsInteger(Contacts.ContactMethodsColumns.TYPE).intValue();
buff.append("{ \"data\": \"" + data + "\", \"aux\": \"" + auxData + "\", \"label\": \"" + (label == null?"\"":label + "\"") + ", \"primary\": "
+ isPrimary + ", \"kind\": " + kind + ", \"type\": " + type + ", \"id\": \"" + id + "\"}");
if (contactMethods.hasNext())
{
buff.append(",");
}
}
writer.print(buff.toString());
writer.print("]");
}
private void getPhones(String who, Phone.PhoneCollection phones, PrintWriter writer)
{
ContentValues phone;
writer.print("[ ");
StringBuffer buff = new StringBuffer();
while (phones != null && (phone = phones.next()) != null)
{
String id = phone.getAsString(android.provider.BaseColumns._ID);
String label = phone.getAsString(Contacts.PhonesColumns.LABEL);
String number = phone.getAsString(Contacts.PhonesColumns.NUMBER);
int type = phone.getAsInteger(Contacts.PhonesColumns.TYPE).intValue();
buff.append("{\"number\": \"" + number + "\", \"label\" : \"" + (label == null?"":label.replace("\'","\\'")) + "\", \"type\" : " + type
+ ", \"id\": \"" + id + "\" }");
if (phones.hasNext())
{
buff.append(", ");
}
}
writer.print(buff.toString());
writer.print(" ]");
}
private void getSummary(ContentValues values, PrintWriter writer)
{
if ((values != null) && (writer != null))
{
String id = values.getAsString(android.provider.BaseColumns._ID);
String name = values.getAsString(Contacts.PeopleColumns.DISPLAY_NAME);
String notes = values.getAsString(Contacts.PeopleColumns.NOTES);
Integer i = values.getAsInteger(Contacts.PeopleColumns.STARRED);
boolean starred = (i == null?false:i.intValue() > 0);
i = values.getAsInteger(Contacts.PeopleColumns.SEND_TO_VOICEMAIL);
boolean voicemail = (i == null?false:i.intValue() > 0);
writer.print("{\"name\": \"" + name.replace("\'","\\'") + "\", ");
writer.print("\"id\": \"" + id + "\", ");
writer.print("\"starred\": " + Boolean.toString(starred) + ", ");
writer.print("\"voicemail\": " + Boolean.toString(voicemail));
if (notes != null)
{
writer.print(", \"notes\": \"" + notes.replace("'","\\'") + "\"");
}
writer.print(" }");
}
}
/**
* Generate a JSON representation of a Contact: eg.
*
* <pre>
* {
* summary : {
* name: "Fred Smith",
* id : 123,
* starred: true/false,
* voicemail: true/false,
* notes: "Notes."
* },
* phones : [
* {
* id: "98989",
* number: "123456",
* label: "Home",
* type: "Home",
* id: 900
* },
* {
* id: "98989",
* number: "989877",
* label: "Work",
* type: "Work",
* id: 901
* }
* ],
* contacts: [
* {
* id: "99999",
* data: "fred@smith.org",
* aux: "",
* label: "Home",
* primary: true/false,
* kind: "Email",
* type: "Home"
* }
* ]
* }
*
* </pre>
*
* @param writer
* @param request
* @param response
* @param who
* @throws ServletException
* @throws IOException
*/
protected void getContact (PrintWriter writer, HttpServletRequest request, HttpServletResponse response, String who) throws ServletException, IOException
{
//query for the user's standard details
ContentValues values = Contact.get(getContentResolver(),who);
if (values == null)
writer.println("{\"error\": \"No such user.\"}");
else
{
writer.print("{ \"summary\" : ");
getSummary(values,writer);
writer.print(", \"phones\" : ");
//query for all phone details
Phone.PhoneCollection phones = Phone.getPhones(getContentResolver(),who);
getPhones(who,phones,writer);
phones.close();
writer.print(", \"contacts\" : ");
//query for all contact details
ContactMethod.ContactMethodsCollection contactMethods = ContactMethod.getContactMethods(getContentResolver(),who);
getContactMethods(who,contactMethods,writer);
contactMethods.close();
writer.print(", \"version\": " + __VERSION);
writer.print(" }");
}
}
private void getContacts (Contact.ContactCollection users, PrintWriter writer, int pgSize)
{
writer.println("{\"version\": " + __VERSION+", ");
writer.println("\"total\": "+users.getTotal()+", ");
writer.println("\"contacts\": ");
writer.println("[");
if (users != null)
{
ContentValues user = null;
int count = pgSize;
while ((pgSize <= 0 || count-- > 0) && (user = users.next()) != null)
{
writer.println("{");
String id = user.getAsString(android.provider.BaseColumns._ID);
String name = user.getAsString(Contacts.PeopleColumns.DISPLAY_NAME);
Integer i = user.getAsInteger(Contacts.PeopleColumns.STARRED);
boolean starred = (i == null?false:i.intValue() > 0);
writer.println("\"id\" : \"" + id + "\", ");
writer.println("\"name\" : \"" + name + "\", ");
writer.println("\"starred\": " + starred);
writer.println("}");
if (users.hasNext())
{
writer.println(",");
}
}
}
writer.println("]");
writer.println("}");
}
public void handleDefault(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
handleGetContacts(request,response, __DEFAULT_PG_START, __DEFAULT_PG_SIZE);
}
public void handleDeleteContact(HttpServletRequest request, HttpServletResponse response, String who)
throws ServletException, IOException
{
response.setContentType("text/json; charset=utf-8");
PrintWriter writer = response.getWriter();
deleteUser(writer,request,response,who);
}
public void handleGetContact(HttpServletRequest request, HttpServletResponse response, String who)
throws ServletException, IOException
{
response.setContentType("text/json; charset=utf-8");
PrintWriter writer = response.getWriter();
getContact (writer,request,response,who);
response.setStatus(HttpServletResponse.SC_OK);
writer.write("\r\n");
writer.close();
}
public void handleGetContacts(HttpServletRequest request, HttpServletResponse response,
int pgStart, int pgSize)
throws ServletException, IOException
{
PrintWriter writer = response.getWriter();
response.setContentType("text/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
Contact.ContactCollection users = Contact.getContacts(getContentResolver(), pgStart, pgSize);
getContacts(users,writer, pgSize);
users.close();
}
public void handleSaveContact(HttpServletRequest request, HttpServletResponse response, String who) throws ServletException, IOException
{
//Do NOT return any data. This is because the json form submission is
//being sent to a hidden iframe otherwise the multipart mime enctype of
//form would cause the browser to replace the whole content of the page
//with the response.
saveContactFormData(request,response,request.getParameter("id"));
}
public void handleGetImage(HttpServletRequest request, HttpServletResponse response, String who) throws IOException
{
InputStream is = null;
try
{
Uri personUri = ContentUris.withAppendedId(Contacts.People.CONTENT_URI,Long.valueOf(who.trim()).longValue());
is = Contacts.People.openContactPhotoInputStream(getContentResolver(),personUri);
if (is == null)
{
response.setContentType("image/jpeg");
OutputStream os = response.getOutputStream();
is = getServletContext().getResourceAsStream("/android.jpg");
IO.copy(is,os);
}
else
{
response.setContentType("image/png");
OutputStream os = response.getOutputStream();
IO.copy(is,os);
}
}
finally
{
if (is != null)
is.close();
}
}
public void saveContactFormData(HttpServletRequest request, HttpServletResponse response, String id) throws ServletException, IOException
{
ContentValues person = new ContentValues();
person.put(Contacts.People.NAME,request.getParameter("name"));
person.put(Contacts.People.NOTES,request.getParameter("notes"));
person.put(Contacts.People.STARRED,request.getParameter("starred") != null?1:0);
person.put(Contacts.People.SEND_TO_VOICEMAIL,request.getParameter("voicemail") != null?1:0);
id = (id == null?id:id.trim());
id = (id == null?id:("".equals(id)?null:id));
Log.i(TAG,"Saving: name=" + request.getParameter("name") + " notes=" + request.getParameter("notes") + " id=" + id + " starred="
+ request.getParameter("starred"));
if (id == null)
{
// Create it first if necessary (so we can save phone data)
id = Contact.create(getContentResolver(),person);
Log.d(TAG,"Inserted new Contact id " + id);
}
File photo = (File)request.getAttribute("new-pic");
if (photo != null)
{
//a new picture for the Contact has been uploaded
Contact.savePhoto(getContentResolver(),id,photo);
}
List<String> deletedPhones = new ArrayList<String>();
Map<String, ContentValues> modifiedPhones = new HashMap<String, ContentValues>();
List<String> deletedContacts = new ArrayList<String>();
Map<String, ContentValues> modifiedContacts = new HashMap<String, ContentValues>();
Enumeration<?> enumeration = request.getParameterNames();
while (enumeration.hasMoreElements())
{
String name = (String)enumeration.nextElement();
if (name.startsWith("phone-del-"))
{
//a phone to delete
String phId = name.substring(10);
Log.d(TAG,"Phone to delete: " + phId + " from " + name);
String val = request.getParameter(name);
if ("del".equals(val))
{
deletedPhones.add(phId);
}
}
else if (name.startsWith("contact-del-"))
{
String methodId = name.substring(12);
Log.d(TAG,"Contact method to delete: " + methodId + " from " + name);
String val = request.getParameter(name);
if ("del".equals(val))
{
deletedContacts.add(methodId);
}
}
else if (name.startsWith("phone-number-"))
{
String phId = name.substring("phone-number-".length());
if (request.getParameter("phone-del-" + phId) == null)
{
String typeStr = request.getParameter("phone-type-"+phId);
String number = request.getParameter("phone-number-" + phId);
String label = request.getParameter("phone-type-label-"+phId);
Log.d(TAG, "Modified phone type="+typeStr+" number="+number+" label="+label);
ContentValues phone = new ContentValues();
phone.put(Contacts.Phones.NUMBER,number);
phone.put(Contacts.Phones.TYPE,typeStr);
if (label != null && !"".equals(label))
phone.put(Contacts.Phones.LABEL, label);
modifiedPhones.put(phId,phone);
}
}
else if (name.startsWith("contact-kind-"))
{
String methodId = name.substring(13);
Log.d(TAG,"Possible contact modification: " + methodId);
if (request.getParameter("contact-del-" + methodId) == null)
{
String kind = request.getParameter(name);
String type = request.getParameter("contact-type-" + methodId);
String val = request.getParameter("contact-val-" + methodId);
String label = request.getParameter("contact-type-label-"+methodId);
ContentValues contactMethod = new ContentValues();
contactMethod.put(Contacts.ContactMethodsColumns.KIND,kind);
contactMethod.put(Contacts.ContactMethodsColumns.TYPE,type);
contactMethod.put(Contacts.ContactMethodsColumns.DATA,val);
if (label != null && !"".equals(label))
contactMethod.put(Contacts.ContactMethodsColumns.LABEL, label);
Log.d(TAG,"Modified contact " + methodId + " kind=" + kind + " type=" + type + " val=" + val+" label="+label);
modifiedContacts.put(methodId,contactMethod);
}
}
}
//Handle addition and modifications to phones
for (String key : modifiedPhones.keySet())
{
ContentValues phone = modifiedPhones.get(key);
if ("x".equals(key))
{
//new phone, check a number has been given
String number = phone.getAsString(Contacts.Phones.NUMBER);
if ((number != null) && !"".equals(number))
{
Log.d(TAG,"Adding new phone with number=" + number);
Phone.addPhone(getContentResolver(),phone,id);
}
}
else
{
//possibly modified phone, save anyway
Log.d(TAG,"Saving phone id=" + key);
Phone.savePhone(getContentResolver(),phone,key,id);
}
}
//Get rid of the deleted phones
for (String phId : deletedPhones)
{
Phone.deletePhone(getContentResolver(),phId,id);
Log.d(TAG,"Deleted phone " + phId);
}
//Handle addition and modifications to contacts
for (String key : modifiedContacts.keySet())
{
ContentValues contactMethod = modifiedContacts.get(key);
if ("x".equals(key))
{
//could be a new contact method, check if any data has been provided
String data = contactMethod.getAsString(Contacts.ContactMethodsColumns.DATA);
Log.d(TAG,"Data for new contact method : " + data);
if ((data != null) && !"".equals(data))
{
Log.d(TAG,"Adding new contact method with data " + data);
ContactMethod.addContactMethod(getContentResolver(),contactMethod,id);
}
}
else
{
//modified contact method, save it
Log.d(TAG,"Saving contact method " + key);
ContactMethod.saveContactMethod(getContentResolver(),contactMethod,key,id);
}
}
//Get rid of deleted contacts
for (String methodId : deletedContacts)
{
ContactMethod.deleteContactMethod(getContentResolver(),methodId,id);
}
Contact.save(getContentResolver(),person,id);
__VERSION++;
Log.d(TAG,"Updating Contact id " + id);
}
}