タグ別アーカイブ: Java

SharedPreferencesでputStringSetを使う時の注意点

こんな感じでgetStringSetで得た結果に直接要素を追加して、
putStringSetしてもダメっぽい。

SharedPreferences pref 
  = context.getSharedPreferences("name",Context.MODE_PRIVATE);

String key = "keyA";

// Setを取得する
Set<String> set1 = pref.getStringSet(key, new HashSet<String>());

// Setに文字列を追加
set1.add("追加する文字列");

// そのまま保存
SharedPreferences.Editor editor = pref.edit();
editor.putStringSet(key, set1);
editor.apply();

 

この状態でgetStringSetするとちゃんと設定されてて正常に動いているように見えるんだけど、
プロセスが再起動されたあとに消えてます。(2件目以降)

対応としては下記のように別のSetを作ってコピーしてあげるとちゃんと動いてくれた。

// Setをクローンする
HashSet<String> set2 = new HashSet<>();
set2.addAll(set1);

SharedPreferences.Editor editor = pref.edit();
editor.putStringSet(key, set2);
editor.apply();

モヤモヤしますな。

Javaで青空文庫のルビをパースする

Swiftで作ったサンプルのJava版です。
仕様などはSwift版と同様なので→Swift版

こちらもJavaで正規表現を利用するサンプルにもなってます。
 

使い方

String sampleText = "てめえらの|血《ち》は|何色《なにいろ》だーっ!!";
AozoraRubyParser parser = new AozoraRubyParser(sampleText);
List<AozoraRubyParser.Result> tokens = parser.parse();
for (AozoraRubyParser.Result r : tokens){
  Log.d("", r.text +  (r.ruby != null ? "[" + r.ruby + "]" : "") );
}

 

パーサーのコード

public class AozoraRubyParser {

  static public class Result {
    public String text;
    public String ruby;

    public Result(String t, String r) {
      this.text = t;
      this.ruby = r;
    }
  }


  private String text;

  public AozoraRubyParser(String text) {
    this.text = text;
  }

  public List<Result> parse() {

    ArrayList<Result> result = new ArrayList<>();

    String patternStr = "|(.+?)《(.+?)》";
    Pattern ptn = Pattern.compile(patternStr);

    int position = 0;

    Matcher matcher = ptn.matcher(text);
    while (matcher.find()) {
      if (position < matcher.start()) {
        String subText = this.text.substring(position, matcher.start());
        result.add(new Result(subText, null));
      }
      position = matcher.end();

      // 文字と読み仮名を抽出
      result.add(new Result(matcher.group(1), matcher.group(2)));
    }

    // 文末の残りを追加
    if (this.text.length() > position) {
      String subText = this.text.substring(position, this.text.length());
      result.add(new Result(subText, null));
    }

    return result;
  }
}

AndroidでQRコードをリーダーを作る(超シンプル版)

GooglePlayサービス 7.8以降では「Mobile Vision API」ってのが追加されていて、これを使うとQRコードの認識が行えるようです。
ググってみると色々とサンプルはあるのですが、いかんせん複雑になっており「QRコードを認識する!」って箇所がわかり辛く思いました。
そこで超シンプルなサンプルを作成しましたよ。

簡単にQRコードが認識できると思いますよ。

まずは準備から。

build.gradleへ依存ライブラリを追記します。

dependencies {
  compile 'com.google.android.gms:play-services-vision:10.2.0'
}

次にManifestへ権限などを追記します。
下記の uses-feature、uses-permission、meta-dataを追記してください。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.eyewhale.sample">
  
  <!-- ↓ この2行を追記  -->
  <uses-feature android:name="android.hardware.camera" android:required="true"/>
  <uses-permission android:name="android.permission.CAMERA" />
  
  <application ・・ >
    
      <!-- ↓ このmeta-dataを追記  -->
      <meta-data
          android:name="com.google.android.gms.version"
          android:value="@integer/google_play_services_version" />
      <meta-data
          android:name="com.google.android.gms.vision.DEPENDENCIES"
          android:value="barcode" />

  </application>
</manifest>

これは説明がいるか不明ですが、、超シンプルなレイアウトファイル。
SurfaceViewにカメラから撮影された画像が表示されます。
(今回のサンプルではFragmentにしてみてます。よくあるサンプルがActivityだったので)

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/camera_preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

ようやく実装です。
実際のコードは最後に記述しておきますが、
大きな流れはこんなかんじです。

