要件
- とあるアプリケーションで、時刻がすぐに狂ってしまう。
- 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を作って詰めてしまいますが、最近はこうじゃないんですかね…新しい方法も勉強しないと ^^;
今年もコツコツ頑張っていきたいと思います。どうぞよろしくお願いいたします。