JAX-RSでファイルダウンロード

全国に寒波がきていたようで、ここしばらくは大分寒かったですね。
バイク乗りの私としては早々に暖かくなっていただきたいものです。
こんにちわ、DiceK Mikamiです。

今回は軽めのネタとして、JAX-RSを利用した際にバイナリを返却する方法を紹介したいと思います。
百聞は一見に如かず。
と言うことで、まずはサンプルコードを御覧ください。

import java.io.File;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;

@Path("/image")
public class SampleResource {
 @GET
 @Produces("image/png") 	//ポイント1
 @Path("{id}")
 public Response getImage(@PathParam("id") Integer imageId){
     File file = new File("<ファイルの場所>"); //ポイント2

     ResponseBuilder response = Response.ok((Object)file); //ポイント3
     String headerVal = "attachment; filename="+imageId.toString()+".png";
     response.header("Content-Disposition",headerVal);
     return response.build();
 }
}

全く簡単だ。
「今回はこれまで」といきたいところですが、ポイントをいくつか下記します。

  • ポイント1
    返却するバイナリのMIMEタイプを指定します。
  • ポイント2
    バイナリをjava.io.File形式で取得します。
  • ポイント3
    javax.ws.rs.core.Response.ResponseBuilderを使用して、ヘッダを追加します。

バイナリファイルをダウンロードさせたいという要求があった場合などに便利ですね。
サンプルでは画像ファイルを返却するようにしておりますが、
@ProducesにてダウンロードさせたいバイナリのMIMEタイプを指定すれば、切り替えることができます。

OAuth2.0 Web Application Flowの実装

はじめに

はじめまして。infoScoop OpenSource開発のお手伝いをさせていただいておりますDiceK Mikamiと申します。
開発者ブログでは、ウェブ/ローカル問わず簡単な技術Tipsなどに関して投稿させていただこうかと考えております。
とは言いましても、私自身まだまだ若輩ゆえ誤り等々があるかと思いますので、
その際には皆様からの生暖かいご指摘/アドバイスなどいただければ幸いです。
今後ともよろしくお願いいたします。

OAuth2.0認証

OAuth2.0は、OAuth1.0Aにて不便と思われていたいくつかの仕様(例えば、署名など)を改善し、
より簡単にサービス間の連携を行うための認証機構です。
OAuth2.0による認証はGoogleやFacebookをはじめ、いくつかのサービスですでに提供されるようになりましたが、
仕様自体はまだドラフトの段階であり、未だ策定作業が続いていると言うのが現状です。

The OAuth 2.0 Authorization Protocol draft-ietf-oauth-v2-23

ですので、現状でサーバーサイドにてOAuth2.0実装を行う場合、
策定されているドラフトを元にするか、あるいは他のサービスとの連携をふまえて実装する必要性があります。
今回は実際にinfoScoop OpenSource V3.0にて実装したコードをパクって経験を活かして、
サーバーサイドでOAuth2.0認証をしてみることにします。

OAuth2.0 Web Application Flow

Web Application Flowでは、下図のような流れで認証し、データを取得することになります。

図では上方向から下方向にかけて、状態が遷移していきます。
Aはアプリケーション側のスタートポイントクラスになります。
ここで認証サーバーに対して認可コードを要求します。
認可コードとはOAuth1.0Aにおけるリクエストトークンに類似したものですが、
基本的にワンタイムの使用となります。
認可コードを得るためには、ユーザーは認証サーバーに対してログインする必要があります。
また、この際にアクセスするAPIに対しての権限を許可するかどうかの応答も行う必要があります。
Bは認証サーバー側のAuthorizationクラスになります。

認可コードはアプリケーションのコールバッククラスに返却されます。(C)
コールバックURLは認証サーバー側に登録しておく必要があります。
Google、Facebookで確認したところ適当なURLではダメでした。
コールバッククラスにて、認可コードを使ってアクセストークンを要求します。
取得先は認証サーバーごとに提示されているAccessTokenクラスになります。(D)
アクセストークンを返却してくる際に、認証サーバーはアクセストークンの持続時間(expires)とリフレッシュトークンも同時に返却してきます。
OAuth2.0ではアクセストークンに持続時間が付与されており、
時間が過ぎてしまったトークンは無効化されてしまいます。
この際、無効化されたトークンを再発行するために利用するのがリフレッシュトークンとなります。
注釈:現状、Facebookではリフレッシュトークンを返却しません。サービスによって実装はまちまちです。

サンプルコード