1. SurfaceViewにコールバックを設定
2. BarcodeDetectorを作ってProcessorを設定する
3. CameraSourceを作成してBarcodeDetectorを設定
4. SurfaceViewの準備ができたらCameraSourceを開始
  →バーコード認識も開始される
5. バーコードが認識されたらProcessorのreceiveDetectionsが呼ばれるので適当に処理

あと、Pause/Resumeでカメラの停止/再開を行っています。

import com.google.android.gms.vision.CameraSource;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;

public class BarcodeSampleFragment extends Fragment {
  private final static String TAG = "BarcodeSampleFragment";
    
  private SurfaceView _surfaceView;
  private CameraSource _cameraSource;
  private boolean _surfaceCreated = false;
  
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_barcode_sample, container, false);

    // SurfaceViewにコールバックを設定
    _surfaceView = (SurfaceView)view.findViewById(R.id.camera_preview);
    _surfaceView.getHolder().addCallback(_surfaceCallback);
    return view;
  }

  @Override
  public void onResume() {
    super.onResume();
      
    // CameraSourceが未作成 or 破棄されていたら作成
    if (_cameraSource == null) {
        setupCameraSource();
    }
        
    // SurfaceViewが準備できていたらキャプチャを開始
    if (_surfaceCreated){
        startCameraSource(_surfaceView.getHolder());
    }
  }

  @Override
  public void onPause() {
    super.onPause();
    // キャプチャを停止
    if (_cameraSource != null){
      _cameraSource.stop();
    }
  }
    
  private void setupCameraSource(){
    // QRコードを認識させるためのBarcodeDetectorを作成
    BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(this.getContext())
        .setBarcodeFormats(Barcode.QR_CODE)
        .build();
    // DetectorにProcessorというコールバックを設定
    barcodeDetector.setProcessor(_detactorProcessor);

    // CameraSourceを作成
    _cameraSource = new CameraSource.Builder(this.getContext(), barcodeDetector)
        .setAutoFocusEnabled(true)
        .setFacing(CameraSource.CAMERA_FACING_BACK)
        .build();
  }

  private void startCameraSource(SurfaceHolder holder){
    try {
      _cameraSource.start(holder);
    } catch (IOException e){
        Log.e(TAG,"",e);
    }
  }
  
  private Detector.Processor<Barcode> _detactorProcessor = new Detector.Processor<Barcode>(){
    @Override
    public void release() {
    }
        
    @Override
    public void receiveDetections(Detector.Detections<Barcode> detections) {
      // 認識された結果がdetectionsに入ってる
      for(int i=0;i<detections.getDetectedItems().size();i++){
        int key = detections.getDetectedItems().keyAt(i);
        Barcode barcode = detections.getDetectedItems().get(key);
        String barcodeValue = barcode.rawValue;
        Rect rect = barcode.getBoundingBox();
        // 認識結果をログへ出力
        Log.d(TAG,"detect barcode:" + barcodeValue + " " + rect.toString());
      }
    }
  };

  private SurfaceHolder.Callback _surfaceCallback = new SurfaceHolder.Callback() {
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
      try {
          // カメラのキャプチャを開始
          _cameraSource.start(holder);
      } catch (IOException e){
          Log.e(TAG,"",e);
      }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
      _cameraSource.stop();
      _cameraSource.release();
      _cameraSource = null;
    }
  }; 
}

※ 注意!
このサンプルはシンプルにするため色々と省いてます
– カメラへアクセスするための権限チェック
– GooglePlayServicesが利用可能であるかのチェック
– BarcodeDetectorが利用可能であるかのチェック
– その他もろもろ
実際に使う時は注意してくださいね!

「javaでファイルのリストを取得したら遅いよ」問題への対策

javaでファイルのリストを取得したら遅いよ。
ローカルでも遅いんだけど、ネットワーク越しだともうね。。

あまりに遅いので対策を考えてみました。

例えば指定されたディレクトリ以下のファイル名を全て取得するとこんなコードになったりしますよね。


private listFiles(File dir){
    File[] files = dir.listFiles();  // (a)
    for (File f : files) {
        if (f.isFile()){  // (c)
            String filename = f.getName();  // (b)
            System.out.println(filename);
        } else {
            listFile(f);
        }
    }
}

単純ですね。
ただ、これだと遅いんです。

