ラベル Java の投稿を表示しています。 すべての投稿を表示
ラベル Java の投稿を表示しています。 すべての投稿を表示

2019年2月27日水曜日

MySQL JDBC での batchInsert

Java で JDBC を使用して MySQL への batchInsert (バルクインサート) で嵌った・・・

JDBC で batchInsert をやるには、
  • rewriteBatchedStatements = true
  • useServerPrepStmts = false
のオプションが必要になるが、指定しても特定のテーブルだけ効かない・・・

テーブル
CREATE TABLE testtbl(selector_type INT);

Java
Class.forName("com.mysql.jdbc.Driver").newInstance();

Properties prop = new Properties();
prop.setProperty("user", "testuser");
prop.setProperty("password", "password");
prop.setProperty("characterEncoding", "utf8");
prop.setProperty("rewriteBatchedStatements", "true");
prop.setProperty("useServerPrepStmts", "false");

try (Connection con= DriverManager.getConnection("jdbc:mysql://localhost/testdb", prop)) {
  PreparedStatement stmt = con.prepareStatement("INSERT INTO testtbl (selector_type) VALUES (?)");
  for (int i = 0; i < 10; ++i) {
    stmt.setInt(1,  i);
    stmt.addBatch();
  }

  stmt.executeBatch();
}

結果
2019-02-27T02:28:18.112397Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (0);
2019-02-27T02:28:18.112734Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (1);
2019-02-27T02:28:18.112853Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (2);
2019-02-27T02:28:18.113839Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (3);
2019-02-27T02:28:18.113953Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (4);
2019-02-27T02:28:18.114038Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (5);
2019-02-27T02:28:18.114121Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (6);
2019-02-27T02:28:18.114202Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (7);
2019-02-27T02:28:18.114291Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (8);
2019-02-27T02:28:18.114370Z        19 Query     INSERT INTO testtbl (selector_type) VALUES (9)

VALUES のあとにスペースが必要との情報もあったが、既についてる・・・

JDBC ドライバーのソースを追ってみたところ、PreparedStatement クラスに

if (StringUtils.indexOfIgnoreCase(statementStartPos, sql, "SELECT", "\"'`", "\"'`", StringUtils.SEARCH_MODE__MRK_COM_WS) != -1) {
  return false;
}
的なコードが・・・

SELECT INSERT を除外しているようだが、もしやということで、カラム名をバッククウォートで囲ったところ

Java
snip ...

  PreparedStatement stmt = con.prepareStatement("INSERT INTO testtbl (`selector_type`) VALUES (?)");

snip ...

結果
2019-02-27T02:28:39.536756Z        20 Query     INSERT INTO testtbl (`selector_type`) VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)
連結されて、1 文で実行されることに!
SQL 文の中に SELECT という文字があるとダメな模様。

さらに調べてみると既に報告が上がっていた。
https://bugs.mysql.com/bug.php?id=81468
2016 年 5 月・・・

2013年9月2日月曜日

Java でフルカラー画像をグレースケールに変換

Java でフルカラー Jpeg を 8bit グレースケールの PNG に変換した際に嵌った記録・・・

元となる画像(1024 x 768)は、
これから、8bit グレースケールの PNG を生成することとする。

共通処理として、

画像読み込み

public static BufferedImage loadImage(String filename) {
  try (FileInputStream in = new FileInputStream(filename)) {
    return ImageIO.read(in);
  }
}

画像出力

public static void outputImage(String filename, BufferedImage image) throws IOException {
  ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
  writer.setOutput(ImageIO.createImageOutputStream(new File(filename)));
  writer.write(image);
  writer.dispose();
}
の 2 つを用意。

変換処理として、書き出し時に JPEGImageWriteParam の PNG 版のようなもので指定して変換できるかと、探したが見つからなかった。
com.sun.imageio.plugins.png 以下にいくつか PNG ようのクラスがあるようだ。
が、よく分からなかったので、タイプ指定で TYPE_BYTE_GRAY を指定した BufferedImage を新たに生成。
それにデータをコピーすることに。

変換処理は、2 パターン。

パターン1Graphics2D#drawImage() を使用

public static BufferedImage grayScale1(BufferedImage image) throws Exception {
  int w = image.getWidth(), h = image.getHeight();
  BufferedImage output = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
  Graphics2D g2d = output.createGraphics();
  g2d.drawImage(image, 0, 0, w, h, null);
  return output;
}

