2017年9月14日木曜日

Spring BootとSpring Securityでログインを実装する方法

Spring Bootな構成でSpring Securityを用いて一般的なログイン機構を実装する方法をまとめる。ここでは、フロントをjavascriptで、サーバー側はJSONを返すAPIとして実装するような構成を想定する。また、認証情報はRDBMS上のユーザー情報テーブルにて管理する。

Spring Securityの機能全般を有効にする

まずは、WebSecurityConfigurerAdapterを継承したクラスを作成し、@EnableWebSecurityアノテーションを付与してSpring Securityの機能を有効化する。

  1.       
  2. @EnableWebSecurity  
  3. public class MySecurityConfig extends WebSecurityConfigurerAdapter {  
  4.     // 略  
  5. }  

Spring Securityの設定

これらの設定は、WebSecurityConfigurerAdapterのconfigure(HttpSecurity)メソッドをオーバーライドして実装する。ここでのポイントは4つ。

  • CSRF対策を無効化(今回は無関係なので無効化しているけど、別途CSRF対策は設定すべし)
  • ログイン認証を行うパスを設定
  • フォーム認証を有効化
  • POST /loginでログイン処理がトリガーされる(カスタマイズすることも可能)

  1. @EnableWebSecurity  
  2. public class MySecurityConfig extends WebSecurityConfigurerAdapter {  
  3.     /** 
  4.      * {@inheritDoc} 
  5.      */  
  6.     @Override  
  7.     protected void configure(HttpSecurity http) throws Exception {  
  8.         //  
  9.         // CSRF対策を無効化  
  10.         //  
  11.         http.csrf().disable();  
  12.   
  13.         //  
  14.         // ログイン認証を行うパスを設定  
  15.         //  
  16.         http.authorizeRequests()  
  17.             // ログイン無しでアクセス許可するパス  
  18.             .antMatchers("/").permitAll()  
  19.             // その他はログインが必要  
  20.             .anyRequest().authenticated();  
  21.   
  22.         //  
  23.         // フォーム認証を有効化  
  24.         //  
  25.         http.formLogin()  
  26.     }  
  27. }  

ログインが成功/失敗した場合の処理を実装

ログインが成功/失敗した場合に、それぞれJSON形式のレスポンスを返すように実装する。

まずは、ログイン成功のハンドラー(AuthenticationSuccessHandler)とログイン失敗のハンドラー(AuthenticationFailureHandler)を定義する。

  1. /** 
  2.  * ログイン成功時の動作を定義 
  3.  */  
  4. private static final AuthenticationSuccessHandler LOGIN_SUCCESS = (req, res, auth) -> {  
  5.     // HTTP Statusは200  
  6.     res.setStatus(HttpServletResponse.SC_OK);  
  7.   
  8.     // Content-Type: application/json  
  9.     res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);  
  10.   
  11.     // Body  
  12.     res.getWriter().write(JsonUtil.encode(ImmutableMap.of("code""login_success")));  
  13.     res.getWriter().flush();  
  14. };  
  15.   
  16. /** 
  17.  * ログイン失敗時の動作を定義 
  18.  */  
  19. private static final AuthenticationFailureHandler LOGIN_FAILED = (req, res, auth) -> {  
  20.     // HTTP Statusは401  
  21.     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  
  22.   
  23.     // Content-Type: application/json  
  24.     res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);  
  25.   
  26.     // Body  
  27.     res.getWriter().write(JsonUtil.encode(ImmutableMap.of("code""login_failed")));  
  28.     res.getWriter().flush();  
  29. };  

ここで使っているImmutableMapは、Guavaライブラリのクラス。JsonUtilはObjectMapperを利用するための自作ユーティリティクラスとする。ObjectMapperは、別途DIコンテナに登録しておいたものを使うのが良さそうだが、ここでは主題ではないため適当に下記のように実装しておく。

  1. public class JsonUtil {  
  2.     public static String encode(Object src) {  
  3.         try {  
  4.             return new ObjectMapper().writeValueAsString(src);  
  5.         } catch (Exception e) {  
  6.             throw new IllegalArgumentException(e);  
  7.         }  
  8.     }  
  9. }  

最後に、前項で設定したフォーム認証の有効化の箇所へ、ハンドラーの紐付け設定を追記する。

  1. @EnableWebSecurity  
  2. public class MySecurityConfig extends WebSecurityConfigurerAdapter {  
  3.     /** 
  4.      * {@inheritDoc} 
  5.      */  
  6.     @Override  
  7.     protected void configure(HttpSecurity http) throws Exception {  
  8.   
  9.         // 中略  
  10.   
  11.         //  
  12.         // フォーム認証を有効化  
  13.         //  
  14.         http.formLogin()  
  15.             //  
  16.             // ログイン成功のハンドラーを設定  
  17.             //  
  18.             .successHandler(LOGIN_SUCCESS)  
  19.             //  
  20.             // ログイン失敗のハンドラーを設定  
  21.             //  
  22.             .failureHandler(LOGIN_FAILED);  
  23.     }  
  24. }  

未ログインアクセスの制御

このままだと、ログインが必要なURL(例えば/test)にアクセスすると、/loginへのリダイレクトがレスポンスされる。Ajaxな通信を行う前提なので、ログインが必要な旨を示すJSONを返すように実装する。

まずは、認証エントリーポイントのハンドラー(AuthenticationEntryPoint)を定義する。

  1. /** 
  2.  * 認証エントリポイントの動作を定義 
  3.  */  
  4. private static final AuthenticationEntryPoint LOGIN_REQUIRED = (req, res, auth) -> {  
  5.     // HTTP Statusは401  
  6.     res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  
  7.       
  8.     // Content-Type: application/json  
  9.     res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);  
  10.   
  11.     // Body  
  12.     res.getWriter().write(JsonUtil.encode(ImmutableMap.of("code""login_required")));  
  13.     res.getWriter().flush();  
  14. };  

次に、前項と同様にconfigure()メソッドの中で、ハンドラーの紐付け設定を追記する。

  1. @EnableWebSecurity  
  2. public class MySecurityConfig extends WebSecurityConfigurerAdapter {  
  3.     /** 
  4.      * {@inheritDoc} 
  5.      */  
  6.     @Override  
  7.     protected void configure(HttpSecurity http) throws Exception {  
  8.   
  9.         // 中略  
  10.   
  11.         http.exceptionHandling()  
  12.             //  
  13.             // 要ログインページアクセスのハンドラーを設定  
  14.             //  
  15.             .authenticationEntryPoint(LOGIN_REQUIRED);  
  16.     }  
  17. }  