Fileのインスタンスから取得している情報は下記の3つなのですが、どうやらそれぞれについて毎回システムコールを行っているようなのです。
それがネットワーク越しだと、ネットワークアクセスが発生します(たぶん)
・ディレクトリに含まれるディレクトリ/ファイルのFileインスタンス (a)
・ファイルの名前 (b)
・Fileインスタンスがディレクトリであるか (c)

で、代替手段として次のようにしました。
1. File.listFiles() の代わりにFile.list() を使う
listFilesはFileインスタンスを返しますが、listはファイル名を返します。
ファイル名からFileインスタンスを作成することで 上記の(a)と(b)が同時に取得できます。

2. isFile()の代わりに File.list() の戻り値がnullであるか判定する
対象のFileインスタンスがファイルの時、list()はnullを返すようです。
ですので、上記1でlist()を実行した際の戻り値を判定することで isFile を呼ぶ必要がなくなります。

3. (これは代替とはちょっと違いますが) Fileのメソッドの戻り値をキャッシュする
例えば File.getName() を複数回呼んでいる場合、変数に保存しておいてFileのメソッドは1回しか呼ばない

上記の処理を抜粋すると


// ファイル名のリストを取得する  
File f = new File(省略);  
String[] filenames = f.list();  
  
// ファイルであるか判定  
boolean isFile = (filenames == null);  
  
Fileのインスタンスを取得する  
File[] subfiles = Arrays.stream(filenames).map(name -> new File(f,name)).toArray(File[]::new);  

こんな感じです。

環境や処理内容にもよると思いますが、これらの対策で処理時間が1/3になってくれましたよ。

“AWS SDK for Java”を使ってCloudSearchから検索結果を取得する

CloudSearchの検索時に”AWS SDK for Java”を使ってみるサンプルです。
HttpClient等を使った例は良く見かけますが、SDKを使った例が見つからなかったので載せときます。

scalaでの記述例ですけど、javaでもほとんど同じですよ。

import com.amazonaws.auth.AWSCredentials
import com.amazonaws.services.cloudsearchdomain.AmazonCloudSearchDomainClient
import com.amazonaws.services.cloudsearchdomain.model.SearchRequest
import com.amazonaws.services.cloudsearchdomain.model.QueryParser
import com.amazonaws.services.cloudsearchdomain.model.SearchResult


// search用のエンドポイント
val endpoint = "search-testdb-xxxxxxxxx.ap-northeast-1.cloudsearch.amazonaws.com"

// クライアントを準備
val credentials = new BasicAWSCredentials("accessKey", "secretKey")
val client = new AmazonCloudSearchDomainClient(credentials)
client.setEndpoint(endpoint)


// リクエストを生成
// 各項目に設定する内容はhttpクライアント等で search apiを実行する時に
// 設定する内容とだいたい一緒です  
// 詳しくは↓
// http://docs.aws.amazon.com/cloudsearch/latest/developerguide/search-api.html
//
var request = (new SearchRequest())
	.withQueryParser(QueryParser.Simple)
	.withQuery("検索ワード")
	// titleとsubtitleに対して"AND"検索を行う	
	.withQueryOptions("{\"defaultOperator\":\"and\",\"fields\":[\"title\",\"subtitle\"]}")
	// 先頭から1000件まで取得	
	.withStart(0)
	.withSize(1000)
	// ソート条件を設定
    .withSort("sortkey1 asc,sortkey2 desc")
    // 結果として取得する項目
    .withReturn("_score,title,ranking")
    

try{
    // 検索処理
    val result = client.search(request)
	
	 
    val status  = result.getStatus()
    val hits = result.getHits()
    
    // インデックス全体でのトータル件数(startとsizeで指定した範囲外も含む)
    val total = hits.getFound();
    
    // 取得したレコードをマップ (javaな人は for-eachしていると思って下さい)
    hits.getHit().map{rec => 
    
      // ID項目
      val id = rec.getId()
      
      // Fieldを参照してみる
      val ranking = rec.getFields().get("ranking").get(0)
      
      Logger.info(" id:" + id + " ranking:" + ranking)
    }

} finally {
   client.shutdown()
}

なぜSDKを使う必要があったかと言うと、IAMロールでのアクセス制限を行いたかったからです。
署名済リクエストを作るのがやたらと面倒だったので・・・
SDKを使う事で簡単に署名済のリクエストが行えます。

“AWS SDK for Java”を使ってCloudSearchへデータを登録する

前回の検索に続いてこんどはドキュメントの更新処理です
これもSDKを使ったサンプルが見つからないんですよね。。。探し方が悪いのかな??