パターン 2BufferedImage#setData() を使用

public static BufferedImage grayScale2(BufferedImage image) throws Exception {
  int w = image.getWidth(), h = image.getHeight();
  BufferedImage output = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
  output.setData(image.getData());
  return output;
}

パターン1、パターン2 をそれぞれ呼び出し

try {
  BufferedImage org = loadImage("org.jpg");
  outputImage("output1.png", grayScale1(org));
  outputImage("output2.png", grayScale2(org));
} catch (Exception e) { e.printStackTrace(); } 

パターン1はいい感じ。
パターン2はデカッ
色が減った分、横に4倍引き伸ばされた感じ?画像データ配列の変換がおかしくなったようだ。

パターン1


パターン2



BufferedImage の setRGB() の説明を読むと、
デフォルトモデルがイメージ ColorModel と一致しない場合は色変換が行われます。
と書かれていたのでこちらを使うようにしてみた。


パターン 3BufferedImage#setRGB() を使用

public static BufferedImage grayScale3(BufferedImage image) throws Exception {
  int w = image.getWidth(), h = image.getHeight();
  BufferedImage output = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY);
  output.setRGB(0, 0, w, h, image.getRGB(0, 0, w, h, null, 0, w), 0, w);
  return output;
} 

パターン1にくらべかなり黒っぽくなった。

パターン3


今回は、パターン1がよさそうだ。

実は、実際嵌った時の画像では、パターン1の場合、元画像の白の部分がグレーがかってしまいパターン3で出力したほうがはっきりしたものになった。
元画像によって変えるのも大変だが・・・

Graphics2D の場合、パラメータを色々指定できるのでそのあたりをいじってみても変わるかもしれない。

2013年8月7日水曜日

Googlebot のクロール時の jsessionid

とあるシステムでアクセスログを見ていたら、Googlebot が jsessionid 付の URL でアクセスしていた・・・

初回アクセス時や、Cookie 未対応の端末の場合、jsessionid を付ける設定になっている。
セキュリティー上よろしくないのだが、ガラケー対応のサイトのため、
アプリケーションサーバ Tomcat の設定で止めることができない。

ログイン必須のページとそうでないページがあり、Googlebot がログインするわけでもないので、
無視することもできるが、SEO 的によろしくない気がする。何より気持ち悪い・・・
というわけで対応することに。

ググってみると、URL Rewrite Filter なるものがあるらしい。
mod_rewrite のフィルターバージョンとのこと。
mod_rewrite にリンク等を書き換える機能があったっけと思いつつみてみると、
<%= response.encodeURL("/world.jsp?country=usa&amp;city=nyc") %>

<c:url value="/world.jsp?country=${country}&amp;city=${city}" />

のように指定した URL が正規表現で指定した値に置換されるようだ。

SAStruts や mobylet を使用しているので、c:url はほとんど使っていないが、
フレームワークの内部的には、response.encodeURL を使っているだろうと思われるので問題はなさそう。

ただ、jsessionid を付加しておいて置換で消すというのは冗長な気がする・・・
ソースを読んだわけではないので実装がどうなっているかは分からないが・・・
最初から付加しなければいいはず。

Tomcat の HttpServletResponse.encodeURL は、
HttpServletRequest.isRequestedSessionIdFromCookie を見てた気がする、ということで、
Bot の UserAgent だった場合、HttpServletRequestWrapper を作成し、
isRequestedSessionIdFromCookie で常に true を返すことに。

public class BotFilter implements Filter {
  private static class BotRequesteWrapper extends HttpServletRequestWrapper {
    public CrawlerRequestWrapper(HttpServletRequest request) {
      super(request);
    }

    @Override
    public boolean isRequestedSessionIdFromCookie() {
      return true;
    }

    @Override
    public boolean isRequestedSessionIdFromURL() {
      return false;
    }
  }


  private static final Pattern PAT_BOT =
    Pattern.compile("Googlebot|Y!J|YahooSeeker|msnbot|bingbot");


  @Override
  public void init(FilterConfig filterConfig) throws ServletException {}

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest)request);
    chain.doFilter(PAT_BOT.matcher(req.getHeader("User-Agent")).find() ? new BotRequestWrapper(req) : request, response);
  }

  @Override
  public void destroy() {}
}

