4 * License : The MIT License
5 * Copyright(c) 2008 olyutorskii
8 package jp.sfjp.jindolf.net;
10 import java.awt.image.BufferedImage;
11 import java.io.IOException;
12 import java.io.InputStream;
13 import java.io.OutputStream;
14 import java.io.UnsupportedEncodingException;
15 import java.lang.ref.SoftReference;
16 import java.net.HttpURLConnection;
17 import java.net.MalformedURLException;
18 import java.net.Proxy;
20 import java.net.URLEncoder;
21 import java.nio.charset.Charset;
22 import java.util.Collections;
23 import java.util.HashMap;
25 import java.util.logging.Logger;
26 import javax.imageio.ImageIO;
27 import jp.sfjp.jindolf.data.Period;
28 import jp.sfjp.jindolf.data.Village;
29 import jp.sourceforge.jindolf.parser.ContentBuilder;
30 import jp.sourceforge.jindolf.parser.ContentBuilderSJ;
31 import jp.sourceforge.jindolf.parser.ContentBuilderUCS2;
32 import jp.sourceforge.jindolf.parser.DecodeException;
33 import jp.sourceforge.jindolf.parser.DecodedContent;
34 import jp.sourceforge.jindolf.parser.SjisDecoder;
35 import jp.sourceforge.jindolf.parser.StreamDecoder;
38 * 国ごとの人狼BBSサーバとの通信を一手に引き受ける。
40 public class ServerAccess{
42 private static final String USER_AGENT = HttpUtils.getUserAgentName();
43 private static final String JINRO_CGI = "./index.rb";
45 Map<String, SoftReference<BufferedImage>> IMAGE_CACHE;
47 private static final Logger LOGGER = Logger.getAnonymousLogger();
50 Map<String, SoftReference<BufferedImage>> cache =
51 new HashMap<String, SoftReference<BufferedImage>>();
52 IMAGE_CACHE = Collections.synchronizedMap(cache);
56 private final URL baseURL;
57 private final Charset charset;
58 private Proxy proxy = Proxy.NO_PROXY;
59 private long lastServerMs;
60 private long lastLocalMs;
61 private long lastSystemMs;
62 private AccountCookie cookieAuth = null;
63 private String encodedUserID = null;
67 * 人狼BBSサーバとの接続管理を生成する。
69 * @param baseURL 国別のベースURL
70 * @param charset 国のCharset
72 public ServerAccess(URL baseURL, Charset charset){
73 this.baseURL = baseURL;
74 this.charset = charset;
82 * @return キャッシュされた画像。キャッシュされていなければnull。
84 private static BufferedImage getImageCache(String key){
85 if(key == null) return null;
89 synchronized(IMAGE_CACHE){
90 SoftReference<BufferedImage> ref = IMAGE_CACHE.get(key);
91 if(ref == null) return null;
93 Object referent = ref.get();
95 IMAGE_CACHE.remove(key);
99 image = (BufferedImage) referent;
108 * @param image キャッシュしたい画像。
110 private static void putImageCache(String key, BufferedImage image){
111 if(key == null || image == null) return;
113 synchronized(IMAGE_CACHE){
114 if(getImageCache(key) != null) return;
115 SoftReference<BufferedImage> ref =
116 new SoftReference<BufferedImage>(image);
117 IMAGE_CACHE.put(key, ref);
124 * 与えられた文字列に対し「application/x-www-form-urlencoded」符号化を行う。
125 * この符号化はHTTPのPOSTメソッドで必要になる。
126 * この処理は、一般的なPC用Webブラウザにおける、
127 * Shift_JISで書かれたHTML文書のFORMタグに伴う
129 * @param formData 元の文字列
132 public static String formEncode(String formData){
133 if(formData == null){
138 result = URLEncoder.encode(formData, "US-ASCII");
139 }catch(UnsupportedEncodingException e){
148 * @param formData 元の文字列
151 public static String formEncode(char[] formData){
152 return formEncode(new String(formData));
159 public Proxy getProxy(){
165 * @param proxy HTTP-Proxy。nullならProxyなしと解釈される。
167 public void setProxy(Proxy proxy){
168 if(proxy == null) this.proxy = Proxy.NO_PROXY;
169 else this.proxy = proxy;
177 public URL getBaseURL(){
182 * 与えられたクエリーとCGIのURLから新たにURLを合成する。
186 protected URL getQueryURL(String query){
187 if(query.length() >= 1 && query.charAt(0) != '?'){
193 result = new URL(getBaseURL(), JINRO_CGI + query);
194 }catch(MalformedURLException e){
202 * 「Shift_JIS」でエンコーディングされた入力ストリームから文字列を生成する。
203 * @param istream 入力ストリーム
205 * @throws java.io.IOException 入出力エラー(おそらくネットワーク関連)
207 public DecodedContent downloadHTMLStream(InputStream istream)
209 StreamDecoder decoder;
210 ContentBuilder builder;
211 if(this.charset.name().equalsIgnoreCase("Shift_JIS")){
212 decoder = new SjisDecoder();
213 builder = new ContentBuilderSJ(200 * 1024);
214 }else if(this.charset.name().equalsIgnoreCase("UTF-8")){
215 decoder = new StreamDecoder(this.charset.newDecoder());
216 builder = new ContentBuilderUCS2(200 * 1024);
221 decoder.setDecodeHandler(builder);
223 // TODO デコーダをインスタンス変数にできないか。
224 // TODO DecodedContentのキャッシュ管理。
227 decoder.decode(istream);
228 }catch(DecodeException e){
232 return builder.getContent();
236 * 与えられたクエリーを用いてHTMLデータを取得する。
237 * @param query HTTP-GET クエリー
239 * @throws java.io.IOException ネットワークエラー
241 protected HtmlSequence downloadHTML(String query)
243 URL url = getQueryURL(query);
244 HtmlSequence result = downloadHTML(url);
249 * 与えられたURLを用いてHTMLデータを取得する。
252 * @throws java.io.IOException ネットワークエラー
254 protected HtmlSequence downloadHTML(URL url)
256 HttpURLConnection connection =
257 (HttpURLConnection) url.openConnection(this.proxy);
258 connection.setRequestProperty("Accept", "*/*");
259 connection.setRequestProperty("User-Agent", USER_AGENT);
260 connection.setUseCaches(false);
261 connection.setInstanceFollowRedirects(false);
262 connection.setDoInput(true);
263 connection.setRequestMethod("GET");
265 AccountCookie cookie = this.cookieAuth;
267 if(shouldAccept(url, cookie)){
268 connection.setRequestProperty(
270 "login=" + cookie.getLoginData());
272 clearAuthentication();
276 connection.connect();
278 long datems = updateLastAccess(connection);
280 int responseCode = connection.getResponseCode();
281 if(responseCode != HttpURLConnection.HTTP_OK){ // 200
282 String logMessage = "発言のダウンロードに失敗しました。";
283 logMessage += HttpUtils.formatHttpStat(connection, 0, 0);
284 LOGGER.warning(logMessage);
288 String cs = HttpUtils.getHTMLCharset(connection);
289 if(!cs.equalsIgnoreCase(this.charset.name())){
293 InputStream stream = TallyInputStream.getInputStream(connection);
294 DecodedContent html = downloadHTMLStream(stream);
297 connection.disconnect();
299 HtmlSequence hseq = new HtmlSequence(url, datems, html);
305 * 絶対または相対URLの指すパーマネントなイメージ画像をダウンロードする。
306 * @param url 画像URL文字列
308 * @throws java.io.IOException ネットワークエラー
310 public BufferedImage downloadImage(String url) throws IOException{
313 URL base = getBaseURL();
314 absolute = new URL(base, url);
315 }catch(MalformedURLException e){
321 image = getImageCache(absolute.toString());
322 if(image != null) return image;
324 HttpURLConnection connection =
325 (HttpURLConnection) absolute.openConnection(this.proxy);
326 connection.setRequestProperty("Accept", "*/*");
327 connection.setRequestProperty("User-Agent", USER_AGENT);
328 connection.setUseCaches(true);
329 connection.setInstanceFollowRedirects(true);
330 connection.setDoInput(true);
331 connection.setRequestMethod("GET");
333 connection.connect();
335 int responseCode = connection.getResponseCode();
336 if(responseCode != HttpURLConnection.HTTP_OK){
337 String logMessage = "イメージのダウンロードに失敗しました。";
338 logMessage += HttpUtils.formatHttpStat(connection, 0, 0);
339 LOGGER.warning(logMessage);
343 InputStream stream = TallyInputStream.getInputStream(connection);
344 image = ImageIO.read(stream);
347 connection.disconnect();
349 putImageCache(absolute.toString(), image);
356 * @param authData 認証情報
357 * @return 認証情報が受け入れられたらtrue
358 * @throws java.io.IOException ネットワークエラー
360 protected boolean postAuthData(String authData) throws IOException{
361 URL url = getQueryURL("");
362 HttpURLConnection connection =
363 (HttpURLConnection) url.openConnection(this.proxy);
364 connection.setRequestProperty("Accept", "*/*");
365 connection.setRequestProperty("User-Agent", USER_AGENT);
366 connection.setUseCaches(false);
367 connection.setInstanceFollowRedirects(false);
368 connection.setDoInput(true);
369 connection.setDoOutput(true);
370 connection.setRequestMethod("POST");
372 byte[] authBytes = authData.getBytes();
374 OutputStream os = TallyOutputStream.getOutputStream(connection);
379 updateLastAccess(connection);
381 int responseCode = connection.getResponseCode();
382 if(responseCode != HttpURLConnection.HTTP_MOVED_TEMP){ // 302
383 String logMessage = "認証情報の送信に失敗しました。";
384 LOGGER.warning(logMessage);
385 connection.disconnect();
389 connection.disconnect();
391 AccountCookie loginCookie = AccountCookie.createCookie(connection);
392 if(loginCookie == null){
396 setAuthentication(loginCookie);
398 LOGGER.info("正しく認証が行われました。");
404 * トップページのHTMLデータを取得する。
406 * @throws java.io.IOException ネットワークエラー
408 public HtmlSequence getHTMLTopPage() throws IOException{
409 return downloadHTML("");
413 * 国に含まれる村一覧HTMLデータを取得する。
415 * @throws java.io.IOException ネットワークエラー
417 public HtmlSequence getHTMLLandList() throws IOException{
418 return downloadHTML("?cmd=log");
422 * 指定された村のPeriod一覧のHTMLデータを取得する。
427 * @throws java.io.IOException ネットワークエラー
429 public HtmlSequence getHTMLBoneHead(Village village) throws IOException{
430 String villageID = village.getVillageID();
431 return downloadHTML("?vid=" + villageID + "&meslog=");
435 * 指定された村の最新PeriodのHTMLデータをロードする。
436 * 既にGAMEOVERの村ではPeriod一覧のHTMLデータとなる。
439 * @throws java.io.IOException ネットワークエラー
441 public HtmlSequence getHTMLVillage(Village village) throws IOException{
442 URL url = getVillageURL(village);
443 return downloadHTML(url);
447 * 指定された村の最新PeriodのHTMLデータのURLを取得する。
451 public URL getVillageURL(Village village){
452 String villageID = village.getVillageID();
453 URL url = getQueryURL("?vid=" + villageID);
458 * 指定されたPeriodのHTMLデータをロードする。
459 * @param period Period
461 * @throws java.io.IOException ネットワークエラー
463 public HtmlSequence getHTMLPeriod(Period period) throws IOException{
464 URL url = getPeriodURL(period);
465 return downloadHTML(url);
469 * 指定されたPeriodのHTMLデータのURLを取得する。
473 public URL getPeriodURL(Period period){
474 String query = period.getCGIQuery();
475 URL url = getQueryURL(query);
481 * @param connection HTTP接続
484 public long updateLastAccess(HttpURLConnection connection){
485 this.lastServerMs = connection.getDate();
486 this.lastLocalMs = System.currentTimeMillis();
487 this.lastSystemMs = System.nanoTime() / (1000 * 1000);
488 return this.lastServerMs;
492 * 指定したURLに対しCookieを送っても良いか否か判定する。
493 * 判別材料は Cookie の寿命とパス指定のみ。
495 * @param cookie Cookie
496 * @return 送ってもよければtrue
498 private static boolean shouldAccept(URL url, AccountCookie cookie){
499 if(cookie.hasExpired()){
503 String urlPath = url.getPath();
504 String cookiePath = cookie.getPathURI().getPath();
506 if( ! urlPath.startsWith(cookiePath) ){
515 * @return ログイン中ならtrue
517 // TODO interval call
518 public boolean hasLoggedIn(){
519 AccountCookie cookie = this.cookieAuth;
523 if(cookie.hasExpired()){
524 clearAuthentication();
531 * 与えられたユーザIDとパスワードでログイン処理を行う。
532 * @param userID ユーザID
533 * @param password パスワード
534 * @return ログインに成功すればtrue
535 * @throws java.io.IOException ネットワークエラー
537 public final boolean login(String userID, char[] password)
543 String id = formEncode(userID);
544 if(id == null || id.length() <= 0){
548 String pw = formEncode(password);
549 if(pw == null || pw.length() <= 0){
553 this.encodedUserID = id;
555 String redirect = formEncode("&#bottom"); // TODO ほんとに必要?
557 StringBuilder postData = new StringBuilder();
558 postData.append("cmd=login");
559 postData.append('&').append("cgi_param=").append(redirect);
560 postData.append('&').append("user_id=").append(id);
561 postData.append('&').append("password=").append(pw);
565 result = postAuthData(postData.toString());
566 }catch(IOException e){
567 clearAuthentication();
576 * @throws java.io.IOException ネットワーク入出力エラー
578 // TODO シャットダウンフックでログアウトさせようかな…
579 public void logout() throws IOException{
583 if(this.encodedUserID == null){
584 clearAuthentication();
588 String redirect = formEncode("&#bottom"); // TODO 必要?
590 StringBuilder postData = new StringBuilder();
591 postData.append("cmd=logout");
592 postData.append('&').append("cgi_param=").append(redirect);
593 postData.append('&').append("user_id=").append(this.encodedUserID);
596 postAuthData(postData.toString());
598 clearAuthentication();
607 // TODO タイマーでExpire date の時刻にクリアしたい。
608 protected void clearAuthentication(){
609 this.cookieAuth = null;
610 this.encodedUserID = null;
616 * @param cookie 認証Cookie
618 private void setAuthentication(AccountCookie cookie){
619 this.cookieAuth = cookie;
623 // TODO JRE1.6対応するときに HttpCookie, CookieManager 利用へ移行したい。