サンプルでは、Facebookからフレンド一覧を取得するコードをもとに実際の挙動を解説します。
今回のサンプルでは、スタートポイントとなるOAuth2Sample.javaと
コールバッククラスとなるOAuth2Callback.javaの2つのサーブレットを使用します。
(サンプルは要点だけを抜粋して記述してあります。また、実際にOAuth2.0を試すためには各サービス毎にアプリケーションの登録を行う必要があります)

// OAuth2Sample.java
 public void doGet(HttpServletRequest req, HttpServletResponse res)
       throws IOException, ServletException {
  // 認可コード取得URLを返却------------------(a)
  res.setContentType("text/html; charset=UTF-8");
  PrintWriter out = res.getWriter();
  out.println(createHTML());
  out.close();
 }
 
 private String createHTML(){
  StringBuffer sb = new StringBuffer();
  sb.append("<html><head><title>OAuth2サンプル</title></head><body>");
  sb.append("<a href='"+AUTH_URL);
  sb.append("?client_id="+APP_ID);
  sb.append("&redirect_uri="+REDIRECT_URL);
  sb.append("&response_type=code");
  sb.append("&state=xxx");
  sb.append("&scope="+"user_about_me,offline_access");
  sb.append("'>認可コードを取得します。");
  sb.append("</a></body></html>");
  return (new String(sb));
 }

(a)より認可コードを取得するためのアドレスをクライアントに返却する処理を記述しています。
認可コード取得先にはGETリクエストする必要がありますので、そのままアドレス返却します。
認可コード取得先にリクエストするためのパラメータは以下になります。

GET https://www.facebook.com/dialog/oauth
 client_id: <サービスから発行されるAPP ID>
 redirect_uri: <サービスに登録したコールバックURL>
 response_type: "code"
 state: <コールバックURLに渡したい値を記述>
 scope: "user_about_me,offline_access"

Facebookではリフレッシュトークンが実装されていません。
そのためアクセストークンを永続化させるためにscopeのパラメータとして「offline_access」を付加しています。
扱えるスコープの情報に関しては、各サービスのドキュメントを参考にしてください。

facebook Developer(英語)

リクエストを行うと、以下のような形で値が返却されます。

// OAuth2Callback.java
 public void doGet(HttpServletRequest req, HttpServletResponse res)
       throws IOException, ServletException {
  // 認可コード解析------------------(b)
  HashMap<String,String> authCodeMap = parseRequest(req);
  HttpClient client = new DefaultHttpClient();
  
  // アクセストークン取得------------------(c)
  HttpPost httpPost = new HttpPost(TOKEN_URL);
  List<NameValuePair> postParams = new ArrayList<NameValuePair>();
  postParams.add(new BasicNameValuePair("code",authCodeMap.get("code")));
  postParams.add(new BasicNameValuePair("client_id",APP_ID));
  postParams.add(new BasicNameValuePair("client_secret",APP_SECRET));
  postParams.add(new BasicNameValuePair("redirect_uri",REDIRECT_URL));
  postParams.add(new BasicNameValuePair("grant_type","authorization_code"));
  httpPost.setEntity(new UrlEncodedFormEntity(postParams, HTTP.UTF_8));
  HttpResponse authTokenResponse = client.execute(httpPost);
  
  // アクセストークン解析------------------(d)
  HttpEntity httpEntity = authTokenResponse.getEntity();
  HashMap<String,String> accessTokenMap = parseQuery(EntityUtils.toString(httpEntity));
  
  // 実データ取得------------------(e)
  HttpGet httpGet
       = new HttpGet(ACCESS_URL+"?access_token="+accessTokenMap.get("access_token"));)
  HttpResponse dataResponse = client.execute(httpGet);
  HttpEntity dataEntity = dataResponse.getEntity();
  
  res.setContentType("text/html; charset=UTF-8");
  PrintWriter out = res.getWriter();
  out.println(EntityUtils.toString(dataEntity));
  out.close();
 }

認可コードは登録したコールバックURLに返却されてきます。
(b)では返却された認可コードを解析し、ハッシュマップに登録しています。

認可コードが取得できたら、認可コードを利用してアクセストークンを取得します。
アクセストークン取得先には、POSTリクエストする必要があります。
(サーバーサイドからHTTPアクセスできるようにApache commons HTTP Clientを利用しています)
リクエストの際のパラメータは以下になります。

POST https://graph.facebook.com/oauth/access_token
 code: <返却された認可コード>
 client_id: <サービスから発行されるAPP ID>
 client_secret: <サービスから発行されるAPP SECRET>
 redirect_uri: <サービスに登録したコールバックURL>
 grant_type: "authorization_code"