ユーザー情報を取得するサービスを定義

最後のステップとして、データベースからユーザー情報を取得して認証する部分を実装する。

まずは、データベースからユーザー情報を取得するサービスをUserDetailsServiceインターフェースの実装クラスとして定義する。

※実際には、JdbcTemplateではなくMyBatisやらDBFluteやらのお好きなO/Rマッパーを利用すると思う。

  1. /** 
  2.  * ユーザー情報を取得するサービス 
  3.  */  
  4. @Service  
  5. public class MyUserDetailsService implements UserDetailsService {  
  6.     private final JdbcTemplate jdbcTemplate;  
  7.   
  8.     private static final String SQL  
  9.         = "select password from user where name = ?";  
  10.   
  11.     private static final SimpleGrantedAuthority ROLE  
  12.         = new SimpleGrantedAuthority("ROLE_USER");  
  13.       
  14.     /** 
  15.      * Constructor 
  16.      */  
  17.     public MyUserDetailsService(final JdbcTemplate jdbcTemplate) {  
  18.         this.jdbcTemplate = jdbcTemplate;  
  19.     }  
  20.   
  21.     /** 
  22.      * {@inheritDoc} 
  23.      */  
  24.     @Override  
  25.     public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
  26.         if (StringUtils.isEmpty(username)) {  
  27.             throw new UsernameNotFoundException("No username");  
  28.         }  
  29.   
  30.         // データベースから該当ユーザー情報を取得  
  31.         final String password = jdbcTemplate.queryForObject(  
  32.                 SQL, new Object[]{username}, String.class);  
  33.           
  34.         if (StringUtils.isEmpty(password)) {  
  35.             throw new UsernameNotFoundException("No user");  
  36.         }  
  37.   
  38.         // ユーザー情報を生成  
  39.         return new User(  
  40.                 username,  
  41.                 password,  
  42.                 Collections.singleton(ROLE));  
  43.     }  
  44. }  

続いて、AuthenticationManagerBuilderへ、ユーザー情報取得サービスの紐付け設定を行う。

  1. @EnableWebSecurity  
  2. public class MySecurityConfig extends WebSecurityConfigurerAdapter {  
  3.     private final MyUserDetailsService service;  
  4.   
  5.     /** 
  6.      * Constructor 
  7.      */  
  8.     public MySecurityConfig(final MyUserDetailsService service) {  
  9.         this.service = service;  
  10.     }  
  11.   
  12.     // 中略  
  13.   
  14.     /** 
  15.      * {@inheritDoc} 
  16.      */  
  17.     @Autowired  
  18.     void configureAuthenticationManager(AuthenticationManagerBuilder auth) throws Exception {  
  19.         //  
  20.         // ユーザー情報取得サービスを紐付ける  
  21.         //  
  22.         auth.userDetailsService(service);  
  23.     }  
  24. }  

GET /testの動作

要ログインURLへのアクセスは、ログインが必要な旨を示すJSONレスポンスが帰ってくる。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Date: Thu, 14 Sep 2017 14:24:14 GMT
Expires: 0
Pragma: no-cache
Set-Cookie: JSESSIONID=8A504C961F44779D571088EBFE5E7BA3; Path=/; HttpOnly
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Xss-Protection: 1; mode=block

{code: "login_required"}

POST /loginの動作(ユーザー名やパスワードが正しくない)

所謂、ログインが失敗するパターン。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Date: Thu, 14 Sep 2017 14:29:08 GMT
Expires: 0
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Xss-Protection: 1; mode=block

{code: "login_failed"}

POST /loginの動作(ユーザー名とパスワードが正しい)

ログインが成功するパターン。さらっと流したが、ログイン処理のデフォルト実装は、application/x-www-form-urlencodedなリクエストで、ユーザー名をusername、パスワードをpasswordという名前のパラメータとして送信する必要がある。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
Date: Thu, 14 Sep 2017 14:35:07 GMT
Expires: 0
Pragma: no-cache
Set-Cookie: JSESSIONID=DA1076E9F2247B35BF5718CC75B9979B; Path=/; HttpOnly
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Xss-Protection: 1; mode=block

{code: "login_success"}

2015年2月19日木曜日

Androidホームアプリ風のドラッグアンドドロップを実装する方法

目指すもの

グリッド表示されたアプリ一覧から、アイコン長押しでドラッグ&ドロップし、任意の場所へアプリを移動するUI

動画をとってみた


アプリの表示

まずは、アプリアイコンの表示を実装する。アプリアイコンは、TextViewへsetCompoundDrawablesWithIntrinsicBounds()メソッドでアイコン画像を指定することで実現する。

layout/appicon.xml

  1.       
  2. <TextView xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:layout_width="wrap_content"  
  4.     android:layout_height="wrap_content"  
  5.     android:paddingTop="4dp"  
  6.     android:textSize="12dp"  
  7.     android:singleLine="true"  
  8.     android:ellipsize="end"  
  9.     android:textColor="@android:color/white"  
  10.     android:gravity="center_horizontal|center_vertical" />  

Javaコード

アプリアイコンを作るところ

  1. // アイコン画像をロード(とりあえず電話っぽいアイコンにしとく)  
  2. Drawable icon = getResources().getDrawable(android.R.drawable.ic_menu_call);  
  3.   
  4. // TextViewをXMLから生成  
  5. TextView appIcon  
  6.     = (TextView) getLayoutInflater().inflate(R.layout.appicon, null);  
  7. // アプリ名称を設定  
  8. appIcon.setText("でんわ");  
  9. // アイコンを設定(left, top, right, bottomの順で指定)  
  10. appIcon.setCompoundDrawables(null, icon, nullnull);  

アプリアイコンをコンテナ(ここではRelativeLayout)へ追加する

  1. // コンテナへ追加  
  2. RelativeLayout c = (RelativeLayout) findViewById(R.id.container);  
  3. c.addView(appIcon);  

Viewをドラッグ可能にする

次に、アプリアイコンをドラッグできるようにする。TextViewのstartDrag()メソッドを呼び出せば良く、長押しでドラッグ開始とするため、OnLongClickListenerを実装してアプリアイコンへセットする。

