Java
gradle
バッチファイル
jsoup

[Java]jsoupでブラウザから時刻合わせ

要件

  • とあるアプリケーションで、時刻がすぐに狂ってしまう。
  • NTPへの接続はできない。社内NTPも不可。
  • アプリケーションはブラウザからIPアドレスでアクセスが出来る。
  • ブラウザ上から手動で時刻設定は可能。
  • 該当アプリケーションが動くサーバは、全国30台近く点在している。
  • サーバは今後増減可能性あり。
  • ログインパスワードはバッチ実行時に入力する(引数もしくはプロンプト)
  • Javaは社内標準で導入されている。それ以外の言語の導入は手続きが必要。

実行環境

  • Windows8.1, 10 (メインは8だが、将来的に10になる可能性大)
  • Java1.8以上

開発環境

  • Windows7
  • Eclipse
  • Java1.8
    • jsoup 1.10.2
  • gradle
    • applicationプラグインでbatファイルからjarを実行可能に設定

ソースコード

時刻合わせ起動バッチ

  • src\dist\以下に配置
TimeAdjusterMain.bat
@echo off

rem ------------------------------------------------
rem --- 時刻合わせバッチ
rem ------------------------------------------------

rem -- カレントディレクトリを基準とする
cd /d %~dp0

echo **** 時刻合わせ開始: %date% %time%

rem -- 引数をチェックする(xxを付ける事で空にならないようにする)
if xx%1==xx goto confirmPw
goto setPw



:confirmPw

rem -- 引数が指定されていない場合、管理者パスワードの入力
set /P PASSWD=管理者パスワードを入力してください: 

goto exec



:setPw

rem -- 引数が指定されている場合、管理者パスワードとして設定
set PASSWD=%1

goto exec



rem -- 処理バッチの実行
:exec

rem -- 処理バッチの実行開始
call bin\TimeAdjuster.bat %PASSWD%

echo **** 時刻合わせ終了: %date% %time%

rem -- 完了させずとどまる
pause

処理対象IPアドレスリストtsv

  • src\dist\以下に配置
list.tsv
10.20.30.40 abc123  ABC123  A支店 Bフロア  A-B#1
10.20.50.60 def456  DEF456  C支店 Dフロア  C-D#2

Javaソースコード

Main

TimeAdjusterMain.java
package jp.co.sample.timeadjuster;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jp.co.sample.timeadjuster.bean.SettingBean;
import jp.co.sample.timeadjuster.launcher.TimeAdjusterLauncher;

public class TimeAdjusterMain {

    private static Logger logger = LoggerFactory.getLogger(TimeAdjusterMain.class);

    public static void main(String[] args) {
        logger.info("TimeAdjusterMain 開始");

        // 設定情報
        SettingBean setting;
        try {
            // args[0]: 管理者パスワード
            // args[1]: src/distフォルダ絶対パス
            setting = new SettingBean(args[0], args[1], "list.tsv");
        } catch (Exception e) {
            logger.error("設定情報エラー", e);
            return;
        }

        // 処理実行
        TimeAdjusterLauncher launcher = new TimeAdjusterLauncher(setting);
        try {
            launcher.execute();
        } catch (Exception e) {
            logger.error("時刻合わせ処理エラー", e);
            return;
        }
    }
}

設定情報