アクセストークンリクエスト時の返却値は以下のようになります。
今回は「offline_access」スコープを付与しているので、expiresは返却されていません。


最後に返却されてきたアクセストークンを利用して、データを取得しています。
アクセストークンをサービスに渡す方法は複数提案されておりますが、
今回はGETアクセスのパラメータとしてトークンを渡しています。

GET https://graph.facebook.com/me/friends
 access_token: <返却されたアクセストークン>

結果として以下のデータが取得できました。
個人情報が含まれるためモザイクを入れておりますが、
data属性にフレンド情報が入っていることが確認できると思います。

Gmailのメールの本文を直接開くURLを取得する方法

Googleは様々なサービスのAPIを公開しており、Gmailもその中の一つです。
今回は、GmailをIMAP APIを使ってメールの情報を取得する方法を紹介します。

IMAPでGmailを取得する

GmailをIMAPで取得するためのAPIとして、OracleからJavaMailが提供されています。
http://www.oracle.com/technetwork/java/javamail/index.html

上記のページの右側のDownloadsから、javamail1_4_4.zip (2011年12月20日時点)をダウンロードします。
ダウンロードした zip を解凍すると、mail.jar を取得できます。

このライブラリを使ったメールの取得については下記の記事を参照してください。
http://harikrishnan83.wordpress.com/2009/01/24/access-gmail-with-imap-using-java-mail-api/

 

メールの本文を開くURLを取得する

JavaMailを使用した場合、メールのタイトルや本文は取得できますが、メールを直接開くURLは取得できません。

そこで、JavaMailの拡張版を使います。
https://code.google.com/p/java-gmail-imap/
JavaMail 1.4.4を拡張したもので、メッセージ(メール)ごとに以下の情報を取得できます。

  • メッセージID
  • スレッドID
  • ラベルリスト

このライブラリのパッケージは、JavaMail 1.4.4と競合しないように以下のように定義されています。

  • com.google.code.com.sun.mail
  • com.google.code.javax.mail

JavaMailから変更する場合は拡張版のクラスをimportするように修正します。

 

メールのURL取得のサンプル 

以下のサンプルでは、最新のメール10件のメールを取得してタイトルとURLとラベルを出力しています。
Gmailは以下のURLでメールを直接開くことができます。

https://mail.google.com/mail/u/0/?shva=1#inbox/messageId

import java.net.URLEncoder;
import java.util.Properties;

import com.google.code.com.sun.mail.imap.IMAPFolder;
import com.google.code.com.sun.mail.imap.IMAPMessage;
import com.google.code.com.sun.mail.imap.IMAPSSLStore;
import com.google.code.javax.mail.FetchProfile;
import com.google.code.javax.mail.Folder;
import com.google.code.javax.mail.Message;
import com.google.code.javax.mail.Session;

public class GetGmailURL {

 private static String email = "email@gmail.com";
 private static String password = "password";
 private static int msg_num = 10;
 private static String host = "imap.gmail.com";

 public static void main(String[] args) throws MessagingException {
  Properties props = new Properties();
  props.setProperty("mail.store.protocol", "imaps");
  Session session = Session.getDefaultInstance(props, null);
  IMAPSSLStore store = new IMAPSSLStore(session, null);
  Folder inbox = null;
  try {
   store.connect(host, email, password);
   inbox = store.getFolder("Inbox");
   inbox.open(Folder.READ_ONLY);
   int count = inbox.getMessageCount();
   Message messages[] = inbox.getMessages(count-msg_num, count);
   FetchProfile fp = new FetchProfile();
   fp.add(IMAPFolder.FetchProfileItem.X_GM_MSGID);
   fp.add(IMAPFolder.FetchProfileItem.X_GM_THRID);
   fp.add(IMAPFolder.FetchProfileItem.X_GM_LABELS);
   inbox.fetch(messages, fp);

   for(int i=msg_num; i>0; i--){
    System.out.println("Title: " + messages[i].getSubject());
    IMAPMessage im = (IMAPMessage) messages[i];
    String msgId = Long.toHexString(im.getGoogleMessageId());
    String url = "https://mail.google.com/mail/u/0/?shva=1#inbox/" + msgId;
    System.out.println("URL: "url);
    String[] labels = im.getGoogleMessageLabels();
    if(labels!=null){
     for(String label: labels){
      System.out.println("Label: " + label);
     }
    }
    System.out.println();
   }
  } catch (Exception e) {
   e.printStackTrace();
  } finally {
   if(inbox.isOpen()) {
    inbox.close(true);
   }
   if(store.isConnected()){
    store.close();
   }
  }
 }
}