特筆すべき点として、ドロップ先へデータを渡すためstartDrag()の第3引数にViewそのものをセットしておく。

  1. /** 
  2.  * Viewの長押しイベントをハンドリングするリスナークラス 
  3.  */  
  4. private static class MyLongClickListener implements View.OnLongClickListener  
  5. {  
  6.     /** 
  7.      * Viewが長押しされた際にシステムから呼び出される。 
  8.      */  
  9.     @Override  
  10.     public boolean onLongClick(View v)  
  11.     {  
  12.         // ドラッグ&ドロップで受け渡しするデータ(使わないのでダミー)  
  13.         ClipData tmpData = ClipData.newPlainText("dummy""dummy");  
  14.         // ドラッグ中に表示するイメージのビルダー  
  15.         View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);  
  16.   
  17.         // ドラッグを開始  
  18.         v.startDrag(tmpData, shadow, v, 0);  
  19.           
  20.         return true;  
  21.     }  
  22. }  

アプリアイコンへリスナーをセットする

  1. MyLongClickListener listener = new MyLongClickListener();  
  2. appIcon.setOnLongClickListener(listener);  

以上でドラッグできるようになっている、が、実行しても見た感じだと出来てるのか分からない。次項の「ドラッグ中に表示するイメージの制御」が必要。

ドラッグ中に表示するイメージの制御

基本的に、ドラッグ中に表示するイメージはDragShadowBuilderで制御しており、デフォルト実装(View#draw(Canvas)を呼び出す)で問題ない。

しかし、TextViewをドラッグ対象とした場合には意図した動作とならない。DragShadowBuilderクラスを継承して、TextViewのキャプチャを表示するようにカスタマイズする。

Javaコード

  1. /** 
  2.  * ドラッグ&ドロップ中に表示するイメージを制御するクラス 
  3.  */  
  4. private static class MyDragShadowBuilder extends DragShadowBuilder  
  5. {  
  6.     /** 
  7.      * コンストラクタ 
  8.      */  
  9.     public MyDragShadowBuilder(View v) {  
  10.         super(v);  
  11.     }  
  12.       
  13.     /** 
  14.      * ドラッグ中のイメージを描画する際にシステムが呼び出すメソッド 
  15.      */  
  16.     @Override  
  17.     public void onDrawShadow(Canvas canvas)  
  18.     {  
  19.         // ドラッグ対象View  
  20.         View view = getView();  
  21.   
  22.         // Viewのキャプチャを取得する準備  
  23.         view.setDrawingCacheEnabled(true);  
  24.         view.destroyDrawingCache();  
  25.   
  26.         // キャプチャを取得し、キャンバスへ描画する  
  27.         Bitmap bitmap = view.getDrawingCache();  
  28.         canvas.drawBitmap(bitmap, 0f, 0f, null);  
  29.     }  
  30. }  

前述のOnLongClickListenerで実装していたイメージビルダーを作成したイメージビルダーへ置き換える。

  1. ...前略...  
  2. // これを  
  3. View.DragShadowBuilder shadow = new View.DragShadowBuilder(v);  
  4. ↓  
  5. // こうする  
  6. View.DragShadowBuilder shadow = new MyDragShadowBuilder(v);  
  7. ...後略...  

ドロップ先を用意する

最後に、ドロップ先としてViewGroup(LinearLayoutとか)を用意しておく。ViewGroupには、ドラッグイベントをハンドリングするためのOnDragListenerをセットすればOK。

OnDragListenerの実装は以下の通り

  • ACTION_DRAG_STARTEDを受けたらtrueを返す
  • ACTION_DROPを受けたらアプリアイコンを作ってViewGroupに追加する

Javaコード

  1. /** 
  2.  * ドラッグイベントをハンドリングするリスナークラス 
  3.  */  
  4. private static class MyDragListener implements View.OnDragListener  
  5. {  
  6.     private LayoutInflater mInflater;  
  7.   
  8.     /** 
  9.      * コンストラクタ 
  10.      */  
  11.     public MyDragListener(Context ctx)  
  12.     {  
  13.         mInflater = LayoutInflater.from(ctx);  
  14.     }  
  15.   
  16.     /** 
  17.      * ドラッグイベントが発生した際に、システムから呼び出されるメソッド 
  18.      */  
  19.     @Override  
  20.     public boolean onDrag(View v, DragEvent event)  
  21.     {  
  22.         switch (event.getAction())  
  23.         {  
  24.             case DragEvent.ACTION_DRAG_STARTED:  
  25.             case DragEvent.ACTION_DRAG_ENTERED:  
  26.             case DragEvent.ACTION_DRAG_LOCATION:  
  27.             case DragEvent.ACTION_DRAG_EXITED:  
  28.                 return true;  
  29.   
  30.             case DragEvent.ACTION_DROP:  
  31.                 //  
  32.                 // ドラッグ元の情報を取得  
  33.                 //  
  34.                 // startDrag()の第3引数で渡したデータを取得  
  35.                 TextView src = (TextView) event.getLocalState();  
  36.                 // アプリ名  
  37.                 CharSequence name = src.getText();  
  38.                 // 画像  
  39.                 Drawable[] imgs = src.getCompoundDrawables();  
  40.   
  41.                 //  
  42.                 // 新しくドロップ先に設置するViewを生成  
  43.                 //  
  44.                 // アプリアイコンを新規作成(TextViewをXMLから生成)  
  45.                 TextView appIcon  
  46.                     = (TextView) mInflater.inflate(R.layout.appicon, null);  
  47.                 // アプリ名称を設定  
  48.                 appIcon.setText(name);  
  49.                 // 画像を設定  
  50.                 appIcon.setCompoundDrawables(  
  51.                     imgs[0], imgs[1], imgs[2], imgs[3]);  
  52.   
  53.                 // ViewGroupへ追加  
  54.                 ViewGroup c = (ViewGroup) v;  
  55.                 c.addView(appIcon);  
  56.   
  57.                 return true;  
  58.   
  59.             default:  
  60.                 break;  
  61.         }  
  62.   
  63.         return false;  
  64.     }  
  65. }  

ドロップ先(ここではLinearLayout)へOnDragListenerをセットする

  1. MyDragListener listener = new MyDragListener(this);  
  2. LinearLayout dropPlace = (LinearLayout) findViewById(R.id.drop_place);  
  3. dropPlace.setOnDragListener(mDragListener);  

ドロップ先の背景

ドラッグしている際に、ドロップ先の背景が変わるようにしておく。以下のようにXMLで背景を作成し、ドロップ先に設定してあげるだけでOK

drawable/drop_place_background.xml

  1. <selector xmlns:android="http://schemas.android.com/apk/res/android">  
  2.   
  3.     <item android:state_drag_hovered="true">  
  4.         <shape>  
  5.             <solid android:color="#0affffff"></solid>  
  6.             <stroke  
  7.                 android:color="#ffd700"  
  8.                 android:dashgap="10dp"  
  9.                 android:dashwidth="15dp"  
  10.                 android:width="3dp">  
  11.         </stroke></shape>  
  12.     </item>  
  13.       
  14.     <item>  
  15.         <shape>  
  16.             <solid android:color="#0affffff"></solid>  
  17.         </shape>  
  18.     </item>  
  19.       
  20. </selector>  

おまけ:アプリ一覧の取得

アプリ一覧を取得するコード例は以下の通り。

  1. PackageManager manager = ctx.getPackageManager();  
  2.   
  3. // アプリ一覧を取得する条件  
  4. Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);  
  5. mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);  
  6.   
  7. // アプリ一覧を取得  
  8. final List<ResolveInfo> apps = manager.queryIntentActivities(mainIntent, 0);  
  9. Collections.sort(apps, new ResolveInfo.DisplayNameComparator(manager));  
  10.   
  11. // アプリ情報一覧を作成  
  12. for (ResolveInfo each : apps)  
  13. {  
  14.     // アプリ名を取得  
  15.     CharSequence label = each.loadLable(manager);  
  16.     // アイコンを取得  
  17.     Drawable icon = each.activityInfo.loadIcon(manager);  
  18.       
  19.     ...略  
  20. }  

