4 * License : The MIT License
5 * Copyright(c) 2008 olyutorskii
8 package jp.sfjp.jindolf.net;
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;
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.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;
35 * 国ごとの人狼BBSサーバとの通信を一手に引き受ける。
37 public class ServerAccess{
39 private static final String USER_AGENT = HttpUtils.getUserAgentName();
40 private static final String JINRO_CGI = "./index.rb";
42 Map<String, SoftReference<BufferedImage>> IMAGE_CACHE;
44 private static final Logger LOGGER = Logger.getAnonymousLogger();
45 private static final String ENC_POST = "UTF-8";
48 Map<String, SoftReference<BufferedImage>> cache =
50 IMAGE_CACHE = Collections.synchronizedMap(cache);
54 private final URL baseURL;
55 private final AuthManager authManager;
57 private final Charset charset;
58 private Proxy proxy = Proxy.NO_PROXY;
59 private long lastServerMs;
60 private long lastLocalMs;
61 private long lastSystemMs;
65 * 人狼BBSサーバとの接続管理を生成する。
67 * @param baseURL 国別のベースURL
68 * @param charset 国のCharset
69 * @throws IllegalArgumentException 不正なURL
71 public ServerAccess(URL baseURL, Charset charset)
72 throws IllegalArgumentException{
75 this.baseURL = baseURL;
76 this.authManager = new AuthManager(this.baseURL);
77 this.charset = charset;
86 * @return キャッシュされた画像。キャッシュされていなければnull。
88 private static BufferedImage getImageCache(String key){
89 if(key == null) return null;
93 synchronized(IMAGE_CACHE){
94 SoftReference<BufferedImage> ref = IMAGE_CACHE.get(key);
95 if(ref == null) return null;
97 Object referent = ref.get();
99 IMAGE_CACHE.remove(key);
103 image = (BufferedImage) referent;
112 * @param image キャッシュしたい画像。
114 private static void putImageCache(String key, BufferedImage image){
115 if(key == null || image == null) return;
117 synchronized(IMAGE_CACHE){
118 if(getImageCache(key) != null) return;
119 SoftReference<BufferedImage> ref =
120 new SoftReference<>(image);
121 IMAGE_CACHE.put(key, ref);
131 public Proxy getProxy(){
137 * @param proxy HTTP-Proxy。nullならProxyなしと解釈される。
139 public void setProxy(Proxy proxy){
140 if(proxy == null) this.proxy = Proxy.NO_PROXY;
141 else this.proxy = proxy;
149 public URL getBaseURL(){
154 * 与えられたクエリーとCGIのURLから新たにURLを合成する。
158 protected URL getQueryURL(String query){
159 if(query.length() >= 1 && query.charAt(0) != '?'){
165 result = new URL(getBaseURL(), JINRO_CGI + query);
166 }catch(MalformedURLException e){
174 * エンコーディングされた入力ストリームから文字列を生成する。
175 * @param istream 入力ストリーム
177 * @throws java.io.IOException 入出力エラー(おそらくネットワーク関連)
179 public DecodedContent downloadHTMLStream(InputStream istream)
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);
193 decoder.setCharDecodeListener(builder);
195 // TODO デコーダをインスタンス変数にできないか。
196 // TODO DecodedContentのキャッシュ管理。
199 decoder.decode(istream);
200 }catch(DecodeBreakException e){
204 return builder.getContent();
208 * 与えられたクエリーを用いてHTMLデータを取得する。
209 * @param query HTTP-GET クエリー
211 * @throws java.io.IOException ネットワークエラー
213 protected HtmlSequence downloadHTML(String query)
215 URL url = getQueryURL(query);
216 HtmlSequence result = downloadHTML(url);
221 * 与えられたURLを用いてHTMLデータを取得する。
224 * @throws java.io.IOException ネットワークエラー
226 protected HtmlSequence downloadHTML(URL url)
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");
237 connection.connect();
239 long datems = updateLastAccess(connection);
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);
249 String cs = HttpUtils.getHTMLCharset(connection);
250 if(!cs.equalsIgnoreCase(this.charset.name())){
254 InputStream stream = TallyInputStream.getInputStream(connection);
255 DecodedContent html = downloadHTMLStream(stream);
258 connection.disconnect();
260 HtmlSequence hseq = new HtmlSequence(url, datems, html);
266 * 絶対または相対URLの指すパーマネントなイメージ画像をダウンロードする。
267 * @param url 画像URL文字列
269 * @throws java.io.IOException ネットワークエラー
271 public BufferedImage downloadImage(String url) throws IOException{
274 URL base = getBaseURL();
275 absolute = new URL(base, url);
276 }catch(MalformedURLException e){
282 image = getImageCache(absolute.toString());
283 if(image != null) return image;
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");
294 connection.connect();
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);
304 InputStream stream = TallyInputStream.getInputStream(connection);
305 image = ImageIO.read(stream);
308 connection.disconnect();
310 putImageCache(absolute.toString(), image);
317 * @param authData 認証情報
318 * @return 認証情報が受け入れられたらtrue
319 * @throws java.io.IOException ネットワークエラー
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");
333 byte[] authBytes = authData.getBytes(ENC_POST);
335 OutputStream os = TallyOutputStream.getOutputStream(connection);
340 updateLastAccess(connection);
342 connection.disconnect();
344 if( ! this.authManager.hasLoggedIn() ){
345 String logMessage = "認証情報の送信に失敗しました。";
346 LOGGER.warning(logMessage);
350 LOGGER.info("正しく認証が行われました。");
356 * トップページのHTMLデータを取得する。
358 * @throws java.io.IOException ネットワークエラー
360 public HtmlSequence getHTMLTopPage() throws IOException{
361 return downloadHTML("");
365 * 国に含まれる村一覧HTMLデータを取得する。
367 * @throws java.io.IOException ネットワークエラー
369 public HtmlSequence getHTMLLandList() throws IOException{
370 return downloadHTML("?cmd=log");
374 * 指定された村のPeriod一覧のHTMLデータを取得する。
379 * @throws java.io.IOException ネットワークエラー
381 public HtmlSequence getHTMLBoneHead(Village village) throws IOException{
382 String villageID = village.getVillageID();
383 return downloadHTML("?vid=" + villageID + "&meslog=");
387 * 指定された村の最新PeriodのHTMLデータをロードする。
388 * 既にGAMEOVERの村ではPeriod一覧のHTMLデータとなる。
391 * @throws java.io.IOException ネットワークエラー
393 public HtmlSequence getHTMLVillage(Village village) throws IOException{
394 URL url = getVillageURL(village);
395 return downloadHTML(url);
399 * 指定された村の最新PeriodのHTMLデータのURLを取得する。
403 public URL getVillageURL(Village village){
404 String villageID = village.getVillageID();
405 URL url = getQueryURL("?vid=" + villageID);
410 * 指定されたPeriodのHTMLデータをロードする。
411 * @param period Period
413 * @throws java.io.IOException ネットワークエラー
415 public HtmlSequence getHTMLPeriod(Period period) throws IOException{
416 URL url = getPeriodURL(period);
417 return downloadHTML(url);
421 * 指定されたPeriodのHTMLデータのURLを取得する。
425 public URL getPeriodURL(Period period){
426 String query = period.getCGIQuery();
427 URL url = getQueryURL(query);
433 * @param connection HTTP接続
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;
444 * 与えられたユーザIDとパスワードでログイン処理を行う。
445 * @param userID ユーザID
446 * @param password パスワード
447 * @return ログインに成功すればtrue
448 * @throws java.io.IOException ネットワークエラー
450 public final boolean login(String userID, char[] password)
452 if(this.authManager.hasLoggedIn()){
456 String postText = AuthManager.buildLoginPostData(userID, password);
459 result = postAuthData(postText);
460 }catch(IOException e){
461 this.authManager.clearAuthentication();
470 * @throws java.io.IOException ネットワーク入出力エラー
472 public void logout() throws IOException{
473 if( ! this.authManager.hasLoggedIn() ){
478 postAuthData(AuthManager.POST_LOGOUT);
480 this.authManager.clearAuthentication();
485 // TODO シャットダウンフックでログアウトさせようかな…
489 * @return ログイン中ならtrue
491 public boolean hasLoggedIn(){
492 boolean result = this.authManager.hasLoggedIn();