JSP/ServletでカレンダーWebアプリを作成しよう

JSP&Servlet

前回、前々回とJavaでのカレンダーの作り方を学習した。その知識を活かして今度はブラウザで動くカレンダーWebアプリを作成してみよう。MVCに役割分担して処理を作成していく。

作成時のポイント

今回はWebアプリケーションなのでURLを考えていこう。どういったURL設定にしたら使いやすいアプリができるか?基本的な動きが実現できるようになったらそういったことも吟味していく必要がある。
今回は
http://ホスト名/calendarapp/main
というurlで実行日時のカレンダー
http://ホスト名/calendarapp/main?year=1980&month=5
とyearとmonthをクエリパラメータで渡すとその年月のカレンダーを表示する仕様とする。
こういうURL設計にしておくとそのカレンダーをブックマークしておくことができるし、自分が生まれた日は何曜日だったかとかもすぐに調べることができる。

完成イメージ

その月のカレンダーと前月と翌月のリンクボタンがある。背景を黄色にすることで当日をわかりやすくしている。なお、このHTMLとCSSは別記事にしてあるのでそちらを参照してもらいたい。

フォルダ構成図

以下がフォルダ構成図だ、作成していて迷子になりそうだったらすぐにここを参照してもらいたい。今回はエクリプスにて新規動的Webプロジェクトでcalendarappを作成することから始めている。

作成

model

基本的なカレンダーの作成方法は、前回の記事でわかっているのだが改めてWebアプリにするとなったらまずはモデルの構成を考えるところから始まる。
ポイントはjsp内であまりJava的な処理を記述したくないというところだ。
例えば2021年というyearを令和3年に変換する処理はどこにでも書ける。ただこういった処理は極力jspから排除する方針で設計していく。今回は以下のようにmodelを作成した。

○model.MyCalendar.java

package model;
import java.io.Serializable;
public class MyCalendar implements Serializable{
	//元号表記
	private String gengou;
	//カレンダーの年
	private int year;
	//カレンダーの月
	private int month;
	//カレンダーの日付を保持する配列
	private String[][] data;

	/*setter & getter*/
	public String getGengou() {
		return gengou;
	}
	public void setGengou(String gengou) {
		this.gengou = gengou;
	}
	public int getYear() {
		return year;
	}
	public void setYear(int year) {
		this.year = year;
	}
	public int getMonth() {
		return month;
	}
	public void setMonth(int month) {
		this.month = month;
	}
	public String[][] getData() {
		return data;
	}
	public void setData(String[][] data) {
		this.data = data;
	}
}

jsp内でやるjava的な仕事が極力少なくなるように設計する。メインとなるカレンダー部分も文字列の2次元配列で作ってしまうのがview表示が一番ラクだろう。令和3年とかの情報もフィールドにしてしまうとview表示が楽になる。

logicクラス

ではmodelのフィールドに情報を詰めていくlogicクラスを作っていこう。ここではJavaの処理を遠慮せずにガシガシ書いてもらって構わない。日頃のJava学習の成果の見せ所だ。

○model.MyCalendarLogic.java