2015年1月26日月曜日

Galaxy NexusにAndroid 5 Lollipopをインストールする

Android 5 Lollipopに対応したアプリケーションの開発やテストを実機で行うために、Galaxy NexusにカスタムROMを突っ込んでみた。

Android 5搭載端末と言えば、Google - Nexus 6かLG - G3かエミュレータか…どれも若干高いし、やっぱり実機が欲しい。

ちなみにGalaxy Nexusは¥15,000〜¥17,000くらいで手に入る!

やること


前提として、作業するPCにAndroid SDKをインストールしておくこと。
adbコマンドとfastbookコマンドを利用するので、$SDK_HOME/platform-toolsにパスを通しておくと便利。
  • ブートローダーのUnlock
  • TWRPの導入
  • ROOT化
  • 全パーティションのバックアップ
  • ROMのバックアップ
  • カスタムROMの導入

とりあえず


PCと端末をUSBケーブルで接続し、adbコマンドが実行出来る状態にしておく。

ブートローダーのUnlock


※この作業を行うと端末のデータが初期化されるため、必要に応じて先にバックアップを取得する必要がある。

まず、PCのターミナルで下記のコマンドを実行する。もしくは、電源ボタンとボリューム+/-ボタンの両方を押しっぱなしにして電源を入れる。
  1. ./adb reboot bootloader  
上記の画面が表示されたら、ターミナルで下記のコマンドを実行し、Unlockする。
  1. ./fastboot oem unlock  
確認画面が表示されるので、ボリューム+ボタンで選択し、電源ボタンで決定する。
うまくいけば下図のように、LOCK STATE - UNLOCKEDとなる。

TWRPの導入


TWRP(Team Win Recovery Project)からTWRP for Galaxy Nexus GSM [maguro]を辿って、imgファイルをダウンロードしておく。ここでは「openrecovery-twrp-2.8.4.0-maguro.img」を利用。

ブートローダーを起動した状態で、ターミナルから下記のコマンドでTWRPを起動する。
  1. ./fastboot boot ~/Downloads/openrecovery-twrp-2.8.4.0-maguro.img  
起動画面はこんな感じ

ROOT化


TWRPのRebootメニューからSystemを選択すると自動的に端末が再起動して、SuperSuのインストールを促されるので指示に従えばOK。

全パーティションのバックアップ


念のため、全パーティションのバックアップ(userdata除く)を行っておく。まずは、ターミナルで下記コマンドを実行し、パーティション一覧を取得する。
  1. ./adb shell  
  2. cd /dev/block/platform/omap/omap_hsmmc.0/by-name  
  3. ls -l  
こんな感じで表示される。
lrwxrwxrwx root     root              2015-01-23 03:43 boot -> /dev/block/mmcblk0p7
lrwxrwxrwx root     root              2015-01-23 03:43 cache -> /dev/block/mmcblk0p11
lrwxrwxrwx root     root              2015-01-23 03:43 dgs -> /dev/block/mmcblk0p6
lrwxrwxrwx root     root              2015-01-23 03:43 efs -> /dev/block/mmcblk0p3
lrwxrwxrwx root     root              2015-01-23 03:43 metadata -> /dev/block/mmcblk0p13
lrwxrwxrwx root     root              2015-01-23 03:43 misc -> /dev/block/mmcblk0p5
lrwxrwxrwx root     root              2015-01-23 03:43 param -> /dev/block/mmcblk0p4
lrwxrwxrwx root     root              2015-01-23 03:43 radio -> /dev/block/mmcblk0p9
lrwxrwxrwx root     root              2015-01-23 03:43 recovery -> /dev/block/mmcblk0p8
lrwxrwxrwx root     root              2015-01-23 03:43 sbl -> /dev/block/mmcblk0p2
lrwxrwxrwx root     root              2015-01-23 03:43 system -> /dev/block/mmcblk0p10
lrwxrwxrwx root     root              2015-01-23 03:43 userdata -> /dev/block/mmcblk0p12
lrwxrwxrwx root     root              2015-01-23 03:43 xloader -> /dev/block/mmcblk0p1
さらに、下記コマンドを1つずつ実行して、バックアップを行う。※mkdirで作成しているディレクトリ名を変更する場合や、上で取得したパーティション一覧に相違がある場合は、適宜コマンドを変更すること。
  1. cd /sdcard  
  2. mkdir partbk20150123  
  3.   
  4. dd if=/dev/block/mmcblk0p7 of=/sdcard/partbk20150123/mmcblk0p7_boot bs=4096  
  5. dd if=/dev/block/mmcblk0p11 of=/sdcard/partbk20150123/mmcblk0p11_cache bs=4096  
  6. dd if=/dev/block/mmcblk0p6 of=/sdcard/partbk20150123/mmcblk0p6_dgs bs=4096  
  7. dd if=/dev/block/mmcblk0p3 of=/sdcard/partbk20150123/mmcblk0p3_efs bs=4096  
  8. dd if=/dev/block/mmcblk0p13 of=/sdcard/partbk20150123/mmcblk0p13_metadata bs=4096  
  9. dd if=/dev/block/mmcblk0p5 of=/sdcard/partbk20150123/mmcblk0p5_misc bs=4096  
  10. dd if=/dev/block/mmcblk0p4 of=/sdcard/partbk20150123/mmcblk0p4_param bs=4096  
  11. dd if=/dev/block/mmcblk0p9 of=/sdcard/partbk20150123/mmcblk0p9_radio bs=4096  
  12. dd if=/dev/block/mmcblk0p8 of=/sdcard/partbk20150123/mmcblk0p8_recovery bs=4096  
  13. dd if=/dev/block/mmcblk0p2 of=/sdcard/partbk20150123/mmcblk0p2_sbl bs=4096  
  14. dd if=/dev/block/mmcblk0p10 of=/sdcard/partbk20150123/mmcblk0p10_system bs=4096  
  15. dd if=/dev/block/mmcblk0p1 of=/sdcard/partbk20150123/mmcblk0p1_xloader bs=4096  
