OSDN Git Service

Merge release/v3.303.106
[jindolf/Jindolf.git] / src / main / java / jp / sfjp / jindolf / net / ServerAccess.java
1 /*
2  * manage HTTP access
3  *
4  * License : The MIT License
5  * Copyright(c) 2008 olyutorskii
6  */
7
8 package jp.sfjp.jindolf.net;
9
10 import io.bitbucket.olyutorskii.jiocema.DecodeBreakException;
11 import io.bitbucket.olyutorskii.jiocema.DecodeNotifier;
12 import java.awt.image.BufferedImage;
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.io.OutputStream;
16 import java.lang.ref.SoftReference;
17 import java.net.HttpURLConnection;
18 import java.net.MalformedURLException;
19 import java.net.Proxy;
20 import java.net.URL;
21 import java.nio.charset.Charset;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.logging.Logger;
26 import javax.imageio.ImageIO;
27 import jp.osdn.jindolf.parser.content.ContentBuilder;
28 import jp.osdn.jindolf.parser.content.ContentBuilderSJ;
29 import jp.osdn.jindolf.parser.content.DecodedContent;
30 import jp.osdn.jindolf.parser.content.SjisNotifier;
31 import jp.sfjp.jindolf.data.Period;
32 import jp.sfjp.jindolf.data.Village;
33
34 /**
35  * 国ごとの人狼BBSサーバとの通信を一手に引き受ける。
36  */
37 public class ServerAccess{
38
39     private static final String USER_AGENT = HttpUtils.getUserAgentName();
40     private static final String JINRO_CGI = "./index.rb";
41     private static final
42             Map<String, SoftReference<BufferedImage>> IMAGE_CACHE;
43
44     private static final Logger LOGGER = Logger.getAnonymousLogger();
45     private static final String ENC_POST = "UTF-8";
46
47     static{
48         Map<String, SoftReference<BufferedImage>> cache =
49                 new HashMap<>();
50         IMAGE_CACHE = Collections.synchronizedMap(cache);
51     }
52
53
54     private final URL baseURL;
55     private final AuthManager authManager;
56
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
63
64     /**
65      * 人狼BBSサーバとの接続管理を生成する。
66      * この時点ではまだ通信は行われない。
67      * @param baseURL 国別のベースURL
68      * @param charset 国のCharset
69      * @throws IllegalArgumentException 不正なURL
70      */
71     public ServerAccess(URL baseURL, Charset charset)
72             throws IllegalArgumentException{
73         super();
74
75         this.baseURL = baseURL;
76         this.authManager = new AuthManager(this.baseURL);
77         this.charset = charset;
78
79         return;
80     }
81
82
83     /**
84      * 画像キャッシュを検索する。
85      * @param key キー
86      * @return キャッシュされた画像。キャッシュされていなければnull。
87      */
88     private static BufferedImage getImageCache(String key){
89         if(key == null) return null;
90
91         BufferedImage image;
92
93         synchronized(IMAGE_CACHE){
94             SoftReference<BufferedImage> ref = IMAGE_CACHE.get(key);
95             if(ref == null) return null;
96
97             Object referent = ref.get();
98             if(referent == null){
99                 IMAGE_CACHE.remove(key);
100                 return null;
101             }
102
103             image = (BufferedImage) referent;
104         }
105
106         return image;
107     }
108
109     /**
110      * 画像キャッシュに登録する。
111      * @param key キー
112      * @param image キャッシュしたい画像。
113      */
114     private static void putImageCache(String key, BufferedImage image){
115         if(key == null || image == null) return;
116
117         synchronized(IMAGE_CACHE){
118             if(getImageCache(key) != null) return;
119             SoftReference<BufferedImage> ref =
120                     new SoftReference<>(image);
121             IMAGE_CACHE.put(key, ref);
122         }
123
124         return;
125     }
126
127     /**
128      * HTTP-Proxyを返す。
129      * @return HTTP-Proxy
130      */
131     public Proxy getProxy(){
132         return this.proxy;
133     }
134
135     /**
136      * HTTP-Proxyを設定する。
137      * @param proxy HTTP-Proxy。nullならProxyなしと解釈される。
138      */
139     public void setProxy(Proxy proxy){
140         if(proxy == null) this.proxy = Proxy.NO_PROXY;
141         else              this.proxy = proxy;
142         return;
143     }
144
145     /**
146      * 国のベースURLを返す。
147      * @return ベースURL
148      */
149     public URL getBaseURL(){
150         return this.baseURL;
151     }
152
153     /**
154      * 与えられたクエリーとCGIのURLから新たにURLを合成する。
155      * @param query クエリー
156      * @return 新たなURL
157      */
158     protected URL getQueryURL(String query){
159         if(query.length() >= 1 && query.charAt(0) != '?'){
160             return null;
161         }
162
163         URL result;
164         try{
165             result = new URL(getBaseURL(), JINRO_CGI + query);
166         }catch(MalformedURLException e){
167             assert false;
168             return null;
169         }
170         return result;
171     }
172
173     /**
174      * エンコーディングされた入力ストリームから文字列を生成する。
175      * @param istream 入力ストリーム
176      * @return 文字列
177      * @throws java.io.IOException 入出力エラー(おそらくネットワーク関連)
178      */
179     public DecodedContent downloadHTMLStream(InputStream istream)
180             throws IOException{
181         DecodeNotifier decoder;
182         ContentBuilder builder;
183         if(this.charset.name().equalsIgnoreCase("Shift_JIS")){
184             decoder = new SjisNotifier();
185             builder = new ContentBuilderSJ(200 * 1024);
186         }else if(this.charset.name().equalsIgnoreCase("UTF-8")){
187             decoder = new DecodeNotifier(this.charset.newDecoder());
188             builder = new ContentBuilder(200 * 1024);
189         }else{
190             assert false;
191             return null;
192         }
193         decoder.setCharDecodeListener(builder);
194
195         // TODO デコーダをインスタンス変数にできないか。
196         // TODO DecodedContentのキャッシュ管理。
197
198         try{
199             decoder.decode(istream);
200         }catch(DecodeBreakException e){
201             return null;
202         }
203
204         return builder.getContent();
205     }
206
207     /**
208      * 与えられたクエリーを用いてHTMLデータを取得する。
209      * @param query HTTP-GET クエリー
210      * @return HTMLデータ
211      * @throws java.io.IOException ネットワークエラー
212      */
213     protected HtmlSequence downloadHTML(String query)
214             throws IOException{
215         URL url = getQueryURL(query);
216         HtmlSequence result = downloadHTML(url);
217         return result;
218     }
219
220     /**
221      * 与えられたURLを用いてHTMLデータを取得する。
222      * @param url URL
223      * @return HTMLデータ
224      * @throws java.io.IOException ネットワークエラー
225      */
226     protected HtmlSequence downloadHTML(URL url)
227             throws IOException{
228         HttpURLConnection connection =
229                 (HttpURLConnection) url.openConnection(this.proxy);
230         connection.setRequestProperty("Accept", "*/*");
231         connection.setRequestProperty("User-Agent", USER_AGENT);
232         connection.setUseCaches(false);
233         connection.setInstanceFollowRedirects(false);
234         connection.setDoInput(true);
235         connection.setRequestMethod("GET");
236
237         connection.connect();
238
239         long datems = updateLastAccess(connection);
240
241         int responseCode = connection.getResponseCode();
242         if(responseCode != HttpURLConnection.HTTP_OK){ // 200
243             String logMessage =  "発言のダウンロードに失敗しました。";
244             logMessage += HttpUtils.formatHttpStat(connection, 0, 0);
245             LOGGER.warning(logMessage);
246             return null;
247         }
248
249         String cs = HttpUtils.getHTMLCharset(connection);
250         if(!cs.equalsIgnoreCase(this.charset.name())){
251             return null;
252         }
253
254         InputStream stream = TallyInputStream.getInputStream(connection);
255         DecodedContent html = downloadHTMLStream(stream);
256
257         stream.close();
258         connection.disconnect();
259
260         HtmlSequence hseq = new HtmlSequence(url, datems, html);
261
262         return hseq;
263     }
264
265     /**
266      * 絶対または相対URLの指すパーマネントなイメージ画像をダウンロードする。
267      * @param url 画像URL文字列
268      * @return 画像イメージ
269      * @throws java.io.IOException ネットワークエラー
270      */
271     public BufferedImage downloadImage(String url) throws IOException{
272         URL absolute;
273         try{
274             URL base = getBaseURL();
275             absolute = new URL(base, url);
276         }catch(MalformedURLException e){
277             assert false;
278             return null;
279         }
280
281         BufferedImage image;
282         image = getImageCache(absolute.toString());
283         if(image != null) return image;
284
285         HttpURLConnection connection =
286                 (HttpURLConnection) absolute.openConnection(this.proxy);
287         connection.setRequestProperty("Accept", "*/*");
288         connection.setRequestProperty("User-Agent", USER_AGENT);
289         connection.setUseCaches(true);
290         connection.setInstanceFollowRedirects(true);
291         connection.setDoInput(true);
292         connection.setRequestMethod("GET");
293
294         connection.connect();
295
296         int responseCode       = connection.getResponseCode();
297         if(responseCode != HttpURLConnection.HTTP_OK){
298             String logMessage =  "イメージのダウンロードに失敗しました。";
299             logMessage += HttpUtils.formatHttpStat(connection, 0, 0);
300             LOGGER.warning(logMessage);
301             return null;
302         }
303
304         InputStream stream = TallyInputStream.getInputStream(connection);
305         image = ImageIO.read(stream);
306         stream.close();
307
308         connection.disconnect();
309
310         putImageCache(absolute.toString(), image);
311
312         return image;
313     }
314
315     /**
316      * 指定された認証情報をPOSTする。
317      * @param authData 認証情報
318      * @return 認証情報が受け入れられたらtrue
319      * @throws java.io.IOException ネットワークエラー
320      */
321     protected boolean postAuthData(String authData) throws IOException{
322         URL url = getQueryURL("");
323         HttpURLConnection connection =
324                 (HttpURLConnection) url.openConnection(this.proxy);
325         connection.setRequestProperty("Accept", "*/*");
326         connection.setRequestProperty("User-Agent", USER_AGENT);
327         connection.setUseCaches(false);
328         connection.setInstanceFollowRedirects(false);
329         connection.setDoInput(true);
330         connection.setDoOutput(true);
331         connection.setRequestMethod("POST");
332
333         byte[] authBytes = authData.getBytes(ENC_POST);
334
335         OutputStream os = TallyOutputStream.getOutputStream(connection);
336         os.write(authBytes);
337         os.flush();
338         os.close();
339
340         updateLastAccess(connection);
341
342         connection.disconnect();
343
344         if( ! this.authManager.hasLoggedIn() ){
345             String logMessage =  "認証情報の送信に失敗しました。";
346             LOGGER.warning(logMessage);
347             return false;
348         }
349
350         LOGGER.info("正しく認証が行われました。");
351
352         return true;
353     }
354
355     /**
356      * トップページのHTMLデータを取得する。
357      * @return HTMLデータ
358      * @throws java.io.IOException ネットワークエラー
359      */
360     public HtmlSequence getHTMLTopPage() throws IOException{
361         return downloadHTML("");
362     }
363
364     /**
365      * 国に含まれる村一覧HTMLデータを取得する。
366      * @return HTMLデータ
367      * @throws java.io.IOException ネットワークエラー
368      */
369     public HtmlSequence getHTMLLandList() throws IOException{
370         return downloadHTML("?cmd=log");
371     }
372
373     /**
374      * 指定された村のPeriod一覧のHTMLデータを取得する。
375      * 現在ゲーム進行中の村にも可能。
376      * ※ 古国では使えないよ!
377      * @param village 村
378      * @return HTMLデータ
379      * @throws java.io.IOException ネットワークエラー
380      */
381     public HtmlSequence getHTMLBoneHead(Village village) throws IOException{
382         String villageID = village.getVillageID();
383         return downloadHTML("?vid=" + villageID + "&meslog=");
384     }
385
386     /**
387      * 指定された村の最新PeriodのHTMLデータをロードする。
388      * 既にGAMEOVERの村ではPeriod一覧のHTMLデータとなる。
389      * @param village 村
390      * @return HTMLデータ
391      * @throws java.io.IOException ネットワークエラー
392      */
393     public HtmlSequence getHTMLVillage(Village village) throws IOException{
394         URL url = getVillageURL(village);
395         return downloadHTML(url);
396     }
397
398     /**
399      * 指定された村の最新PeriodのHTMLデータのURLを取得する。
400      * @param village 村
401      * @return URL
402      */
403     public URL getVillageURL(Village village){
404         String villageID = village.getVillageID();
405         URL url = getQueryURL("?vid=" + villageID);
406         return url;
407     }
408
409     /**
410      * 指定されたPeriodのHTMLデータをロードする。
411      * @param period Period
412      * @return HTMLデータ
413      * @throws java.io.IOException ネットワークエラー
414      */
415     public HtmlSequence getHTMLPeriod(Period period) throws IOException{
416         URL url = getPeriodURL(period);
417         return downloadHTML(url);
418     }
419
420     /**
421      * 指定されたPeriodのHTMLデータのURLを取得する。
422      * @param period 日
423      * @return URL
424      */
425     public URL getPeriodURL(Period period){
426         String query = period.getCGIQuery();
427         URL url = getQueryURL(query);
428         return url;
429     }
430
431     /**
432      * 最終アクセス時刻を更新する。
433      * @param connection HTTP接続
434      * @return リソース送信時刻
435      */
436     public long updateLastAccess(HttpURLConnection connection){
437         this.lastServerMs = connection.getDate();
438         this.lastLocalMs = System.currentTimeMillis();
439         this.lastSystemMs = System.nanoTime() / (1000 * 1000);
440         return this.lastServerMs;
441     }
442
443     /**
444      * 与えられたユーザIDとパスワードでログイン処理を行う。
445      * @param userID ユーザID
446      * @param password パスワード
447      * @return ログインに成功すればtrue
448      * @throws java.io.IOException ネットワークエラー
449      */
450     public final boolean login(String userID, char[] password)
451             throws IOException{
452         if(this.authManager.hasLoggedIn()){
453             return true;
454         }
455
456         String postText = AuthManager.buildLoginPostData(userID, password);
457         boolean result;
458         try{
459             result = postAuthData(postText);
460         }catch(IOException e){
461             this.authManager.clearAuthentication();
462             throw e;
463         }
464
465         return result;
466     }
467
468     /**
469      * ログアウト処理を行う。
470      * @throws java.io.IOException ネットワーク入出力エラー
471      */
472     public void logout() throws IOException{
473         if( ! this.authManager.hasLoggedIn() ){
474             return;
475         }
476
477         try{
478             postAuthData(AuthManager.POST_LOGOUT);
479         }finally{
480             this.authManager.clearAuthentication();
481         }
482
483         return;
484     }
485     // TODO シャットダウンフックでログアウトさせようかな…
486
487     /**
488      * ログイン中か否か判定する。
489      * @return ログイン中ならtrue
490      */
491     public boolean hasLoggedIn(){
492         boolean result = this.authManager.hasLoggedIn();
493         return result;
494     }
495
496 }