package model;
import java.util.Calendar;
public class MyCalendarLogic {
	//カレンダーインスタンスを生成するメソッド(int...は可変長引数)
	public MyCalendar createMyCalendar(int... args) {
		//マイカレンダーインスタンス生成
		MyCalendar mc=new MyCalendar();
		//現在日時でカレンダーインスタンス生成
		Calendar cal=Calendar.getInstance();
		//2つの引数が来ていたら
		if(args.length==2) {
			//最初の引数で年を設定
			cal.set(Calendar.YEAR, args[0]);
			//次の引数で月を設定
			cal.set(Calendar.MONTH, args[1]-1);
		}
		//マイカレンダーに年を設定
		mc.setYear(cal.get(Calendar.YEAR));
		//マイカレンダーの元号の設定
		if(mc.getYear() > 2018) {
			mc.setGengou("令和"+(mc.getYear()-2018));
		}else if(mc.getYear() >1988 ) {
			mc.setGengou("平成"+(mc.getYear()-1988));
		}else if(mc.getYear() > 1925) {
			mc.setGengou("昭和"+(mc.getYear()-1925));
		}else if(mc.getYear() > 1911) {
			mc.setGengou("大正"+(mc.getYear()-1911));
		}else {
			mc.setGengou(""+mc.getYear());
		}
		//マイカレンダーに月の設定
		mc.setMonth(cal.get(Calendar.MONTH)+1);
		//その月の1日が何曜日かを調べる為に日付を1日にする
		cal.set(Calendar.DATE, 1);
		//カレンダーの最初の空白の数
		int before=cal.get(Calendar.DAY_OF_WEEK)-1;
		//カレンダーの日付の数
		int daysCount=cal.getActualMaximum(Calendar.DAY_OF_MONTH);
		//その月の最後の日が何曜日かを調べるために日付を最終日にする
		cal.set(Calendar.DATE, daysCount);
		//最後の日後の空白の数
		int after=7-cal.get(Calendar.DAY_OF_WEEK);
		//すべての要素数
		int total=before+daysCount+after;
		//その要素数を幅7個の配列に入れていった場合何行になるか
		int rows=total/7;
		//その行数で2次元配列を生成
		String[][] data=new String[rows][7];
		//今見ているカレンダーが今月かどうかを調べるために、この瞬間の日付情報をもつもう一つのインスタンス作成しておく
		Calendar now=Calendar.getInstance();
		for(int i=0;i<rows;i++) {
			for(int j=0;j<7;j++) {
				if(i==0 && j<before || i==rows-1 && j>=(7-after)) {
					//カレンダーの前後に入る空白の部分は空文字
					data[i][j]="";
				}else {
					//カウンター変数と実際の日付の変換
					int date=i*7+j+1 - before;
					//配列に日付を入れる
					data[i][j]=String.valueOf(date);
					//今作業しているマイカレンダーが今月のカレンダーだったら今日の日付の先頭に*を付与する
					if(now.get(Calendar.DATE)== date && now.get(Calendar.MONTH)==mc.getMonth()-1  && now.get(Calendar.YEAR)==mc.getYear()) {
						data[i][j]="*"+data[i][j];
					}
				}
			}
		}
		//作成した2次元配列をマイカレンダーにセットする。
		mc.setData(data);
		return mc;
	}
}

基本的なロジックは前回、前々回やってきたことと同じだ。ただ、今回はカレンダー表記の部分を2次元配列にした。工夫した点は引数を int... args と可変長引数にして、引数が0のときは現在のカレンダー、年と月を渡されればその月のカレンダーを作るようにしている。

ポイント解説

2次元配列を作成していく部分で質問があったので解説。

ソースコードの36行目でその月の最初の空白部分をbeforeとして求めている上の図だとこれは5だ。
38行目でその月の日数をdaysCountとして求めている(上の図だと31)
42行目でその月の最後の空白の数をafterとして求めてる(上の図だと6)
44行目で合計のマス目の数をtotalとして求めている(5+31+6=42)
46行目でマス目の数がtotalだった場合の行数を求めている。(42/7 ->6)
48行目で6行7列の二次元配列を作成。
51行目からの2重for文でこの配列に要素を詰めていくことやっている
53行目のif文は空白が入る部分を判別している。空白が入る可能性のある行は最初と最後。つまりiが0の時とiが5の場合だ(上の図の場合)
その行のときの列の値jの値をみて空白かどうかを判別している。
上の例で行くと今回はiが0のときjは4までが空白となる
58行目で実際の日付の数字を計算している。
上の例でいくとiが0でjが5の時に最初の数字入る。このときに実際に入る数字は
i*7+j+1 - before
で求めることができる。なのでiが0でjが5の時は
0*7 + 5+1 - 5 ->1
iが0、jが6時は
0*7 + 6+1 - 5 ->2
行が変わってiが1,jが0のときは
1*7 + 0+1-5 ->3
以降53行目のif文が空白になるまでこの作業が続く。
最後に残りを空白で埋めて以下のような2次元配列が完成する。

data=
{
  {"","","","","","1","2"},
  {"3","4","5","6","7","8","9"},
  {"10","11","12","13","14","15","16"},
  {"17","18","19","20","21","22","23"},
  {"24","25","26","27","28","29","30"},
  {"31","","","","","",""}
}

controller

コントローラーを作成していこう。新規サーブレットからcontroller.Main.javaを以下のように作成する。

○controller.Main.java

package controller;
import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import model.MyCalendar;
import model.MyCalendarLogic;

@WebServlet("/main")
public class Main extends HttpServlet {
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String s_year=request.getParameter("year");
		String s_month=request.getParameter("month");
		MyCalendarLogic logic=new MyCalendarLogic();
		MyCalendar mc=null;
		if(s_year != null && s_month != null) {
			int year =Integer.parseInt(s_year);
			int month=Integer.parseInt(s_month);
			if(month==0) {
				month=12;
				year--;
			}
			if(month==13) {
				month=1;
				year++;
			}
			//年と月のクエリパラメーターが来ている場合にはその年月でカレンダーを生成する
			mc=logic.createMyCalendar(year,month);
		}else {
			//クエリパラメータが来ていないときは実行日時のカレンダーを生成する。
			mc=logic.createMyCalendar();
		}
		//リクエストスコープに格納
		request.setAttribute("mc", mc);
		//viewにフォワード
		RequestDispatcher rd=request.getRequestDispatcher("/WEB-INF/view/main.jsp");
		rd.forward(request, response);
	}
}