大体20分くらいはかかるので気長に作業を行う。

ROMのバックアップ


TWRPを起動してメニューのBakcupを選択する。対象項目(System、Data、Boot)をチェックして「Swipe to Back Up」をスワイプする。10分くらいでバックアップが完了し、Backup Completeが表示される。

画面に表示されたパス(/data/media/0/TWRP/BACKUPS/01498B2D17012007/2015-01-22--21-20-46 JDQ39.SC04DOMMD4/といった場所)にバックアップが作成される。

カスタムROMの導入


今回は、FML(Fork My Life)のAndroid Lollipop版を焼くので、下記2つのファイルをダウンロードしておく。

※ファイルについての詳細は、[ROM][5.0.2/LRX22G][AOSP][LINARO/OPTIMIZED] FML: Fork My Life (2015/01/08) - xdadevelopersを参照されたし

ダウンロードしたファイルは、ターミナルから下記のようにして端末へ転送しておく。
  1. ./adb shell  
  2. cd /sdcard/  
  3. mkdir tmp  
  4. exit  
  5.   
  6. ./adb push ~/Downloads/FML-AOSP-5.0-20150108-maguro.zip /sdcard/tmp/  
  7. ./adb push ~/Downloads/pa_gapps-modular-micro-5.0.1-BETA12-20150116-signed.zip /sdcard/tmp/  
次に、現在のROMを消す。TWRPを起動し、Wipe->Advanced Wipeを辿って以下を選択し「Swipe to Back Up」をスワイプする。
  • /data
  • /system
  • /cache
  • dalvik cache
最後に、TWRPのメニューからInstallを選択し、/sdcard/tmpのFML-AOSP-5.0-2015018-maguro.zipを選択する。

Installが完了したら、TWRPのメニューに戻ってInstallを選択し、/sdcard/tmpのpa_gapps-modular-micro-5.0.1-BETA12-20150116-signed.zipを選択する。

完了後にSystem Rebootする。最初の1回は、アニメーション表示のまま固まって困ったけど、電池を抜いて再度電源を入れたら問題なく起動した。

2013年9月6日金曜日

DialogFragmentでのカスタムダイアログ実装方法

DialogFragmentによるカスタムダイアログ実装方法について、下記の点をまとめておく。お題としてパズドラ風のダイアログを実装してみる。
  • 基本
  • コンテンツ部分
  • スタイル
完成形

基本

DialogFragmentを継承したpublicクラスを作成する。注意点は下記の通り。

  • ファクトリーメソッド(下記例ではnewInstance())を用意する。
  • コンストラクタのオーバーロードを作らない、使わない
  1. public class MyDialogFragment extends DialogFragment  
  2. {  
  3.   /** 
  4.    * ファクトリーメソッド 
  5.    */  
  6.   public static MyDialogFragment newInstance(String param)  
  7.   {  
  8.     MyDialogFragment instance = new MyDialogFragment();  
  9.   
  10.     // ダイアログに渡すパラメータはBundleにまとめる  
  11.     Bundle arguments = new Bundle();  
  12.     arguments.putString("parameter", param);  
  13.   
  14.     instance.setArguments(arguments);  
  15.     return instance;  
  16.   }  
  17. }  

コンテンツ部分(お手軽パターン)

コンテンツ部分の実装は、onCreateDialog()やonCreateView()をオーバーライドして行う。 お手軽パターンでは、onCreateDialog()のみをオーバーライドして、必要な機能を有するDialogインスタンスを生成する。

  1. /** 
  2.  * ダイアログコンテナを生成する。 
  3.  */  
  4. @Override  
  5. public Dialog onCreateDialog(Bundle b)  
  6. {  
  7.   // ダイアログのコンテンツ部分  
  8.   LayoutInflater i  
  9.     = (LayoutInflater) getActivity()  
  10.         .getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
  11.   View content = i.inflate(R.layout.mydialog_content, null);  
  12.   
  13.   AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());  
  14.   
  15.   // タイトル  
  16.   builder.setTitle("My Custom Dialog");  
  17.   // コンテンツ  
  18.   builder.setView(content);  
  19.   // OK  
  20.   builder.setPositiveButton(android.R.string.ok, null);  
  21.   
  22.   Dialog dialog = builder.create();  
  23.   
  24.   // ダイアログ外タップで消えないように設定  
  25.   dialog.setCanceledOnTouchOutside(false);  
  26.   
  27.   return dialog;  
  28. }  
  • 「お手軽」ではあるが、埋め込み部品として再利用(レイアウトの一部として配置)することができないデメリットがある。
  • HoneyComb以降は、ダイアログ外タップで閉じるのがデフォルトなので注意。

コンテンツ部分(お上品パターン)

お上品パターンでは、onCreateDialog()でダイアログコンテナを生成し、onCreateView()でコンテンツを生成する。OKボタンなどはレイアウトで、ダイアログの見栄えは後述のスタイルで賄う。

  1. /** 
  2.  * ダイアログコンテナを生成する。 
  3.  */  
  4. @Override  
  5. public Dialog onCreateDialog(Bundle b)  
  6. {  
  7.   Dialog dialog = super.onCreateDialog(b);  
  8.   
  9.   // タイトル  
  10.   dialog.setTitle("My Custom Dialog");  
  11.   // ダイアログ外タップで消えないように設定  
  12.   dialog.setCanceledOnTouchOutside(false);  
  13.   
  14.   return dialog;  
  15. }  
  16.   
  17. /** 
  18.  * UIを生成する。 
  19.  */  
  20. @Override  
  21. public View onCreateView(LayoutInflater i, ViewGroup c, Bundle b)  
  22. {  
  23.   View content = i.inflate(R.layout.mydialog_content, null);  
  24.   return content;  
  25. }  
  • onCreateDialog()をオーバーライドしつつ、onCreateView()でnull以外を返すとAndroidRuntimeExceptionが発生する…と読めるような情報もあるが、AlertDialog使用方法の問題でありDialogFragment固有の問題では無い (AlertDialogを使わなければ良い)

