Добрый день, коллеги!
Как я и обещал в своей предыдущей статье, хочу поделится с вами информацией касательно использования Google Contacts API. Кому интересно узнать, как вызывать Google Contacts API из Java на Google Apps Engine — добро пожаловать под кат.
В своей разработке я использовал Google API Client Livrary for Java 1.8 — в этой версии появились нововведения, упрощающие авторизацию через OAuth. Фактически, теперь всю работу по авторизации на себя берет библиотека.
Для начала необходимо объявить класс, который будет отображать структуру контакта. Я создал класс, подклассы которого соответствуют элементам контактной информации — адресам, e-mail'ам, телефонам и мессенджерам.
public class Contact implements Serializable {
private static final String GDATA_URI = "http://schemas.google.com/g/2005#";
public static final String SOURCE_LABEL = "Source";
public class TypedSubElem implements Serializable{
protected String getURIPrefix(){
return Contact.GDATA_URI;
}
protected Map<String,String> getRelList(){
final Map<String,String> relList = Collections.unmodifiableMap(new HashMap<String,String>()
{{put("home","дом.");
put("other","проч.");
put("work","раб.");
}});
return relList;
}
@Key("@rel")
protected String type = getURIPrefix()+"other";
@Key("@label")
protected String label = null;
public String getType(){
return (type == null)?null:type.substring(getURIPrefix().length());
}
public String getLabel(){
return label;
}
public String getReadableType(){
String ret = getRelList().get(getType());
if(ret == null)
ret = label;
return ret;
}
public void setType(String type){
this.label = null;
this.type = getURIPrefix()+(getRelList().containsKey(type)?type:"other");
}
public void setLabel(String label){
this.type = null;
this.label = label;
}
}
public class FN implements Serializable {
@Key("text()")
public String text;
}
public class Content implements Serializable {
@Key("text()")
public String text;
}
public class Org extends TypedSubElem {
@Key("gd:orgName")
public String orgName;
@Key("gd:orgTitle")
public String orgTitle;
@Key("gd:orgDepartment")
public String orgDepartment;
/**
* @param type the type to set
*/
public void setWork(Boolean work) {
setType(work?"work":"other");
}
}
public class Phone extends TypedSubElem {
@Override
protected Map<String,String> getRelList(){
final Map<String,String> relList = Collections.unmodifiableMap(new HashMap<String,String>()
{{put("assistant","секретарь");
put("callback","перезв.");
put("car","авто");
put("company_main","орг. осн."));
put("fax","факс");
put("home","дом.");
put("home_fax","дом. факс");
put("isdn","ISDN");
put("main","осн.");
put("mobile","моб.");
put("other","проч.");
put("other_fax","проч. факс");
put("pager","пейджер");
put("radio","радио");
put("telex","телекс");
put("tty_tdd","терм.");
put("work","раб.");
put("work_fax","раб. факс");
put("work_mob","раб. моб.");
put("work_pager","раб. пейджер");
}});
return relList;
}
@Key("text()")
public String text;
}
public class Email extends TypedSubElem{
@Key("@address")
public String address;
}
public class Address extends TypedSubElem{
@Key("gd:street")
public String street;
@Key("gd:city")
public String city;
@Key("gd:region")
public String region;
@Key("gd:postcode")
public String postcode;
@Key("gd:country")
public String country;
@Key("gd:formattedAddress")
public String fullAddress;
}
public class Name extends SubElem{
@Key("gd:givenName")
public String givenName;
@Key("gd:additionalName")
public String additionalName;
@Key("gd:familyName")
public String familyName;
@Key("gd:namePrefix")
public String namePrefix;
@Key("gd:nameSuffix")
public String nameSuffix;
@Key("gd:fullName")
private String fullName;
}
public class Link extends TypedSubElem {
@Key("@href")
public String url;
@Override
protected Map<String,String> getRelList(){
final Map<String,String> relList = Collections.unmodifiableMap(new HashMap<String,String>()
{{put("home-page","дом.");
put("blog","блог");
put("work","раб.");
put("profile","профиль");
put("other","проч.");
}});
return relList;
}
@Override
protected String getURIPrefix(){
return "";
}
}
public class IM extends Email {
@Key("@protocol")
public String protocol;
@Override
protected Map<String,String> getRelList(){
final Map<String,String> relList = Collections.unmodifiableMap(new HashMap<String,String>()
{{put("home","дом.");
put("other","проч.");
put("netmeeting","NetMeeting");
put("work","раб.");
}});
return relList;
}
protected Map<String,String> getProtoList(){
final Map<String,String> relList = Collections.unmodifiableMap(new HashMap<String,String>()
{{put("AIM","AIM");
put("MSN","MSN");
put("YAHOO","Yahoo");
put("SKYPE","Skype");
put("QQ","QQ");
put("GOOGLE_TALK","GTalk");
put("ICQ","ICQ");
put("JABBER","Jabber");
}});
return relList;
}
public String getProtocol(){
return (protocol == null)?null:protocol.substring(protocol.lastIndexOf('#')+1);
}
public String getReadableProto(){
return getProtoList().get(getProtocol());
}
public void setProtocol(String type){
this.protocol = (getProtoList().containsKey(type))?GDATA_URI+type:null;
}
}
@Key("gContact:website")
public List<Link> links = new ArrayList<Link>();
@Key
public Content content;
@Key("title")
public FN fn;
@Key("gd:phoneNumber")
public List<Phone> phones = new ArrayList<Phone>();
@Key("gd:email")
public List<Email> emails = new ArrayList<Email>();
@Key("gd:organization")
public List<Org> orgs = new ArrayList<Org>();
@Key("gd:structuredPostalAddress")
public List<Address> addresses = new ArrayList<Address>();
@Key("gd:im")
public List<IM> IMs = new ArrayList<IM>();
@Key("gd:name")
public Name name;
}
Класс и его субклассы объявлены как Serializable — это даст возможность Google API Client преобразовать контакт в корректный Atom. Для этого же все поля, которые предполагается сохранять, аннотированы @Key
— так указывается соответствие полей нашего класса и структуры Atom.
Отдельного пояснения заслуживают поля rel и label. rel — это типовая маркировка контактной информации из предопределенного списка. Например, для e-mail'а это будет указание на то, является ли адрес домашним или рабочим. В Google API передается значение вида «schemas.google.com/g/2005#<вид>». Но кроме типовых маркировок контактную информацию можно пометить произвольным обозначением. Для этого служит поле label. Google API ожидает, что будет заполнено одно из них — или rel, или label.
Более подробно про структуру данных контакта можно почитать здесь.
Для отправки контакта в Google создадим сервлет, наследующий от AbstractAppEngineAuthorizationCodeServlet. Этот класс позволяет автоматически проверить, авторизован ли пользователь (посредством авторизации в Google Accounts или, если установить соответствующие настройки в Google Apps Engine — то средствами Federated Login). Если пользователь авторизован — то у него будет запрошено (опять же средствами библиотечного сервлета — самому ничего писать не надо) разрешение на доступ к его контактам в Google Contacts. Полученные токены будут сохранены для дальнейшего использования в хранилище токенов (я использовал AppEngineCredentialStore). За всю последовательность действий отвечает GoogleAuthorizationCodeFlow.
import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.appengine.auth.oauth2.AbstractAppEngineAuthorizationCodeServlet;
import com.google.api.client.extensions.appengine.auth.oauth2.AppEngineCredentialStore;
import com.google.api.client.extensions.appengine.http.urlfetch.UrlFetchTransport;
import com.google.api.client.googleapis.GoogleHeaders;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.http.*;
import com.google.api.client.http.xml.atom.AtomContent;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson.JacksonFactory;
import com.google.api.client.xml.XmlNamespaceDictionary;
import java.io.IOException;
import java.util.Collections;
import java.util.ResourceBundle;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class Action extends AbstractAppEngineAuthorizationCodeServlet {
private static final String DEFAULT_BASE_URL = "https://www.google.com/m8/feeds/contacts/default/full";
public static final String SCOPE = "https://www.google.com/m8/feeds/";
public static final String CALLBACK_URI = "/action/google_contacts/oauth2callback";
private static final String APP_NAME = "<Имя приложения>";
public static final String CLIENT_ID = "<идентификатор приложения>";
public static final String CLIENT_SECRET = "<секрет>";
private static final HttpTransport transport = new UrlFetchTransport();
private static final JsonFactory jsonFactory = new JacksonFactory();
static final XmlNamespaceDictionary DICTIONARY = new XmlNamespaceDictionary()
.set("", "http://www.w3.org/2005/Atom")
.set("gd", "http://schemas.google.com/g/2005")
.set("gContact", "http://schemas.google.com/contact/2008");
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
Credential credential = getCredential();
Contact contact = (Contact) request.getSession().getAttribute("Contact");
if(credential != null && contact != null) {
HttpRequestFactory requestFactory = transport.createRequestFactory(getCredential());
assert requestFactory!=null;
HttpRequest req = requestFactory.buildPostRequest(new GenericUrl(DEFAULT_BASE_URL), null);
GoogleHeaders headers = new GoogleHeaders();
headers.setApplicationName(APP_NAME);
headers.setGDataVersion("3");
req.setHeaders(headers);
AtomContent content = AtomContent.forEntry(DICTIONARY, contact);
req.setContent(content);
try{
HttpResponse resp = req.execute();
} catch (HttpResponseException e) {
}
}
}
@Override
protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
GenericUrl url = new GenericUrl(req.getRequestURL().toString());
url.setRawPath(CALLBACK_URI);
return url.build();
}
@Override
protected AuthorizationCodeFlow initializeFlow() throws IOException {
return new GoogleAuthorizationCodeFlow.Builder(new UrlFetchTransport(), new JacksonFactory(),
CLIENT_ID, CLIENT_SECRET,
Collections.singleton(SCOPE)).setCredentialStore(
new AppEngineCredentialStore()).build();
}
}
В принципе, заголовки запроса можно не устанавливать, но в этом случае Google API считает, что передается контакт версии 2, в которой не используется, в частности, структурированное имя контакта, а вместо него используется FN. Также, если не ошибаюсь, в этой версии не сохраняются данные IM.
Более подробно прочих методах Contacts API можно прочитать на странице документации.
Автор: gorynych_zmey