SettingBean.java
package jp.co.sample.timeadjuster.bean;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SettingBean {

    public static Logger logger = LoggerFactory.getLogger(SettingBean.class);

    /**
     * 管理者パスワード
     */
    String passwd;

    /**
     * 実行パスの基準ディレクトリ
     */
    String basePath;

    /**
     * tsvファイル名
     */
    String tsvName;

    /**
     * サーバ情報リスト
     */
    ArrayList<ServerBean> serverList = new ArrayList<>();


    public SettingBean(String passwd, String basePath, String tsvName) throws IOException {
        this.passwd = passwd;

        this.basePath = basePath;

        logger.debug("this.basePath: "+ this.basePath);

        this.tsvName = tsvName;

        logger.debug("this.tsvName: "+ this.tsvName);

        File targetFile = new File(this.basePath, this.tsvName);
        if (!targetFile.isFile()) {
            logger.error("実在するファイルパスを指定して下さい。: "+ path);
            return;
        }

        // ファイルを読み込む
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(targetFile),"UTF-8"));) {
            // TAB区切りのファイルで、ヘッダあり。空白は除外する
            Iterable<CSVRecord> records = CSVFormat.TDF.withIgnoreEmptyLines().withIgnoreSurroundingSpaces().parse(reader);
                for (CSVRecord record : records) {

                    logger.debug(record.get(0));

                    // サーバのIPアドレスはIPアドレス型のチェックを入れる
                    // メモ帳でtsvファイルを作成すると、先頭に?が入ってしまうため
                    Pattern p = Pattern.compile("\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}");
                    Matcher m = p.matcher(record.get(0));

                    if (!m.find()) {
                        throw new RuntimeException("IPアドレスの指定方法が間違っています。 IPアドレス:"+ record.get(0));
                    }

                    // サーバ情報を取得し、リストに追加する
                    ServerBean server = new ServerBean(m.group(), record.get(1), record.get(2), record.get(3), record.get(4), record.get(5));
                    serverList.add(server);

                    logger.debug("server: "+ server.getIpaddress() + " / "+ server.getSerial_no() + " / "+ server.getDevice_name());
                }
        } catch (FileNotFoundException e) {
            logger.error("TSVファイル読み込みエラー", e);
            throw e;
        } catch (IOException e) {
            logger.error("TSVファイル読み込みエラー", e);
            throw e;
        }

    }


    public String getPasswd() {
        return passwd;
    }


    public void setPasswd(String passwd) {
        this.passwd = passwd;
    }


    public String getBasePath() {
        return basePath;
    }


    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }


    public String getTsvName() {
        return tsvName;
    }


    public void setTsvName(String tsvName) {
        this.tsvName = tsvName;
    }


    public ArrayList<ServerBean> getServerList() {
        return serverList;
    }


    public void setServerList(ArrayList<ServerBean> serverList) {
        this.serverList = serverList;
    }

}

サーバ情報(tsvから読み込んだ情報)

ServerBean.java
package jp.co.sample.timeadjuster.bean;

public class ServerBean {
    /**
     * IPアドレス
     */
    String ipaddress;

    /**
     * ホスト名
     */
    String host_name;

    /**
     * シリアル番号
     */
    String serial_no;

    /**
     * 拠点名
     */
    String base_name;


    /**
     * 設置場所
     */
    String installation_location;

    /**
     * 機器名称
     */
    String device_name;

    public ServerBean(String ipaddress, String host_name, String serial_no, String base_name,
            String installation_location, String device_name) {
        super();
        this.ipaddress = ipaddress;
        this.host_name = host_name;
        this.serial_no = serial_no;
        this.base_name = base_name;
        this.installation_location = installation_location;
        this.device_name = device_name;
    }

    public String getIpaddress() {
        return ipaddress;
    }

    public void setIpaddress(String ipaddress) {
        this.ipaddress = ipaddress;
    }

    public String getHost_name() {
        return host_name;
    }

    public void setHost_name(String host_name) {
        this.host_name = host_name;
    }

    public String getSerial_no() {
        return serial_no;
    }

    public void setSerial_no(String serial_no) {
        this.serial_no = serial_no;
    }

    public String getBase_name() {
        return base_name;
    }

    public void setBase_name(String base_name) {
        this.base_name = base_name;
    }

    public String getInstallation_location() {
        return installation_location;
    }

    public void setInstallation_location(String installation_location) {
        this.installation_location = installation_location;
    }

    public String getDevice_name() {
        return device_name;
    }

    public void setDevice_name(String device_name) {
        this.device_name = device_name;
    }

}

時刻合わせ処理

TimeAdjusterLauncher.java
package jp.co.sample.timeadjuster.launcher;

import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

import org.apache.commons.lang3.StringUtils;
import org.jsoup.Connection.Method;
import org.jsoup.Connection.Response;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jp.co.sample.timeadjuster.bean.ServerBean;
import jp.co.sample.timeadjuster.bean.SettingBean;