コンテンツのレイアウト

ここでは、前述のお上品パターンで利用するレイアウト(ボタンも含める)を想定し、下図のようなレイアウトを作成する。RelativeLayoutを利用することで、ダイアログをお好みの位置に表示する。(下記例では画面下部に配置)

このレイアウトのコードは以下の通り。
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout  
  3.   xmlns:android="http://schemas.android.com/apk/res/android"  
  4.   android:layout_width="match_parent"  
  5.   android:layout_height="match_parent" >  
  6.   
  7.   <LinearLayout  
  8.     android:layout_width="match_parent"  
  9.     android:layout_height="wrap_content"  
  10.     android:layout_alignParentBottom="true"  
  11.     android:layout_margin="10dp"  
  12.     android:background="@drawable/dialog_bg"  
  13.     android:gravity="center_horizontal"  
  14.     android:orientation="vertical"  
  15.     android:paddingBottom="10dp"  
  16.     android:paddingLeft="10dp"  
  17.     android:paddingRight="10dp"  
  18.     android:paddingTop="20dp" >  
  19.   
  20.     <TextView  
  21.         android:layout_width="match_parent"  
  22.         android:layout_height="match_parent"  
  23.         android:layout_margin="10dp"  
  24.         android:background="#d595c2d7"  
  25.         android:minLines="7"  
  26.         android:text="以上の内容で送信してよろしいですか?"  
  27.         android:textColor="@android:color/black" />  
  28.   
  29.     <Button  
  30.         android:id="@android:id/button1"  
  31.         android:layout_width="wrap_content"  
  32.         android:layout_height="35sp"  
  33.         android:background="@drawable/button_bg"  
  34.         android:minWidth="100dp"  
  35.         android:text="@android:string/ok"  
  36.         android:textColor="@android:color/white"  
  37.         android:textSize="22sp"  
  38.         android:textStyle="bold" />  
  39.     </LinearLayout>  
  40. </RelativeLayout>  
さらに、ダイアログ背景をXMLで作成する。
drawable/dialog_bg_part1.xml(白枠だけの画像)
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <shape xmlns:android="http://schemas.android.com/apk/res/android" >  
  3.   <corners android:radius="10dp" />  
  4.   <stroke  
  5.     android:width="2dp"  
  6.     android:color="#ffffff" />  
  7. </shape>  
drawable/dialog_bg_part2(黒枠にグラデーション塗りつぶしの画像)
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <shape xmlns:android="http://schemas.android.com/apk/res/android" >  
  3.   <corners android:radius="10dp" />  
  4.   <gradient  
  5.     android:angle="270"  
  6.     android:endColor="#f1114461"  
  7.     android:startColor="#f13c91ba" />  
  8.   <stroke  
  9.     android:width="1dp"  
  10.     android:color="#000000" />  
  11. </shape>  
drawable/dialog_bg.xml(合体)
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <layer-list xmlns:android="http://schemas.android.com/apk/res/android" >  
  3.   <item android:drawable="@drawable/dialog_bg_part1"/>  
  4.   <item  
  5.     android:bottom="2dp"  
  6.     android:drawable="@drawable/dialog_bg_part2"  
  7.     android:left="2dp"  
  8.     android:right="2dp"  
  9.     android:top="2dp"/>  
  10. </layer-list>  
ボタンの背景画像も同様に作成する
drawable/button_bg.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <shape xmlns:android="http://schemas.android.com/apk/res/android" >  
  3.   <corners android:radius="10dp" />  
  4.   <gradient  
  5.     android:angle="270"  
  6.     android:endColor="#222656"  
  7.     android:startColor="#6a75c7" />  
  8.   <stroke  
  9.     android:width="1px"  
  10.     android:color="#0c2446" />  
  11. </shape>  

スタイル

最後に、背景(前項キャプチャの黒いところ)やタイトル(前項キャプチャのvalueと書いてあるとこ)を消して、ダイアログの表示アニメーションを制御するためにスタイルを定義および適用する。

values/styles.xml
Androidのダイアログテーマを拡張し、タイトル指定・背景・アニメーションの設定を上書きする。

  1. <!-- タイトル無し、背景透明、アニメーション指定 -->  
  2. <style name="Theme.MyDialog" parent="@android:style/Theme.Dialog">  
  3.   <item name="android:windowNoTitle">true</item>  
  4.   <item name="android:windowBackground">@android:color/transparent</item>  
  5.   <item name="android:windowAnimationStyle">@style/Animation.MyDialog</item>  
  6. </style>  

ウィンドウアニメーションスタイルは下記の通り定義する。表示する時のアニメーション(windowEnterAnimation)と消える時のアニメーション(windowExitAnimation)をそれぞれ指定。

  1. <style name="Animation.MyDialog" parent="android:Animation.Dialog">  
  2.   <item name="android:windowEnterAnimation">@anim/options_panel_enter</item>  
  3.   <item name="android:windowExitAnimation">@anim/options_panel_exit</item>  
  4. </style>  

アニメーション定義

anim/options_panel_enter.xml

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <set xmlns:android="http://schemas.android.com/apk/res/android"  
  3.   android:interpolator="@android:anim/decelerate_interpolator" >  
  4.   <translate  
  5.     android:duration="@android:integer/config_shortAnimTime"  
  6.     android:fromYDelta="25%"  
  7.     android:toYDelta="0" />  
  8.   <alpha  
  9.     android:duration="@android:integer/config_shortAnimTime"  
  10.     android:fromAlpha="0.0"  
  11.     android:toAlpha="1.0" />  
  12. </set>  

anim/options_panel_exit.xml

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <set xmlns:android="http://schemas.android.com/apk/res/android"  
  3.   android:interpolator="@android:anim/accelerate_interpolator" >  
  4.   <translate  
  5.     android:duration="@android:integer/config_shortAnimTime"  
  6.     android:fromYDelta="0"  
  7.     android:toYDelta="50%" />  
  8.   <alpha  
  9.     android:duration="@android:integer/config_shortAnimTime"  
  10.     android:fromAlpha="1.0"  
  11.     android:toAlpha="0.0" />  
  12. </set>  

