Добрый день, коллеги!
Как я и обещал в своей предыдущей статье, хочу поделится с вами информацией касательно использования Google Contacts API. Кому интересно узнать, как вызывать Google Contacts API из Java на Google Apps Engine — добро пожаловать под кат.
В своей разработке я использовал Google API Client Livrary for Java 1.8 — в этой версии появились нововведения, упрощающие авторизацию через OAuth. Фактически, теперь всю работу по авторизации на себя берет библиотека.
Для начала необходимо объявить класс, который будет отображать структуру контакта. Я создал класс, подклассы которого соответствуют элементам контактной информации — адресам, e-mail'ам, телефонам и мессенджерам.
Класс и его субклассы объявлены как Serializable — это даст возможность Google API Client преобразовать контакт в корректный 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.
В принципе, заголовки запроса можно не устанавливать, но в этом случае Google API считает, что передается контакт версии 2, в которой не используется, в частности, структурированное имя контакта, а вместо него используется FN. Также, если не ошибаюсь, в этой версии не сохраняются данные IM.
Более подробно прочих методах Contacts API можно прочитать на странице документации.
Как я и обещал в своей предыдущей статье, хочу поделится с вами информацией касательно использования 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 можно прочитать на странице документации.