実行結果

Title: Box Buzz: Good-bye Box.net, Hello Box
URL: https://mail.google.com/mail/u/0/?shva=1#inbox/13459016de7c3784
Label: Cloud Service
:
:

ブラウザで上記のURLを開くとGmailのメールの本文が開きました。

アメリカに行ってきました 〜Google編 前半〜

初めまして

宮本と申します。現在はinfoScoop for Google Appsの開発をしています。今年の4月まではinfoScoop Cloud Enterpriseの開発もしていました。

これからinfoScoopの開発者としてちょくちょくブログを書いていこうと思います。宜しくお願いします。

アメリカに行ってきました

さっそく開発にはあまり関係ない話です。

11月6日〜12日の5日間で弊社Beacon ITが企画をしている海外IT動向研究会というイベントに同行させて頂き、アメリカのサンフランシスコに行ってきました。

その企画の中で、Googleさんの本社に行けるというなかなかめったにない機会がありました。

Googleの本社では、現地の営業の方(ローレンさん)にGoogleについての色々なことを紹介してもらいました。中でも印象に残った話を書いていこうと思います。

20% Time

ご存知の方も多いかもしれませんが、Googleの社員には20% Timeという自分の労働時間の20%の時間を使って、メインのプロジェクトとは別のプロジェクトを企画したり、自分のやりたい プロジェクトを探して参加したりする時間を設けなければいけないという規則のようなものがあります。プロジェクトの企画や参加は非公式に行われているそうで、募集なども勝手に掲示板などで募集をしてメンバーを集めたりしているそうです。

ちなみにGmailやGoogle Talkなどサービスはこの20% Timeから生まれたそうです。

うちの会社でもやらないかな〜とか少し思ったりもしたのですが、日本の企業では難しいのかなとも思いました。

Googleではうまくいっているのは、色々な要因があるとは思いますが、Googleの文化で「フラットな組織と、小規模なチームでプロジェクトを進める」というのが関係しているのではないかな〜と思います。それと非公式に行っているので、カッチリしたプロジェクトマネジメントではなく比較的カジュアルな感じでプロジェクトを進めているのも要因の1つかもしれません。

まぁこんな感じでGoogleさんはものすごいスピードで色々なサービスを生み出しているのですね。

ちなみに、80%のメインプロジェクトが火を噴いていたりしても20%の方はやるんですか?という質問をしたら、やっぱり80%を優先にして作業するそうです。ただ、Googleの社員はの大半は情熱を持っているので、120%で稼働することがほとんどとも言っていました。

結局プロジェクトを掛け持ちしているのとあまり変わらないような気がするのは気のせいですよね。

写真撮ってきました

施設内は基本的に写真を撮ってはダメと言われたのですが、外観などはOKとのことだったので何枚か撮ってきました。

Googleのオフィスの外観です。手前の像は誰だか分かりませがすごい人だと思います。

Googleには恐竜の骨もあります。元々はどっかの企業が使っていた施設をGoogleが買い取ったそうです。

ちょっと手前の柵でわかりずらくなっていますが、ビーチバレーコートです。社員は休憩時間にバレーを嗜みます。視察中もプレーしている人がいました。Google社員のジャンプ力はすごかったです。

今回はここまでにしておきます。

infoScoop開発者ブログ、始めます!

3つのinfoScoopの情報を発信

このたび、infoScoopの開発者ブログを始めることになりました。

infoScoopは現在、infoScoop Cloud EnterpriseinfoScoop OpenSourceinfoScoop for Google Appsという3つのラインナップで展開しています。

この開発者ブログでは種類に問わず、infoScoopに関わるニュース、イベント情報、動向、技術ネタ、様々なことについてinfoScoopの開発者たちが情報を発信していきます。

右サイドバーの一番下の「ブログをフォロー」から、メールでこのブログの更新を購読することもできますよ。

どうぞよろしくお願いします。

Facebookもやってます

infoScoopはFacebookページもあります。そちらでも情報を発信しつつ、イベント参加の募集なども行っていく予定です。ぜひinfoScoopページで「いいね!」ボタンを押して参加してみてください。

infoScoopって何?という方は・・・

infoScoopって何?という方に、簡単にinfoScoopをご紹介しましょう。
ものすごーく簡単な説明は、Aboutに書きましたのでこちらもよろしければご参照ください。

この投稿の続きを読む

フォロー

Get every new post delivered to your Inbox.