DialogFragmentのonCreate()をオーバーライドして、スタイルを適用するコードを記述する。

  1. /** 
  2.  * フラグメント生成コールバックメソッド 
  3.  */  
  4. @Override  
  5. public void onCreate(Bundle b)  
  6. {  
  7.   super.onCreate(b);  
  8.   
  9.   setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_MyDialog);  
  10. }  

おしまい


参考サイト

2012年10月15日月曜日

AndroidでParse(Baas)を使ってPush通知する

巷で流行のBaaS(Backend as as Service)に、Parseというのがある。
色々なことができるけど、現状では「各種APIとデータベースを兼ね備えたサーバーアプリケーション」という風に認識され、使われていることが多いみたい。

今回はそんなParseの機能の一つであるNotification(Push 通知)を使って、サーバーサイドの開発無しにPush通知を実装してみる。

ちなみに100万リクエスト/月、1GBストレージまでは無料で使える。

ユーザー登録


www.parse.comへアクセスし、Try it freeボタンからサクっとユーザー登録を行う。

Quick Start

登録が済んだらログインする。自動生成とかスケルトンとかに抵抗がなければそのままQuick Start Guideへ進むと下記のページになる。



Choose your platformでAndroidを選択し、未作成であればCreate an Appあたりからアプリケーション作成を行う。
さらにBlank Android project with Parse SDK(.zip)というリンクから空のAndroidプロジェクトファイルをダウンロードする。Eclipseでダウンロードしたzipファイルを既存のプロジェクトとしてインポートする。

Quick Startの中身

自動生成とかスケルトンとかに抵抗がある場合、何を自動生成しているのか不安でいっぱいになるので、何が起きているのかを簡単にまとめておく。

  1. 空のAndroidプロジェクトを作成
  2. SDK(zipファイル)をlibsディレクトリへ設置
  3. プロジェクトのライブラリエクスポート設定に上記SDKを追加
  4. Applicationクラスを継承した新規クラスを作成し、onCreateメソッドをオーバーライド。さらにParse.initialize()メソッドの呼び出しを追加
  5. AndroidManifest.xmlのapplicationタグにandroid:name="作成したAppicationクラス名"を追記

といったことをやったのと同じ。


AndroidManifest.xmlの設定

AndroidManifest.xmlにサービスとブロードキャストレシーバの設定を追加する。

  1. <!-- サービスを登録 -->  
  2. <service android:name="com.parse.PushService" />  
  3.   
  4. <!-- ブロードキャストレシーバを登録 -->  
  5. <receiver android:name="com.parse.ParseBroadcastReceiver">  
  6.   <intent-filter>  
  7.     <action android:name="android.intent.action.BOOT_COMPLETED" />  
  8.     <action android:name="android.intent.action.USER_PRESENT" />  
  9.   </intent-filter>  
  10. </receiver>  
パーミッションの利用設定を追加する。 ※必要最低限のみなので公式ドキュメントのチュートリアルとは少し異なる ※公式ドキュメントには書いていなかったけど、ACCESS_NETWORK_STATEも必要だった
  1. <uses-permission  
  2.   android:name="android.permission.INTERNET" />  
  3. <uses-permission  
  4.   android:name="android.permission.ACCESS_NETWORK_STATE"/>  

参考 : Push Notifications - Parse.com



Application IDとClient Keyの設定

Parseサイトにログインした状態で、アプリケーションのOverviewページを開くと、Application IDとClient Keyが表示されるのでこれをメモしておく。

ParseApplicationクラスのonCreateメソッドに記載されている、Parse.initializeの引数を上記の内容に書き換える。

Push通知を受け取るためのコードの追加

前項同様ParseApplicationクラスのonCreateメソッドに下記のコードを追加する。

  1. PushService.subscribe(this"", ParseStarterProjectActivity.class);  

Push通知送信

Parseサイトにログインした状態で、アプリケーションのPush Notificationsページを開くと簡単なPush通知送信管理画面が開くので、適当なメッセージを記入してボタンを押すだけ。


REST APIでPush通知を行う

ParseサイトでのPush通知は、REST APIで以下のリクエストを送信しているっぽい。
送信する内容と受け側のアプリをカスタマイズすれば「Push通知をステータスバーに表示する」というデフォルト動作を変更することができる。

Header

項目
URLhttps://api.parse.com/1/push
MethodPOST
X-Parse-Application-IdアプリケーションID
X-Parse-REST-API-KeyREST APIキー
Content-Typeapplication/json

Body

{
  "channels": [ "" ],
  "type": "android",
  "data": {
    "alert": "This is test message."
  }
}

Push通知で独自の動作を実装する



配信側

REST APIへ送信するJSONデータのalertをactionに変更する。(以下例)

{
  "channels": [ "" ],
  "type": "android",
  "data": {
    "action": "jp.blogspot.tomokey.action.SAMPLE",
    "msg": "This is test message."
  }
}

受信側

