2014年7月13日日曜日

Spring MVC + Thymeleaf による WEB アプリケーション開発(1)プロジェクト作成

このシリーズの1回目では、Spring MVC と Thymeleaf によるWEBアプリケーション開発の雛形となるプロジェクトを作成します。
開発環境は以下の通りです。
  • Maven 3.1.1
  • MySQL 5.6.17
  • Eclipse IDE for Java EE 4.3.2
  • Tomcat 7.0.29

依存ライブラリの主なものは以下の通りです。詳細は、後述する pom.xml を参照してください。
  • Spring MVC 3.2.8.RELEASE
  • Spring Data JPA 1.5.1.RELEASE
  • Thymeleaf 2.1.3.RELEASE
  • Hibernate 4.1.9.FINAL

プロジェクトの作成
eclipse メニューから File > New > Project… で新規プロジェクト作成ウィザードを表示します:

Dynamic Web Project を選択して Next をクリックします:

プロジェクト名:mvcdemo
ターゲットランタイム:Apache Tomcat v7.0
ダイナミックWEBのモジュールバージョン:3.0
を設定して Finish をクリックします。

以下のような構造のプロジェクトが作成されます:



Maven による依存ライブラリの管理
プロジェクトを Maven プロジェクトに変換します。プロジェクトを右クリックして、ポップアップメニューから Configure > Convert to Maven を選択してください。 POMファイル作成ウィザードが表示されます:
上図のように、グループID、アーティファクトIDなどを設定して Finish をクリックします。
以下のような pom.xml ファイルがプロジェクトのルートに作成されます:
<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
  http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.itrane</groupId>
  <artifactId>mvcdemo</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>
  <name>mvcdemo</name>
  <build>
    <sourceDirectory>src</sourceDirectory>
    <plugins>
      <plugin>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.4</version>
        <configuration>
          <warSourceDirectory>WebContent</warSourceDirectory>
          <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

この pom.xml に properties と dependencies を追加します:
<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  ...
  <name>mvcdemo</name>

  <!-- ここに依存ライブラリを指定 -->  
  <properties>
    <!-- Spring / Hibernate version -->
    <version.spring>3.2.8.RELEASE</version.spring>
    <version.spring.data>1.5.1.RELEASE</version.spring.data>
    <version.spring.boot>1.0.0.RELEASE</version.spring.boot>
    <version.hibernate>4.1.9.FINAL</version.hibernate>

    <version.aopalliance>1.0</version.aopalliance>
    <version.thymeleaf>2.1.3.RELEASE</version.thymeleaf>
    <version.mysql>5.1.22</version.mysql>
    <!-- Logger -->
    <version.sl4j>1.7.7</version.sl4j>
    <version.logback>1.0.7</version.logback>
    <!-- Servlet & taglibs -->
    <version.servlet>3.0.1</version.servlet>
    <version.jstl>1.2</version.jstl>
    <version.taglibs>1.1.2</version.taglibs>
    <!-- Test -->
    <version.junit>4.11</version.junit>
    <version.dbunit>2.4.9</version.dbunit>
  </properties>

  <dependencies>
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf</artifactId>
        <version>${version.thymeleaf}</version>
    </dependency>
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring3</artifactId>
        <version>${version.thymeleaf}</version>
    </dependency>
    
    <!-- Servlet & taglibs -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>${version.servlet}</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>jstl</artifactId>
        <version>${version.jstl}</version>
    </dependency>
    <dependency>
        <groupId>taglibs</groupId>
        <artifactId>standard</artifactId>
        <version>${version.taglibs}</version>
    </dependency>
    
    <!-- JSR-330 -->
    <dependency>
        <groupId>javax.inject</groupId>
        <artifactId>javax.inject</artifactId>
        <version>1</version>
    </dependency>
     
    <!-- Spring dependencies -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-expression</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-orm</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>${version.spring}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${version.spring}</version>
    </dependency>
 
    <!-- Third Party dependencies -->
    <dependency>
        <groupId>aopalliance</groupId>
        <artifactId>aopalliance</artifactId>
        <version>${version.aopalliance}</version>
    </dependency>
 
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>2.2.2</version>
    </dependency>
 
    <!-- Spring DATA -->
    <dependency>
      <groupId>org.springframework.data</groupId >
      <artifactId>spring-data-jpa</artifactId>
      <version>${version.spring.data}</version >
    </dependency>
    <!-- Hibernate and JPA -->
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>${version.hibernate}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>${version.hibernate}</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate.javax.persistence</groupId>
        <artifactId>hibernate-jpa-2.0-api</artifactId>
        <version>1.0.1.Final</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate.java-persistence</groupId>
        <artifactId>jpa-api</artifactId>
        <version>2.0-cr-1</version>
    </dependency>

    <!-- JSR 303 with Hibernate Validator -->
    <dependency>
        <groupId>javax.validation</groupId>
        <artifactId>validation-api</artifactId>
        <version>1.0.0.GA</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>4.3.0.Final</version>
    </dependency>
 
    <!-- MySQL JDBC Driver -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${version.mysql}</version>
    </dependency>
 
    <dependency>
        <groupId>c3p0</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.0.4</version>
    </dependency>
    
    <!-- Logger Library -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>${version.sl4j}</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${version.logback}</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-batch</artifactId>
        <version>${version.spring.boot}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>${version.spring.boot}</version>
    </dependency>
    <dependency>
        <groupId>com.googlecode.json-simple</groupId>
        <artifactId>json-simple</artifactId>
        <version>1.1</version>
    </dependency>

    <!-- Test -->
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-all</artifactId>
      <version>1.3</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${version.junit}</version>
      <scope>test</scope>
      <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${version.spring}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.github.springtestdbunit</groupId>
      <artifactId>spring-test-dbunit</artifactId>
      <version>1.0.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.dbunit</groupId>
      <artifactId>dbunit</artifactId>
      <version>${version.dbunit}</version>
      <scope>test</scope>
      <exclusions>
        <exclusion>
            <artifactId>junit</artifactId>
            <groupId>junit</groupId>
        </exclusion>
      </exclusions>
    </dependency>
  </dependencies>
  
  <build>
    <sourceDirectory>src</sourceDirectory>
    <plugins>
    ...
    </plugins>
  </build>