Google, Yahoo, MSN, Bing 等の場合、isRequestedSessionIdFromCookie で常に true、
念のために、isRequestedSessionIdFromURL は 常に false を返す。
そして、フィルターを設定。
<filter>
  <filter-name>botFilter</filter-name>
  <filter-class>com.example.BotFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>botFilter</filter-name>
  <url-pattern>/*</url-pattern>
  <dispatcher>REQUEST</dispatcher>
  <dispatcher>FORWARD</dispatcher>
  <dispatcher>INCLUDE</dispatcher>
  <dispatcher>ERROR</dispatcher>
</filter-mapping> 
UserAgent を Bot のものにして、Cookie を削除後アクセスすると、ものの見事に
jsessionid が付加されている・・・

何故・・・と言う事で追ってみると、どうも ServletResponse 内に ServletRequest を保持しており、
 HttpServletRequestWrapper とは別物のよう・・・

仕方がないので、HttpServletResponseWrapper を作成し、直接 encodeURL を変更することに。
public class CrawlerFilter implements Filter {
  private static class BotResponseWrapper extends HttpServletResponseWrapper {
    public BotResponseWrapper(HttpServletResponse response) {
      super(response);
    }


    @Override
    public String encodeRedirectURL(String url) {
      return url;
    }

    @Override
    public String encodeURL(String url) {
      return url;
    }
  }


  private static final Pattern PAT_BOT =
    Pattern.compile("Googlebot|Y!J|YahooSeeker|msnbot|bingbot");


  @Override
  public void init(FilterConfig filterConfig) throws ServletException {}

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException {
    chain.doFilter(request,
      PAT_BOT.matcher(((HttpServletRequest)request).getHeader("User-Agent")).matches() ?
        new BotResponseWrapper((HttpServletResponse)response) : response);
    }

    @Override
    public void destroy() {}
}
encodeUR(), encodeRedirectURL() ともに渡された URL を返すだけにする。

これで、jsessionid が付加されなくなった。

2012年10月19日金曜日

Java による画像縮小 lanczos 編

JDK を使った縮小の結果納得行かなかったので、GIMP なんかにもある lanczos による縮小を実装してみることに。
元画像は Jpeg なので、RGB の 3 色のみを扱う。

こちらの Ruby バージョンをとりあえず Java にポーティング。
今回縮小のみなので、拡大部分を削除。実行してみると 6 秒前後かかった・・・

もう少し早くならないかなと、さらにコメントのリンク先を読んでみる。

何をやっているかというと、中心を 1 として距離によって重みを付けるということのようだ。
| 0.3 | 0.5 | 0.3 |
|-----+-----+-----|
| 0.5 | 1.0 | 0.5 |
|-----+-----+-----|
| 0.3 | 0.5 | 0.3 |

といった具合に。
重みを計算するのが lanczos 関数。
lanczos2 や lanczos3 の数字は距離で、縮小画像のあるピクセルを計算するために、対応する元画像ピクセルからどれくらいの範囲を参照するかを決めるものっぽい。

となると、縮小率と距離の数字が決まれば、対象のピクセルがどこであろうと、重みは一緒?
というわけで、先に上記のような重みの表を作成しておき、各ピクセル計算時はそれを使用すれば計算量は減るだろう。

中心からのオフセットと重みを保持するクラス

public class Weight {
  public final int offsetX;
  public final int offsetY;
  public final double weight;

  public Weight(int x, int y, double w) {
    offsetX = x;
    offsetY = y;
    weight = w;
  }
}

重みの一覧と合計を保持するクラス

public class WeightFilter {
  public final double weightTotal;
  public final Weight[] weightList;


  public WeightFilter(double sumw, Weight[] weights) {
    weightTotal = sumw;
    weightList = weights;
  }
}

lanczos 関数

public static double lanczos(double x, double n) {
  if (x == 0.0) return 1.0;
  if (Math.abs(x) >= n) return 0.0;
  return Math.sin(Math.PI * x) / Math.PI / x * (Math.sin(Math.PI * x / n) / Math.PI / x * n);
}

縮小率と距離で重み一覧作成

縦・横別でなく斜めも距離を計算してみる
public static WeightFilter getReduceFilter(double scale, int distance) {
  double bdx0 = 0 + 0.5, bdy0 = 0 + 0.5;
  int ss = (int)((bdx0 - distance) / scale), se = (int)((bdx0 + distance) / scale);
  double sumw = 0.0;
  List<Weight> weightList = new ArrayList<>((se - ss) * (se - ss));

  for (int sy = ss; sy <= se; ++sy) {
    for (int sx = ss; sx <= se; ++sx) {
      double xl = (sx + 0.5) * scale - bdx0;
      double yl = (sy + 0.5) * scale - bdy0;
      double w = lanczos(Math.sqrt(xl * xl + yl * yl), distance);
      if (w == 0.0) continue;
      weightList.add(new Weight(sx, sy, w));
      sumw += w;
    }
  }
  return new WeightFilter(sumw, weightList.toArray(new Weight[0]));
}


ユーティリティ関数も作成。

指定座標の RGB 値取得

範囲外は鏡像として取得。
public static int getRGB(int[] image, int x, int y, int w, int h) {
  x = Math.abs(x); y = Math.abs(y);
  if (x >= w) x = w + w - x - 1;
  if (y >= h) y = h + h - y - 1;
  return image[y * w + x];
}

0 - 255 の範囲で色値取得

public static int getColor(double val, double sumw)
{
  if (sumw != 0.0) val /= sumw;
  if (val < 0.0) return 0;
  if (val > 255.0) return 255;
  return (int)val;
}

縮小処理

public static BufferedImage reduce3(BufferedImage image, int n, int dw, int dh)
  int sw = image.getWidth(), sh = image.getHeight();

  double scale = (double)dw / sw;
  BufferedImage thumb = new BufferedImage(dw, dh, image.getType());
  WeightFilter filter = getFilter(scale, n);
  double sumw = filter.weightTotal;
  int[] colors = image.getRGB(0, 0, sw, sh, null, 0, sw);

  for (int dy = 0; dy < dh; ++dy) {
    for (int dx = 0; dx < dw; ++dx) {
      double dr = 0.0, db = 0.0, dg = 0.0;
      int bsx = (int)(dx / scale), bsy = (int)(dy / scale);
      for (Weight weight : filter.weightList) {
        int rgb = getRGB(colors, bsx + weight.offsetX, bsy + weight.offsetY, sw, sh);
        dr += ((rgb >> 16) & 0xFF) * weight.weight;
        dg += ((rgb >> 8) & 0xFF) * weight.weight;
        db += (rgb & 0xFF) * weight.weight;
      }
      thumb.setRGB(dx, dy, (getColor(dr, sumw) << 16) | (getColor(dg, sumw) << 8) | getColor(db, sumw));
    }
  }
  return thumb;
}

呼び出し

lanczos2 と lanczos3 で縮小。
long start3_1 = System.currentTimeMillis();
BufferedImage reduce3_1 = reduce3(base, 2, 120, 90);
System.out.println("reduce3_1: " + (System.currentTimeMillis() - start3_1));
outputImage("reduce3_1.jpg", reduce3_1);

long start3_2 = System.currentTimeMillis();
BufferedImage reduce3_2 = reduce3(base, 3, 120, 90);
System.out.println("reduce3_2: " + (System.currentTimeMillis() - start3_2));
outputImage("reduce3_2.jpg", reduce3_2);

結果

lanczos2: 126.3ms
lanczos3: 233.0ms

元画像と左から、パターン1 3段階、パターン2 3段階、lanczos2、lanczos3




大分早くなった。JDK に比べると遅いが画質はいい。
一度作成してた後は、キャッシュするようにすればいいので、満足。

Java による画像縮小 JDK 編

忙しい日々が続いて、色々嵌っているが書けていない・・・
また同じ事で嵌まることになるだろう・・・

半年以上前だが、Java で画像を縮小してサムネイルを作成する際に、なるべく画質を落としたくなかったので色々試したのでその記録を。
結局ブログ書くために再調査したわけだけ・・・

元となる画像(1024 x 768)は、
これから 120 x 90 のサムネイルを作成することとする。


共通処理として、

画像読み込み

public static BufferedImage loadImage(String filename) {
  try (FileInputStream in = new FileInputStream(filename)) {
    return ImageIO.read(in);
  }
}

画像出力

// 品質 95 Jpeg
public static void outputImage(String filename, BufferedImage image) throws IOException {
  JPEGImageWriteParam param = new JPEGImageWriteParam(Locale.getDefault());
  param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
  param.setCompressionQuality(0.95f);

  ImageWriter writer = ImageIO.getImageWritersByFormatName("jpg").next();
  writer.setOutput(ImageIO.createImageOutputStream(new File(filename)));
  writer.write(null, new IIOImage(image, null, null), param);
  writer.dispose();
}
の 2 つを用意。

縮小処理は、2 パターン。

パターン1 BufferedImage#getScaledInstance() を使用

public static BufferedImage reduce1(BufferedImage image, int dw, int dh) {
  BufferedImage thumb = new BufferedImage(dw, dh, image.getType());
  thumb.getGraphics().drawImage(image.getScaledInstance(dw, dh, Image.SCALE_AREA_AVERAGING), 0, 0, dw, dh, null);
  return thumb;
}

パターン 2 Graphics2D#drawImage() を使用

public static BufferedImage reduce2(BufferedImage image, int dw, int dh) {
  BufferedImage thumb = new BufferedImage(dw, dh, image.getType());
  Graphics2D g2d = thumb.createGraphics();
  g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
  g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
  g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
  g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
  g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
  g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
  g2d.drawImage(image, 0, 0, dw, dh, null);
  return thumb;
}

パターン1、パターン2 をそれぞれ呼び出し

try {
  BufferedImage base = loadImage("base.jpg");
  int dw = 120, dh= 90;

  long start1_1 = System.currentTimeMillis();
  BufferedImage reduce1_1 = reduce1(base, dw, dh);
  System.out.println("reduce1_1: " + (System.currentTimeMillis() - start1_1));
  outputImage("reduce1_1.jpg", reduce1_1);

  long start2_1 = System.currentTimeMillis();
  BufferedImage reduce2_1 = reduce2(base, dw, dh);
  System.out.println("reduce2_1: " + (System.currentTimeMillis() - start2_1));
  outputImage("reduce2_1.jpg", reduce2_1);
} catch (Exception e) { e.printStackTrace(); } 

パターン1はぼやけた感じ、パターン2はざらついた感じ。
連続で 4 回実行し、1 回目を除いた 3 回の平均時間を計測。
速度はパターン2が早いが、今回は作成した画像をキャッシュするので速度より画質。

パターン1:  80.3ms


パターン2: 0.7ms


段階的に縮小

近傍縮小法だと、大きな画像から一気に縮小すると色情報の欠落が大きくなるので、段階的に縮小してみる。
  • 1024x768 -> 640x480 -> 120x90 の 2 段階
  • 1024x768 -> 800x600 -> 480x360 -> 120x90 の 3 段階
の 2 つそれぞれのパターンで実行。
int dw2_1 = 640, dh2_1 = 480;
int dw3_1 = 800, dh3_1 = 600, dw3_2 = 480, dh3_2 = 360;

long start1_2 = System.currentTimeMillis();
outputImage("reduce1_2.jpg", reduce1(reduce1(base, dw2_1, dh2_1), dw, dh));
System.out.println("reduce1_2: " + (System.currentTimeMillis() - start1_2));

long start2_2 = System.currentTimeMillis();
outputImage("reduce2_2.jpg", reduce2(reduce2(base, dw2_1, dh2_1), dw, dh));
System.out.println("reduce2_2: " + (System.currentTimeMillis() - start2_2));

long start1_3 = System.currentTimeMillis();
outputImage("reduce1_3.jpg", reduce1(reduce1(reduce1(base, dw3_1, dh3_1), dw3_2, dh3_2), dw, dh));
System.out.println("reduce1_3: " + (System.currentTimeMillis() - start1_3));

long start2_3 = System.currentTimeMillis();
outputImage("reduce2_3.jpg", reduce2(reduce2(reduce2(base, dw3_1, dh3_1), dw3_2, dh3_2), dw, dh));
System.out.println("reduce2_3: " + (System.currentTimeMillis() - start2_3));
パターン1 2段階: 141.7ms


パターン1 3段階: 215ms


パターン2 2段階: 7.3ms


パターン2 3段階: 16.3ms


多少マシになったが、ちょっと納得いかないレベル・・・
続く

2012年4月22日日曜日

Apache + Tomcat のレスポンス

ここのところ忙しく、全然更新していなかったが、実際には色々嵌りまくりな日々・・・

その中でも最近一番大きかったのが、Apache + Tomcat の構成でレスポンスがおかしい現象が発生。
しばらく使っていると、別のリクエストのレスポンスが返ってくるようになる。
別のユーザのユーザ情報が見えたり・・・セッションがおかしくなったり・・・

Apache または、Tomcat のいずれかを再起動すると普通に戻るがしばらくするとまた発生し始める
プログラムのせいではないことを祈り調査。

接続は mod_proxy_ajpを使用して mod_proxy_balancer で分散していた。
分散していないテスト環境でも発生していたため、mod_proxy_balancer は調査から除外。
mod_proxy_ajp を mod_proxy_http に変更したところ発生しなくなった。
過去に何度も Apache + Tomcat, mod_proxy_ajp 構成をやったことはあるが初めての現象。

次に、Apache 2.4 を使っていたため、2.2 にダウングレード。
mod_proxy_ajp で接続して問題は発生しなかった。

つまり、Apache 2.4 の mod_proxy_ajp が問題だったと思われる。
Apache 2.4 は今回初めて使用したが、まだ早かったんだろうか・・・

2012年1月11日水曜日

SAStruts + Maven

久々、Java での Web アプリケーションを作成をすることに。
最近のフレームワークが分からないため、今更 SAStruts を選択。最近は何がメジャーなんだろうか・・・

とりあえず開発環境を最新版でそろえていく。
  • JDK 1.7.0_02
  • Maven 3.0.3
  • Eclipse 3.7 64bit(Pleiades のプラグイン + Maven プラグイン)
  • Tomcat 7.0.23 -> 6.0.35
Maven で SAStruts のプロジェクトを作成。
# mvn archetype:generate -DarchetypeRepository=https://www.seasar.org/maven/maven2/ \
  -DarchetypeGroupId=org.seasar.sastruts \
  -DarchetypeArtifactId=sa-struts-archetype -DarchetypeVersion=1.0.4-sp9.0 \
  -DgroupId=com.example.sample.sas -DartifactId=sas -Dversion=1.0.0 
Eclipse 用プロジェクト作成。
# cd sas
# mvn eclipse:eclipse -Dwtpversion=1.5
Eclise で「インポート」->「一般」->「既存プロジェクトをワークスペースへ」でインポート。が、ここで問題発生。
プロジェクトを右クリックしても Maven のメニューが出てこない・・・ Maven プラグインは入れたはずなのに。 昔はこれでいけてた気がしたが、バージョンが上がって変わったのだろうか。

色々検索したり、Maven のバージョン変えたりプラグインを再インストールしたりしてみたがうまくいかない。で、かなりして Eclipse のインポートメニューに Maven の項目を発見。
 mvn archietype:generate でプロジェクト作成後、mvn eclipse:eclipse は行わずに、Eclipse の「インポート」->「Maven」->「Existing Maven Project」でインポート。途中、「maven-antrun-plugin:1.3:run (1 errors)」 とエラーが出るがかまわず完了。これで、プロジェクトの右クリックメニューに Maven の項目が出た。

取り込み後、pom.xml に警告とエラーが出ているので潰していく。
まず、<project> タグで警告がでるため、クイック・フィックスで修正。

次に、maven-antrun-plugin の「Plugin execution not covered by lifecycle configuration: org.apache.maven.plugins:maven-antrun-plugin:1.3:run (execution: delete-lib-dir, phase: initialize)」もクイック・フィックスの「Permanently mark goal run in pom.xml as ignored in Eclipse build」で修正する。

最後の、maven-dependency-plugin の「maven-dependency-plugin (goals "copyff-dependencies", "unpack") is not supported by m2e.」は、上記修正で追加された、pluginManagement/plugins/plugin/configuration/lifecycleMappingMetadata/pluginExecutions に設定を追加する。
<pluginExecution>
  <pluginExecutionFilter>
    <groupId>
      org.apache.maven.plugins
    </groupId>
    <artifactId>
      maven-dependency-plugin
    </artifactId>
    <versionRange>[1.3,)</versionRange>
    <goals>
      <goal>copy-dependencies</goal>
    </goals>
  </pluginExecutionFilter>
  <action>
    <ignore></ignore>
  </action>
</pluginExecution>
ついでに、build/plugins/plugin/artifactId が maven-compiler-plugin の source と target を 1.7 に変更。

プロジェクトを右クリックして 「Maven」 -> 「プロジェクト構成の更新」を実行して反映すればエラーと警告が消えた。

次に、WTP の設定。
プロジェクトを右クリックしてプロパティを開き、プロジェクト・ファセットを選択。
Convert to faceted from... をクリック。動的 Web モジュールにチェックを入れて、バージョンを確認すると、3.0 になっている。SAStruts は 3.0 で動くのだろうか・・・調べてみるとどうもダメっぽいので、2.5 に変更。よって Tomcat も 6 に落とす。
プロジェクト・ファセットの設定がなぜだか、1 回目だと失敗する。2 回やると通ったのでそのまま続けることにする・・・
次に、プロパティダイアログの Deployment Assembly を選択。/WebContent の行を削除。追加をクリックして、 「フォルダー」より src/main/webapp を追加。/WebConent のディレクトリは物理的に削除して大丈夫だと思われる。さらに、追加をクリックして、「Java Build Path Entries」より Maven 依存関係を追加。

src/main/webapp/WEB-INF/web.xml のバージョンが 2.4 になっているので 2.5 に変更。
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  id="WebApp_ID" version="2.5">
dependencies/dependency の geronimo-jsp_2.0_spec を geronimo-jsp_2.1_spec に、バージョンを 1.0 から 1.0.1 に変更。
同じく、geronimo-servlet_2.4_spec を geronimo-servlet_2.5_spec に、バージョンを 1.0 から 1.2 に変更。

プロジェクトを右クリックして、「実行」->「サーバで実行」でサーバを起動すると特にエラーもなく無事起動。
だが、ブラウザで http://localhost:8080/sas/ にアクセスすると、「org.apache.jasper.JasperException: /WEB-INF/view/index.jsp(1,1) /WEB-INF/view/common/common.jsp(1,61) JARファイル "file:/ワークスペースのパス/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/sas/WEB-INF/lib/standard-1.1.2.jar" からTLD "META-INF/c.tld" を読み込めません: org.apache.jasper.JasperException: TagLibraryValidatorクラスのロード又はインスタンス化に失敗しました: org.apache.taglibs.standard.tlv.JstlCoreTLV 」と 500 エラーになる。 

これも結構嵌まったが、どうも pom.xml の provided の jar を読み込んでいる模様。
WTP 実行時に、pom.xml の povided を認識しないらしい。

検索してみると m2e-wtp なるものを発見。インストールしてみる。
「ヘルプ」->「Eclipse マーケットプレース」検索してインストール。
プロジェクトを右クリックして 「Maven」 -> 「プロジェクト構成の更新」を実行後、起動すると無事「Hello!」が表示された。

プラグインをインストールしたことでかなり飛ばせる手順がありそうだが、次回もこの手順でやれば同じ結果になるだろう・・・

2011年11月14日月曜日

Jericho HTML で一部サイトが文字化け

前回のソースで一部サイトが文字化けを起こした。

Souce のコンストラクタに URLを渡した場合は、ヘッダー情報の Content-Type の charset からエンコードが取得できるかもだが(Jericho が取得しているかは未調査)、BODY 部分のストリームを渡す場合、取得出来ないため内容から自動判別すようで、そこがおかしくなっているようだった。
なので指定してやることに、
HttpClient http = new DefaultHttpClient();
HttpResponse res = http.execute(new HttpGet(URI.create("http://www.example.com")));
if (res.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
{
  LOG.error("Failed to load uri.");
  return false;
}    

InputStream in = res.getEntity().getContent();
if (res.getHeaders("Content-Encoding").length > 0 &&
  "gzip".equals(res.getHeaders("Content-Encoding")[0].getValue()))
  in = new GZIPInputStream(in);

String charset = EntityUtils.getContentCharSet(res.getEntity());

Source source = null;
if (charset != null) source = new Source(new InputStreamReader(in, charset));
else source = new Source(in);
14 行目で Content-Type の charset からエンコードを取得して、指定があれば、17 行明で明示的に指定してやることに。直接渡しているので、Shift_JIS 等で部分的に文字化けがおきそうだがとりあえず。

Source(final Reader reader) の実装が、
this(reader,(reader instanceof InputStreamReader) ? ((InputStreamReader)reader).getEncoding() : null);
となっていて、instanceof が気に入らない。Source(final Reader reader, final String encoding) が public だといいのだが。

Content-Type の charset がなければ今まで通り自動判別。

もっとスマートにいかないものか・・・

2011年11月2日水曜日

Jericho HTML で Content-Encoding が gzip なサイトのパース

Jericho HTML でパースできないサイトがあるようで、調べてみると、Content-Encoding が gzip になっていた。
元ソース。
Source source = new Source(new URL("http://www.example.com"));

Accept-Encoding からはずせばいいのかもしれないが、HttpClient も使用しているため、そちらで対応することに。
HttpClient http = new DefaultHttpClient();
HttpResponse res = http.execute(new HttpGet(URI.create("http://www.example.com")));
if (res.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
{
  LOG.error("Failed to load uri.");
  return false;
}    

InputStream in = res.getEntity().getContent();
if (res.getHeaders("Content-Encoding").length > 0 &&
  "gzip".equals(res.getHeaders("Content-Encoding")[0].getValue()))
  in = new GZIPInputStream(in);
Source source = new Source(in);
Jericho HTML だけで対処出来る方法があればいいのだが。
compress, deflate は未対応・・・

文字化けしたので、続き

2011年10月23日日曜日

オーバーライド時のアクセス修飾子変更 Java 編

PHP 編 に続き、今度は Java で検証。

まず、親クラス。
class ParentClass
{
  private void func1() { System.out.println("Parent private func1"); }
  protected void func2() { System.out.println("Parent protected func2"); }
  public void func3() { System.out.println("Parent public func3"); }

  public void call()
  {
    this.func1();
    this.func2();
    this.func3();
  }
}
private, protected, public の 3 種類のメソッドと、それらを呼び出すメソッドを作成。

子クラス1 は public に変更。
class Child1Class extends ParentClass
{
  // メソッドはスーパータイプのメソッドをオーバーライドまたは実装しません
  // @Override
  public void func1() { System.out.println("Child1 private -> public func1"); }
  @Override
  public void func2() { System.out.println("Child1 protected -> public func2"); }
  @Override
  public void func3() { System.out.println("Child1 public func3"); }
}
func1 は private メソッドをオーバーライド出来ないため、コンパイルエラーとなる。
@Override アノテーションを外すと、コンパイルは通る。

子クラス2 は protected に変更。
class Child2Class extends ParentClass
{
  // メソッドはスーパータイプのメソッドをオーバーライドまたは実装しません
  // @Override
  protected void func1() { System.out.println("Child2 private -> protected func1"); }
  @Override
  protected void func2() { System.out.println("Child2 protected func2"); }
  // Child2Class の func3() は ParentClass の func3() をオーバーライドできません。スーパークラスでの定義(public) より弱いアクセス特権を割り当てようとしました。
  // @Override
  // protected void func3() { System.out.println("Child2 public -> protected func3"); }
}
func1 は同じようにエラー。
func3 は PHP と同じように、public -> private への変更でエラーとなる。func1 とは異なり、アノテーションを外しただけではエラー。定義自体を削除。

子クラス3 は private に変更。
aclass Child3Class extends ParentClass
{
  // メソッドはスーパータイプのメソッドをオーバーライドまたは実装しません
  // @Override
  private void func1() { System.out.println("Child3 private func1"); }
  // Child3Class の func2() は ParentClass の func2() をオーバーライドできません。スーパークラスでの定義(protected) より弱いアクセス特権を割り当てようとしました。
  // @Override
  // private void func2() { System.out.println("Child3 protected -> private func2"); }
  // Child3Class の func3() は ParentClass の func3() をオーバーライドできません。スーパークラスでの定義(public) より弱いアクセス特権を割り当てようとしました。
  // @Override
  // private void func3() { System.out.println("Child3 public -> private func3"); }
}

func1 は同じようにエラー。
func2, func3 でエラーとなる。やはり、厳しい方向への変更はできないようだ。

実行してみると、
ParentClass p = new ParentClass();
p.call();
System.out.println("----------");
Child1Class c1 = new Child1Class();
c1.call();
System.out.println("----------");
Child2Class c2 = new Child2Class();
c2.call();
System.out.println("----------");
Child3Class c3 = new Child3Class();
c3.call();

結果は、
Parent private func1
Parent protected func2
Parent public func3
----------
Parent private func1
Child1 protected -> public func2
Child1 public func3
----------
Parent private func1
Child2 protected func2
Parent public func3
----------
Parent private func1
Parent protected func2
Parent public func3

エラー部分を削除したソース、結果共に、PHP と同じようになった。

protected -> public への変更はできるが、public -> protected -> private への変更はできない。
private はオーバーライドできない。

余談だが、コンパイルエラーとなるのはやはりいい。