public class TimeAdjusterLauncher {

    public static Logger logger = LoggerFactory.getLogger(TimeAdjusterLauncher.class);

    /**
     * 設定情報
     */
    SettingBean setting;

    /**
     * ログイン画面表示結果
     * Cookie等を保持
     */
    Response loginRes;

    /**
     * コンストラクタ
     *
     * @param setting 設定情報
     */
    public TimeAdjusterLauncher(SettingBean setting) {
        super();

        // 設定を保持する
        this.setting = setting;
    }

    /**
     * 時刻合わせ処理を実行します
     * @throws RuntimeException
     * @throws IOException
     */
    public void execute() throws RuntimeException, IOException {

        // サーバ一台ごとに処理をループする
        for (ServerBean server : setting.getServerList()) {
            logger.info("時刻合わせ開始 ****************** ");
            logger.info("対象サーバ: "+ server.getIpaddress() + " / "+ server.getDevice_name());

            // ログイン
            login(server);

            // 現在設定時刻確認
            String zone = getTimer(server);

            // 新規時刻設定
            adjustTimer(server, zone);
        }

    }

    /**
     * ログイン処理
     * @throws RuntimeException
     * @throws IOException
     */
    private void login(ServerBean server) throws RuntimeException, IOException {
        // ログイン処理実行
        String loginUrl = "http://"+ server.getIpaddress() + "/logon.cgi";
        try {
            loginRes = Jsoup.connect(loginUrl)
                    .data("Password", setting.getPasswd())
                    .data("Mode", "Admin")
                    .data("Lang", "Japanese")
                    .method(Method.POST)
                    .execute();
        } catch (IOException e) {
            logger.error("初期画面ログインエラー", e);
            throw e;
        }

//      logger.debug(loginRes.parse().outerHtml());

        // HTTPステータス照合
        checkStatusCode(loginRes, loginUrl);


        if (StringUtils.contains(loginRes.parse().outerHtml(), "/pages/_top2.htm") ) {
            // ログインエラー画面にリダイレクトがかかっている場合、
            // 管理者ログインに失敗したという事なので、エラー終了
            throw new RuntimeException("管理者ログインに失敗しました。処理を中断します。");
        }


        // ログイン成功後、リダイレクトを行う
        // 実際には特に必要ない処理。確認のために行っただけ。
        String redirectUrl = "http://"+ server.getIpaddress() + "/pages/_devadm.htm";
        Response redirectRes;
        try {
            redirectRes = Jsoup.connect(redirectUrl)
                // ログイン時に取得したcookieを設定する
                .cookies(loginRes.cookies())
                .method(Method.GET)
                .execute();
        } catch (IOException e) {
            logger.error("初期画面ログインリダイレクトエラー", e);
            throw e;
        }

//      logger.debug(redirectRes.parse().outerHtml());

        // HTTPステータス照合
        checkStatusCode(redirectRes, redirectUrl);

    }

    /**
     * 現在サーバに設定されている時刻を取得します
     *
     * @param server
     * @throws RuntimeException
     * @return ゾーン
     * @throws IOException
     */
    private String getTimer(ServerBean server) throws RuntimeException, IOException {

        // サーバ時刻設定画面を取得します
        String timerUrl = "http://"+ server.getIpaddress() + "/pages/ed_time.htm";
        Response timerRes;
        try {
            timerRes = Jsoup.connect(timerUrl)
                    // ログイン時に取得したcookieを設定する
                    .cookies(loginRes.cookies())
                    .method(Method.GET)
                    .execute();
        } catch (IOException e) {
            logger.error("時刻設定画面取得エラー", e);
            throw e;
        }

        // HTTPステータス照合
        checkStatusCode(timerRes, timerUrl);

//      logger.debug(timerRes.parse().outerHtml());

        Document doc = timerRes.parse();

        // 現在サーバに設定されている時刻を取得します。
        String year = doc.getElementsByAttributeValue("name", "DateYYYY").get(0).val();
        String month = doc.getElementsByAttributeValue("name", "DateMM").get(0).val();
        String day = doc.getElementsByAttributeValue("name", "DateDD").get(0).val();
        String hour = doc.getElementsByAttributeValue("name", "TimeHH").get(0).val();
        String minutes = doc.getElementsByAttributeValue("name", "TimeMM").get(0).val();
        String zone = doc.select("select option[selected]").get(0).val();

        logger.info("現在サーバ時刻: "+ year + "/"+ month + "/"+ day + " "+ hour +":"+ minutes + " ("+ zone + ")");

        // zoneだけ、既存値を使用するので、返します。
        return zone;
    }