</project>


ログ出力のための設定
ログ出力には、sl4j と logback を使います(pom.xml の dependencies に追加)。以下のような logback設定ファイルをプロジェクトの src ディレクトリに作成します。
mvcdemo/src/logback.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <prudent>true</prudent>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>

        <encoder>
            <pattern>%date{yyyy/MM/dd HH:mm:ss:SSS} %.5level - %logger{0}.%.20method %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target>
        <encoder>
            <pattern>%date{yyyy/MM/dd HH:mm:ss:SSS} %.5level - %logger{0}.%.20method %msg%n</pattern>
        </encoder>
    </appender>
    
    <logger name="com.itrane.mvcdemo" level="DEBUG"/>

    <root>
        <level value="info" />
        <appender-ref ref="FILE" />
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

上記の設定では、com.itrane.mvcdemo パッケージ以下のクラスに対してログレベルをDEBUGに設定しています。

Spring MVC Java 設定
Spring では従来、アプリケーションのコンテキスト XML ファイルを使用して設定を行うのが一般的ですが、このプロジェクトでは JAVA ベースの設定を使用します。AbstractAnnotationConfigDispatcherServletInitializer を拡張して以下のような AppInit クラスを作成します。
com.itrane.mvcdemo.init.AppInit:
/**
 * web.xml に変わり、アプリの初期化を行う.
 */
public class AppInit extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[0];
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{ WebAppConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{ "/" };
    }
    
    /*
     * 日本語の文字化けを防ぐために エンコーディング・フィルタを設定する
     * @see org.springframework.web.servlet.support.AbstractDispatcherServletInitializer#getServletFilters()
     */
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");
        characterEncodingFilter.setForceEncoding(true);
        
        return new Filter[] { characterEncodingFilter };
    }
}

上記イニシャライザーの getServletConfigClasses( ) メソッドで、WEBアプリケーション設定クラスとして、WebAppConfig クラスを指定します。このクラスの内容は以下のようになります。
com.itrane.mvcdemo.init.WebAppConfig:
/**
 * Spring フレームワークの設定.
 *  注意:この Java 設定クラスを使用するには Spring Framework 3.1 以上が必要
 */
@Configuration
@EnableWebMvc
@ComponentScan("com.itrane.mvcdemo")
@PropertySource("classpath:resources/app.properties")
public class WebAppConfig extends WebMvcConfigurerAdapter {

    @Resource
    private Environment env;
    
    //静的リソースの設定
    @Override  
    public void addResourceHandlers(ResourceHandlerRegistry registry) {  
            registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");  
    }

