Сохранение контактной информации в Google Contacts

в 8:07, , рубрики: Google API, google app engine, google contacts, java, метки: , , ,

Добрый день, коллеги!

Как я и обещал в своей предыдущей статье, хочу поделится с вами информацией касательно использования 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js