クエリパラメータでyearとmonthが来ているかどうかで処理を分岐している。定型的な処理ばかりなので難しい部分はないだろう。細かい点としては@WebServlet("/main")
としてURLを小文字のmainに変更している。今回URLはすべて小文字になるようにした。

view

WEB-INFフォルダの中にviewフォルダを作成し、以下のようにmain.jspを作成する

○main.jspの作成

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" import="model.MyCalendar"%>
<%
	MyCalendar mc=(MyCalendar)request.getAttribute("mc");
%>
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title><%=mc.getGengou() %>年<%=mc.getMonth() %>月カレンダー</title>
  <link rel="stylesheet"  href="http://yui.yahooapis.com/3.18.1/build/cssreset/cssreset-min.css">
  <link href="https://fonts.googleapis.com/css?family=M+PLUS+Rounded+1c" rel="stylesheet">
  <link rel="stylesheet"  href="css/main.css">
</head>
<body>
  <div id="container">
    <h1><%=mc.getGengou() %>年<%=mc.getMonth() %>月のカレンダー</h1>
    <p>
		 <a href="?year=<%=mc.getYear()%>&month=<%=mc.getMonth()-1%>">前月</a>
    	<a href="?year=<%=mc.getYear()%>&month=<%=mc.getMonth()+1%>">翌月</a>
    </p>
    <table>
      <tr>
        <th>日</th>
        <th>月</th>
        <th>火</th>
        <th>水</th>
        <th>木</th>
        <th>金</th>
        <th>土</th>
      </tr>
      <%for(String[] row: mc.getData()){ %>
      <tr>
      	<%for(String col:row) {%>
      		<%if (col.startsWith("*")){ %>
      			<td class="today"><%=col.substring(1)%></td>
      		<%}else{ %>
      			<td><%=col %></td>
      		<%} %>
      	<%} %>
      </tr>
      <%} %>
    </table>
  </div><!-- end container-->
</body>
</html>

model作成時に工夫しているのでviewの表示はとてもシンプルな処理になっていることがわかる。本日を表す*が先頭についている日付データはtodayクラスを付与し、*は削除している。

css

WebContentの中にcssフォルダを作り以下のようにmain.cssを作成する

○main.cssの作成

@charset "UTF-8";
body{
	background:#7ce2d9;
}
#container{
  width:1000px;
  min-height:100vh;
  margin:0 auto;
  font-family: "M PLUS Rounded 1c";
  padding:20px 0 40px;
}
#container > p{
	display:flex;
	width:90%;
	margin:0 auto 20px;
	justify-content:space-between;
	align-items:center;
}
#container > p > a{
	display:block;
	width:70px;
	height:70px;
	text-align:center;
	line-height:70px;
	border-radius:50%;
	background:#fefefe;
	font-size:20px;
	text-decoration:none;

}
h1{
  width:800px;
  margin:0 auto 20px;
  text-align:center;
  font-size:64px;
  border:5px solid #fff00f;
  border-radius: 20px;
  padding:0 10px;
  background-color:#00ff76;
  color:#ff69b4;
  text-shadow: 1px 1px 0 #333;
}
table{
  width:90%;
  margin:20px auto 0;
  border-collapse: collapse;
  border-spacing: 0;
}
td,th{
  border:5px solid #fff00f;
  text-align: center;
  background-color:#ffffff;
  font-size:50px;
  padding:5px 0;
}
th{
  color:white;
  background-color:#00ff76;
  text-shadow: 1px 1px 0 #333;
}
tr *:first-child{
  color:#ff69b4;
}
tr *:last-child{
  color:#0ba9ea;
}
td.today{
	background:#ffff80;
}

カレンダーをデザインしている。繰り返しになるがこのマークアップとCSSに関しては別記事にまとめてあるので詳しく知りたい方はそちらを参照すること

完成

以上で完成だ。クエリパラメーターで自分が何曜日に生まれたのかを調べてみてもらいたい。今回、元デザインにあった画像の部分は削除してしまったが12ヶ月分画像を用意して月によって画像が切り替わるようにしても面白いだろう。是非チャレンジしてもらいたい。

関連記事

html,cssなどの見た目に関して

Javaで作るカレンダーの基本、Calendarクラスの使いかた

Javaで作るカレンダー応用(OOP)

コメント

タイトルとURLをコピーしました