scalaでの記述になっていますけど、javaでもほとんど同じです。

import com.amazonaws.auth.AWSCredentials
import com.amazonaws.services.cloudsearchdomain.AmazonCloudSearchDomainClient
import com.amazonaws.services.cloudsearchdomain.model.UploadDocumentsRequest
import com.amazonaws.services.cloudsearchdomain.model.ContentType
import java.io.ByteArrayInputStream


// document用のエンドポイント
val endpoint = "document-testdb-xxxxxxxxx.ap-northeast-1.cloudsearch.amazonaws.com"

// クライアントを準備
val credentials = new BasicAWSCredentials("accessKey", "secretKey")
val client = new AmazonCloudSearchDomainClient(credentials)
client.setEndpoint(endpoint)

// データ投入用のjsonデータを作成する
val datajson = "[{ ...省略... }]"

// json文字列をbyte配列としてInputStreamを取得
val postdata = datajson.getBytes()
val data = new ByteArrayInputStream(postdata); 

// リクエストを生成
var request = (new UploadDocumentsRequest())
    .withContentType(ContentType.Applicationjson)
    .withContentLength(postdata.length)
    .withDocuments(data)
     
// uploadを実行
val response = client.uploadDocuments(request)

// 何件登録して、何件削除したのかを確認できます      
Logger.info(response.toString())

client.shutdown()

datajsonに入れる文字列はHttpクライアント等で “documents/batch” APIにpostする内容と同じです
詳しくは↓
http://docs.aws.amazon.com/cloudsearch/latest/developerguide/document-service-api.html

簡単なサンプルですとこんな感じです

[
  { 
  	"fields": { "title": "タイトル100","subtitle": "サブタイトル" }, 
	"id": "100", 
	"type": "add"
  }, 
  {
  	"fields": { "title": "タイトル101", "subtitle": "サブタイトル" }, 
	"id": "101", 
	"type": "add"
  },
  {
  	"id": "900",
  	"type": "delete"
  }
]

jsonはJacksonとかGsonとかで作ると楽ですね。

隣接したQuadKeyを求める

googleマップ、Bingマップなどで使用するクアッドキー
大変良く出来たキー体系で、設計者の頭の良さが想像できますね。

さて、ちょっと地図情報を扱う上で、並んだQuadKeyを求める必要がありましたので
プログラムを書いてみました。

今回はjavaですが、簡単なソースなので他の言語へも適用しやすいと思います。

/**
 * 指定されたキーの隣にあたるQuadKeyを求める
 * @param srcKey 元のQuadKey
 * @param direction 0:左 1:右 2:上 3:下
 * @return
 */
private static String calcNextQuadKey(String srcKey,int direction)
{
     int lv = srcKey.length();

     long latbits = 0;
     long lonbits = 0;
     char c;
     int val;
     int bit;

     for (int i = 0; i < lv; i++) {
          c = srcKey.charAt(i);
          val = c - '0';

          // valの2ビット目を縦位置を示すlatbitsに格納
          bit = val >> 1;
          latbits += (bit << (lv - i - 1));

          // valの1ビット目を横位置を示すlonbitsに格納
          bit = val & 1;
          lonbits += (bit << (lv - i - 1));
     }

     // 隣接したQuadKeyを求めるためlat、lonを変更
     switch (direction) {
     case 0:// 左
          lonbits -= 1;
          break;
     case 1:// 右
          lonbits += 1;
          break;
     case 2:// 上
          latbits -= 1;
          break;
     case 3:// 下
          latbits += 1;
          break;
     }

     // lv以上の精度に繰り上がった場合には最上位ビットをクリアする
     long floodmask = 1 << lv;
     if ((lonbits & floodmask) != 0) lonbits ^= floodmask;
     if ((latbits & floodmask) != 0) latbits ^= floodmask;

     // latbits、lonbitsからクアッドキーを求める
     StringBuilder sb = new StringBuilder(lv);
     long mask = 1 << (lv - 1);
     boolean lonbit, latbit;
     for (int i = 0; i < lv; i++) {

          lonbit = ((lonbits & mask) != 0);
          latbit = ((latbits & mask) != 0);

          if (latbit) {
               c = lonbit ? '3' : '2';
          } else {
               c = lonbit ? '1' : '0';
          }
          sb.append(c);

          mask >>>= 1;
     }
     return sb.toString();

}

以上.