    //テンプレートリゾルバーの設定
    @Bean
    public ServletContextTemplateResolver templateResolver() {
        ServletContextTemplateResolver resolver = new ServletContextTemplateResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".html");
        //NB, selecting HTML5 as the template mode.
        resolver.setTemplateMode("HTML5");
        resolver.setCacheable(false);
        resolver.setCharacterEncoding("UTF-8");
        return resolver;
    }
    
    //Thymeleaf テンプレートエンジンの設定
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine engine = new SpringTemplateEngine();
        engine.setTemplateResolver(templateResolver());
        return engine;
    }
    
    //Thymeleaf ビューリゾルバー設定
    @Bean
    public ViewResolver viewResolver() {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine());
        viewResolver.setOrder(1);
        viewResolver.setViewNames(new String[]{"*"});
        viewResolver.setCache(false);
        viewResolver.setCharacterEncoding("UTF-8");
        return viewResolver;
    }
    
    //メッセージソースの設定
    //WEBページでプロパティファイルを使用できる
    //日本語メッセージ:messages_ja.properties 
    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename(env.getRequiredProperty("message.source.basename"));

        // trueをセットすれば、メッセージのキーがない場合にキーを表示
        // false の場合、NoSuchMessageExceptionを投げる
        messageSource.setUseCodeAsDefaultMessage(true);
        messageSource.setDefaultEncoding("UTF-8");
        // # -1 : リロードしない、0 : 常にリロードする
        messageSource.setCacheSeconds(0);
        return messageSource;
    }
}

上記設定クラスでは、静的リソースの設定、Thymeleaf のテンプレートエンジン、テンプレートリゾルバーの設定、メッセージソースの設定を行っています。
アノテーションの @Configuration はこのクラスが設定ファイルであることを示します。 @EnableWebMvc は Spring MVC の機能を有効にします。@ComponentScan("com.itrane.mvcdemo") は、com.itrane.mvcdemo パッケージ以下の Spring コンポーネント(@Controller や @Service などの注釈がついたクラス)を検出して登録します。@PropertySource は、設定内容を外部のプロパティファイルから読み込む事を指定します 。
src/resources/app.properties ファイルの内容は以下のようになります:
#message source
message.source.basename=classpath:resources/messages

コントローラの作成
アプリケーションのホームページを表示するための簡単なコントローラを作成します。このコントローラはアプリケーションの URL "http://localhost:8080/mvcdemo/" がリクエストされたとき、ログを出力し、現在の時間を取得して、Model オブジェクトに設定します。表示ビューとして、"home/home.html" を指定します。
com.itrane.mvcdemo.controller.HomeController:
@Controller
public class HomeController {
    
    final static private Logger log = LoggerFactory.getLogger(HomeController.class); 

    @RequestMapping(value="/", method=RequestMethod.GET)
    public String navbar(Model model) {
        log.debug("");
        model.addAttribute("today", Calendar.getInstance().getTime());
        return "/home/home";
    }
}

ビューの作成
ビューフレームワークとして ThymeleafBootstrap を使用します。Bootstrap の使用に必要なファイルをダウンロードして、 WebContent/resources フォルダにコピーしてください:

設定クラスの WebAppConig のテンプレートリゾルバーの設定にしたがって、ビューは WEB-INF/views フォルダ以下に作成する必要があります。アプリケーションのホームページ・ビューをviews フォルダ下の home フォルダーに作成します。
WEB-INF/views/home/home.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org">
<head th:include="fragment/frag01 :: htmlhead" th:with="title='Spring MVC デモ'"></head>

<style>
.contents {
    margin: 50px;
}
</style>
<body>

<div th:include="fragment/frag01 :: navbar"></div>

<div class="contents">
<div class="container">

  <div class="row">
    <div class="col-lg-12">
      <h2 class="page-header" th:utext="#{home.title}">Home
      <small>(Spring MVC Demo)</small>
      </h2>
    </div>
  </div>

  <div class="row">
    <div class="col-lg-12">
        <p th:utext="#{home.welcome}">Welcome to our mvc sample</p>
        <p><span th:text="${today}">2014/05/01</span></p>
        <br />
        <br />
        <hr />
        <div th:include="fragment/frag01 :: footer"></div>
    </div>
  </div>

</div>
</div>
</body>
</html>