ブロードキャストレシーバを作成し、インテントフィルタのactionに上記のjp.blogspot.tomokey.action.SAMPLEを指定する。ブロードキャストレシーバの実装は、以下のようにcom.parse.DataをキーとしてJSONデータを取得すれば、あとはお好みの処理を。

  1. public class MyReceiver extends BroadcastReceiver  
  2. {  
  3.   public void onReceive(Context ctx, Intent intent)  
  4.   {  
  5.     try  
  6.     {  
  7.       // データを取得  
  8.       Bundle extra = intent.getExtras();  
  9.       String data = extra.getString("com.parse.Data");  
  10.         
  11.       // jsonオブジェクトへパース  
  12.       JSONObject json = new JSONObject(data);  
  13.         
  14.       String msg = json.getString("msg");  
  15.       Toast.makeText(ctx, msg, Toast.LENGTH_LONG).show();  
  16.     }  
  17.     catch (JSONException e)  
  18.     {  
  19.       e.printStackTrace();  
  20.     }  
  21. }  

念のためブロードキャストレシーバの定義も。

  1. <receiver android:name="MyReceiver">  
  2.   <intent-filter>  
  3.     <action android:name="jp.blogspot.tomokey.action.SAMPLE"/>  
  4.   </intent-filter>  
  5. </receiver>  

2012年5月5日土曜日

ノートPC(G570)へのUbuntuインストールメモ

LenovoのノートPC(G570)へUbuntuをインストールした際のメモを残しておく。

無線LANを使えるようにする


Ubuntu 12.04の場合

  1. STAドライバをアンインストールする(パッケージマネージャーから下記を検索してアンインストール)
    bcmwl-kernel-source
    
  2. 以下のツールをインストールする(パッケージマネージャーで検索してインストール)
    firmware-b43-installer(これはいらないかも)
    b43-fwcutter
    
  3. blacklist.confを修正
    # sudo vim /etc/modprobe.d/blacklist.conf
    blacklist bcm43xxという行をコメントアウトする
    
  4. マシンを再起動

Ubuntu 11.04の場合

  1. デバイスを確認する
    # lspci -v | grep Broadcom -A 4
    02:00.0 Network controller: Broadcom Corporation BCM4313 802.11b/g/n Wireless LAN Controller (rev 01)
    Subsystem: Broadcom Corporation Device 051b
    Flags: bus master, fast devsel, latency 0, IRQ 17
    Memory at d0400000 (64-bit, non-prefetchable) [size=16K]
    Capabilities: 
    Kernel modules: brcm80211
    
  2. ドライバモジュールを読み込ませる
    # modprobe brcm80211
    
  3. 起動時にドライバモジュールを読み込むように設定する
    # sudo vim /etc/modules
    brcm80211を追記
    

Ubuntu 10.10の場合

  1. デバイスを確認する
    # lspci | grep Broadcom
    02:00.0 Network controller: Broadcom Corporation BCM4313 802.11b/g/n Wireless LAN Controller (rev 01)
    
  2. ここからドライバソースをダウンロードする
  3. ドライバをインストールする
    # tar xvzf hybrid-portsrc_x86_32-v5_100_82_112.tar.gz
    # make && make install
    
  4. ドライバモジュールを読み込ませる
    # depmod
    # modprobe wl
    
  5. 起動時にドライバモジュールを読み込むように設定する
    # sudo vim /etc/modules
    wlを追記
    

インストール後の設定


ユーザディレクトリを英語表記へ変更する

# LANG=C xdg-user-dirs-gtk-update

タッチパッドを無効化する

  1. 確認
    # xinput list | grep TouchPad
    
  2. 無効化(デバイスIDは適宜変更)
    # xinput set-prop 13 "Device Enabled" 0
    

JDK 7のインストール

  1. ダウンロードして展開(ここからダウンロードする。)
  2. コピー
    $ sudo mkdir /usr/lib/jvm
    $ sudo cp -R jdk1.7.0_03 /usr/lib/jvm/
    
  3. update-javaツールインストール
    $ sudo add-apt-repository ppa:nilarimogard/webupd8
    $ sudo apt-get update
    $ sudo apt-get install update-java
    
  4. java実行環境設定
    $ sudo update-java
    

環境

購入したもの

Lenovo G570 4334C3J ¥37,000
SODIMM DDR3-1333(PC3-10600) 4GB x 2枚 ¥3,200

スペック

Size15.6インチ
CPUCore i3 2.2GHz(Dual Core)
MemDDR3-1333(PC-10600) 8GB
HDDSeagate ST9500325AS 500GB
DVDHLDS GT50N DVD±RW
LANAtheros AR8152
WLANBroadcom BCM4313 802.11b/g/n

2012年5月2日水曜日

Visitorパターン

Visitorパターンは、ツリー構造のデータ群(ディレクトリ構造を含めたファイル群みたいな)を処理する際に役立つ設計パターン。別にツリー構造に制限される訳ではないけど、実際それ以外で使わない気がする。

重要な登場人物

Element
データ構造の各要素を表すクラス。ファイルに当たるもの。
ObjectStructure
Elementの集合を扱うクラス。ディレクトリに当たるもの。
Visitor
データ構造の各要素を利用した処理の実装。ElementとObjectStructureに対して行う処理を実装する。


Visitorパターンでは、データ構造の階層の深さを意識することなく、またデータ構造側にデータ処理を一切書くことなく走査を行うことができる。データ走査は、Visitorをroot要素に渡すだけ。あとは勝手にデータ構造を舐め回して各Element(i.e ファイル)へ到達してくれる。


Visitorの実装

各要素に対して処理を行うためのAPIのみを定義した抽象クラスを定義する。

  1. abstract void visit(ObjectStructure aDir);  
  2. abstract void visit(Element aFile);  

具体的な処理内容に依ってVisitorの継承クラスを実装する。例えば処理内容が「ファイルの名前一覧を作成する」であった場合、FilenameVisitorクラスを作成して、次のような実装を行う。

  1. /** 
  2.  * ディレクトリに対して行う処理を定義。 
  3.  * @param aDir ObjectStructure 
  4.  */  
  5. public void visit(ObjectStructure aDir)  
  6. {  
  7.   for (Element each : aDir.children())  
  8.   {  
  9.     each.accept(this);  
  10.   }  
  11. }  
  12.   
  13. /** 
  14.  * ファイルに対して行う処理を定義。 
  15.  * @param aFile Element 
  16.  */  
  17. public void visit(Element aFile)  
  18. {  
  19.   logger.print(aFile.getName());  
  20. }  

ObjectStructureの実装

Elementの集合を扱うクラスであり、以下のような実装になる。データ処理に関する実装は一切必要ないところがポイント。

  1. /** 
  2.  * 子要素のリストを取得する。 
  3.  * @return 子要素リスト 
  4.  */  
  5. public List<Element> children()  
  6. {  
  7.   return this.children;  
  8. }  
  9.   
  10. /** 
  11.  * データ処理インターフェースの受け口。 
  12.  * @param aVisitor Visitor 
  13.  */  
  14. public void accept(Visitor aVisitor)  
  15. {  
  16.   aVisitor.visit(this);  
  17. }  

Elementの実装

最小の要素を示すクラスであり、以下のような実装になる。これもデータ処理に関する実装は一切必要ない。

  1. /** 
  2.  * 名前を取得する。 
  3.  * @return 名前 
  4.  */  
  5. public String getName()  
  6. {  
  7.   return this.name;  
  8. }  
  9.   
  10. /** 
  11.  * データ処理インターフェースの受け口。 
  12.  * @param aVisitor Visitor 
  13.  */  
  14. public void accpt(Visitor aVisitor)  
  15. {  
  16.   aVisitor.visit(this);  
  17. }