    /**
     * 時刻合わせを行います。
     *
     * @param server
     * @param zone
     * @throws RuntimeException
     * @throws IOException
     */
    private void adjustTimer(ServerBean server, String zone) throws RuntimeException, IOException {
        // 実行環境の現在時刻を取得します。
        LocalDateTime now = LocalDateTime.now();

        logger.info("現在時刻: "+ now.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

        // 1分後の0秒に合わせるため、次の時刻合わせのタイミングを取得します。
        LocalDateTime nextLdt =
                LocalDateTime.now()
                .plusMinutes(1)
                .truncatedTo(ChronoUnit.MINUTES);

        logger.debug("合わせる時刻: "+ nextLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

        // 待機ミリ秒
        long localDiffMsec = ChronoUnit.MILLIS.between(now, nextLdt);

        logger.debug("合わせる時刻まで、ミリ秒待機: "+ localDiffMsec);

        try {
            // 待機ミリ秒分、待つ
            Thread.sleep(localDiffMsec);
        } catch (InterruptedException e) {
            logger.error("サーバ時刻合わせミリ秒待機失敗");
        }

        logger.info("時刻合わせ実行");

        // サーバ時刻合わせ処理実行(POST)
        // なぜかcookieがなくても動いたので、そのままPOST投げてます
        String timerUrl = "http://"+ server.getIpaddress() + "/settime.cgi";
        Response timerRes;
        try {
            timerRes = Jsoup.connect(timerUrl)
                    .data("DateYYYY", ""+ nextLdt.getYear())
                    .data("DateMM", ""+ nextLdt.getMonthValue())
                    .data("DateDD", ""+ nextLdt.getDayOfMonth())
                    .data("TimeHH", ""+ nextLdt.getHour())
                    .data("TimeMM", ""+ nextLdt.getMinute())
                    .data("TimeZone", ""+ zone)
                    .method(Method.POST)
                    .execute();
        } catch (IOException e) {
            logger.error("サーバ時刻合わせ投稿エラー", e);
            throw e;
        }

//      logger.debug(timerRes.parse().outerHtml());

        // HTTPステータス照合
        checkStatusCode(timerRes, timerUrl);

        logger.info("時刻合わせ完了: "+ server.getIpaddress() + " / "+ server.getDevice_name() + " / " + nextLdt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));

    }

    /**
     * HTTPステータスのチェック
     *
     * @param res
     * @param url
     * @throws RuntimeException
     */
    private void checkStatusCode(Response res, String url) throws RuntimeException {

        if (res.statusCode() != 200) {
            // HTTPステータスが200(正常)ではない場合、エラーとする
            throw new RuntimeException("URLアクセス失敗: url="+ url + " status="+ res.statusCode());
        }
    }

}

所感

  • jsoupというとスクレイピングがよくに出てきますが、こういう使い方もありますよ、という事で。
  • ログイン->cookie保持->ログイン後処理 というjsoupコードが日本語であんまりなかったので、せっかくの機会ですし纏めてみました。
  • jsoupで一番苦労するのは、実際に処理が実行されるURLがどういうものなのか、を見つける事ですね。form で指定されていたり、リダイレクトされたり…
  • メモ帳でtsvを触ると、なぜか先頭に?が入ってしまう、という謎の現象が起きたので、IPアドレスの取得方法には正規表現を使用しています。エディタの指定とかできないんで…
  • つい昔の習慣で、beanを作って詰めてしまいますが、最近はこうじゃないんですかね…新しい方法も勉強しないと ^^;

今年もコツコツ頑張っていきたいと思います。どうぞよろしくお願いいたします。