(home.html : フラグメントのインクルード)
このビューでは、Thymeleaf のテンプレート機能を使って、共通コンテンツ(HTMLヘッダ、ナビゲーション・バー、フッター)をフラグメントとしてインクルードしています。
HTMLヘッダのインクルード:
<head th:include="fragment/frag01 :: htmlhead" th:with="title='Spring MVC デモ'"></head>
ナビゲーション・バーのインクルード:
<div th:include="fragment/frag01 :: navbar"></div>
フッターのインクルード:
<div th:include="fragment/frag01 :: footer"></div>

th:include では "テンプレートのフォルダ/テンプレート名 :: フラグメント名" により、インクルードするフラグメントを参照します。

アプリケーションの各ビューが参照するテンプレートファイルを作成します。
WEB-INF/fragment/frag01.html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<!-- HTMLヘッダ -->
<head th:fragment="htmlhead">
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
      <title th:text="${title}">(title)</title>

    <!-- CSS -->
    <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" />

    <!-- HTML5 shim, IE6-8 で HTML5 要素をサポートする -->
    <!--[if lt IE 9]>
    <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
    <![endif]-->
    
    <!-- JavaScript : jquery, bootstrap, custom -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
    <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
</head>      

<body>

<!-- ナビゲーション・バー -->
<div th:fragment="navbar">
    <nav class="navbar navbar-default navbar-fixed-top" role="navigation" >
        <div class="container">
            <div class="navbar-header">
                <a class="navbar-brand" th:href="@{/}">Spring MVC デモ</a>
            </div>

            <div class="collapse navbar-collapse navbar-ex1-collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="#">About</a></li>

                    <li class="dropdown"><a href="#" class="dropdown-toggle"
                        data-toggle="dropdown"> 店舗管理 <b class="caret"></b></a>
                        <ul class="dropdown-menu" role="menu">
                            <li><a href="#">店舗一覧(ajax)</a></li>
                            <li class="divider"></li>
                            <li><a href="#">全件削除</a></li>
                            <li><a href="#">1000件追加</a></li>
                        </ul></li>
                </ul>
            </div>
        </div>
    </nav>
</div>
<!-- フッター -->
<div th:fragment="footer">
    <footer>
        <p>&copy; Spring MVC デモ 2013</p>
    </footer>
</div>

</body>
</html>

上のファイル中の th:fragment 属性はフラグメントを定義します。フラグメント "htmlhead" では各ビューで共通の HTMLヘッダーを定義します。ここでは Bootstrap の使用に必要な css , javascript を指定しています。

(home.html : 国際化メッセージファイル)
<p th:utext="#{home.welcome}">Welcome to our mvc sample</p>

home.html の上記の部分はウェルカムメッセージを日本語のメッセージに変換します。 #{home.welcom} 式は、メッセージファイル中の識別キー "home.welcome" に対応するメッセージに変換されます。th:utext または th:text は変換結果で、タグのボディ部を書き換えます。home.html と同じフォルダに、メッセージファイル"home_ja.properties"があれば、 Thymeleaf は、このファイルから対応するメッセージを見つけます。
WEB-INF/views/home/home_ja.propeties の内容は次のようになります:
home.title=Spring MVC デモ <small>ホーム</small>
home.welcome=<b>MVCサンプル</b>へようこそ!


ここまでの作業を確認
作成したアプリケーションを実行します:


 実行の結果:
フラグメント(htmlhead, navbar, footer) が正しくインクルードされています。
Bootstrap により、ナビゲーションバーの表示、ページのレイアウトが正しく行われています。
ウェルカムメッセージは、正しく日本語に変換されています。
コントローラーから返された日付が正しく表示されています。

さらに、eclipse IDE のコンソールビューで、ログ出力を確認することもできます:
2014/06/13 13:17:36:829 DEBUG - HomeController.navbar 
2014/06/13 13:17:36:848 INFO - TemplateEngine.initialize [THYMELEAF] INITIALIZING TEMPLATE ENGINE
.....

まとめ
Spring フレームワークの Spring MVC を利用して、モデル・ビュー・コントローラ構造のWEBアプリケーションを柔軟に構築できます。Spring MVC とThymeleaf を連携することで、ビューの共通要素をフラグメント化したり、HTML5の機能を利用することが可能です。jQueryBootsrap により、クールな UI を簡単に実現できます。これらのフレームワークはアプリ要件に応じて他の強力なフレームワークと比較的簡単に組み合わせる事ができます。
※データベースアクセスのための設定は、次回に説明します。

ソースコード
ソースコードは GitHub-branch:mvcdemo01 からダウンロードできます。

0 件のコメント: