Neo4j及びSpring Data Neo4j 4.0.0-RC1を使用した検索アプリケーションを開発する

  • 9
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

概要

グラフデータベースにNeo4j、WEBアプリケーションフレームワークにSpring Boot 1.3.0.M3とSpring Data Neo4j 4.0.0-RC1を使用した検索アプリケーションを開発します。
検索機能をメインに開発しますが一部更新系も実装します。
このアプリケーションで使用するデータセットは、Neo4jに用意されてるNorthwind Graphを使わせて貰いました。

環境

参考

下記のサイトを参考にさせていただきました。

Neo4j

Spring Data Neo4J

Spring Data Neo4j Version 4.0

Stack Overflow

sdn4 example

Github

ソースコードはsdn4rc2-exampleにあります。

事前準備

Java,Eclipse,Maven,Neo4jのインストール、設定方法については省略します。

使用するNeo4j Community Editionのバージョンは2.2.2です(記事作成時点での2.2系の最新は2.2.4)。
ダウンロードページにはバージョン2.2.2をダウンロードするリンクが消えていますが、ダウンロードURLの一部を2.2.2に書き換えるとダウンロードすることができます。
ダウンロードができたらNeo4jのインストールを行いサーバーを起動させておきます。

サンプルデータについて

Neo4jサーバーの起動ができたら、このアプリケーションで使用するサンプルデータを投入します。

使用するサンプルデータは概要でも述べたとおり、Neo4jに用意されているNorthwind_Graphデータセットを使用します。
このデータセットの構造は下記の通りです。
Cyhperの表記に則り、ノードは(Customer)の様に丸括弧で、リレーションは[PURCHASED]の様に角括弧で表現しました。

Northwind_Graph
                                           (Category)
                                                │
                                             1  │
                                            [PART_OF]
                                                │  n
             1                     1            ↓
(Customer)──[PURCHASED]─→(Order)──[ORDERS]─→(Product)
                     n                  n       ↑
                                             n  │
                                            [SUPPLIES]
                                                │  1
                                                │
                                            (Supplier)

Nodeの種類

Node records
Customer 91
Order 830
Product 77
Supplier 29
Category 8

Customerノード

field name data type desc
customerID string カスタマーID
contactName string 連絡先(担当者)
contactTitle string 担当者の役職、肩書き
country string
region string
city string
address string 番地、ストリート名
postalCode string 郵便番号
phone string 電話番号
fax string FAX番号

Orderノード

field name data type desc
orderID int オーダーID
orderDate string 注文日
shipName string 出荷先の名称
shipCountry string 出荷先の国
shipRegion string 出荷先の州
shipCity string 出荷先の市
shipAddress string 出荷先の番地、ストリート名
shipPostalCode string 出荷先の郵便番号
shippedDate string 出荷日
requiredDate string 所要日
shipVia string 輸送方法
freight string 輸送運賃
customerID int カスタマーID
employeeID int エンプロイイーID

Productノード

field name data type desc
productID int プロダクトID
productName string 製品名
quantityPerUnit string 数量単位
unitPrice double 単価
unitsInStock int 在庫数
unitsOnOrder int 受注数
reorderLevel int 追加注文数
discontinued bool 取り扱い中止フラグ(true:中止)
supplierID int サプライヤーID
categoryID int カテゴリーID

Supplierノード

field name data type desc
supplierID int サプライヤーID
companyName string 会社名
contactName string 連絡先(担当者)
contactTitle string 担当者の役職、肩書き
homePage string ホームページ
country string
region string
city string
address string 番地、ストリート名
postalCode string 郵便番号
phone string 電話番号
fax string FAX番号

Categoryノード

field name data type desc
categoryID int カテゴリーID
categoryName string カテゴリ名
description string 説明
picture string ?

サンプルデータの作成

Northwind GraphデータセットをNeo4jデータベースに作成するには、Browser Interfaceを立ち上げて画面上部のプロンプトに下記のコマンドを入力して実行します。

$ :play northwind graph

play_northwind_graph.png

データ作成方法についての説明画面が表示されますので、その説明に従ってデータを作成します。

play_northwind_graph_1.png

上記の手順でデータの作成が終わったら下記の処理を実行してデータの修正を行います。

OrderノードのshippedDateプロパティに'NULL'という文字列がセットされているデータがあるのでこれを除去ます。

$ MATCH (o:Order) WHERE o.shippedDate = 'NULL' REMOVE o.shippedDate return o;

各ノードのIDプロパティが文字列として登録されているのでこれを数値型に変換します。

$ MATCH (o:Order) set o.orderID = toInt(o.orderID), o.employeeID = toInt(o.employeeID);
$ MATCH (p:Product) set p.productID = toInt(p.productID), p.categoryID = toInt(p.categoryID), p.supplierID = toInt(p.supplierID);
$ MATCH (s:Supplier) set s.supplierID = toInt(s.supplierID);
$ MATCH (c:Category) set c.categoryID = toInt(c.categoryID);

アプリケーションの作成

プロジェクトの雛形を生成

アプリケーション名:sdn4m1_example

mavenでアプリケーションの雛形を作成

> mvn archetype:generate -DgroupId=com.example.sdn4m1 -DartifactId=sdn4m1_example -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
> cd sdn4m1_example
> mvn eclipse:eclipse

eclipseにインポート

  • メニューバーの"File" -> "Import..." -> "Maven" -> "Existing Maven Projects"を選択します。
  • プロジェクトのディレクトリを選択し、"Finish"ボタンをクリックします。

pom.xmlの編集

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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example.sdn4m1</groupId>
  <artifactId>sdn4m1_example</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>sdn4m1_example</name>
  <url>http://maven.apache.org</url>

  <properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-data-neo4j.version>4.0.0.RC1</spring-data-neo4j.version>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.0.M3</version>
  </parent>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-neo4j</artifactId>
      <version>${spring-data-neo4j.version}</version><!--$NO-MVN-MAN-VER$-->
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aspects</artifactId>
    </dependency>

    <dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.4</version>
    </dependency>

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <repositories>
    <repository>
      <id>spring-libs-snapshot</id>
      <name>Spring</name>
      <url>http://repo.spring.io/libs-snapshot</url>
    </repository>
    <repository>
      <id>spring-snapshots</id>
      <name>Spring Snapshot</name>
      <url>http://repo.spring.io/snapshot</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>spring-milestones</id>
      <name>Spring Milestones</name>
      <url>http://repo.spring.io/milestone</url>
      <snapshots>
          <enabled>false</enabled>
      </snapshots>
    </repository>
    <repository>
      <id>neo4j-snapshots</id>
      <url>http://m2.neo4j.org/content/repositories/snapshots</url>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
  </repositories>

  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>1.2.5.RELEASE</version>
        </plugin>
      </plugins>
    </pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <verbose>true</verbose>
          <source>${java.version}</source>
          <target>${java.version}</target>
          <encoding>${project.build.sourceEncoding}</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>versions-maven-plugin</artifactId>
      </plugin>
      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>7.6.5.v20120716</version>
        <configuration>
          <scanIntervalSeconds>10</scanIntervalSeconds>
          <stopKey>foo</stopKey>
          <stopPort>9999</stopPort>
          <jvmArgs></jvmArgs>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

resourcesフォルダの作成

src/main/resourcesフォルダを作成します。

  • "Build Path" -> "Configure Build Path" -> "Java Buld Path" -> "Source"タブを選択する。
  • "Add Folder"ボタンをクリック -> 作成した"resources"フォルダにチェックを入れる。

application.ymlの作成

src/main/resourcesフォルダ内にapplication.ymlを作成します。

application.yml
# EMBEDDED SERVER CONFIGURATION (ServerProperties)
server:
  port: 9000

spring:
# THYMELEAF (ThymeleafAutoConfiguration)
  thymeleaf:
    enabled: true
    cache: false
# INTERNATIONALIZATION (MessageSourceAutoConfiguration)
  messages:
    basename: messages
    cache-seconds: -1
    encoding: UTF-8

# ENDPOINTS (AbstractEndpoint subclasses)
endpoints:
  enabled: true

# NEO4j
neo4j:
  username: neo4j
  password: neo4jpass

neo4jセクションには、Neo4jサーバーにログインできるユーザー名とパスワードを指定します。
この例で使用しているユーザーneo4jは、Neo4jサーバーのデフォルトユーザーです。

logback.xmlの作成

src/main/resourcesフォルダ内にlogback.xmlを作成します。
ログの出力先フォルダを"D:/logs"に指定しました。

logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <property name="LOG_DIR" value="D:/logs" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MMM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{35} - %msg %n</pattern>
    </encoder>
  </appender>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
     <file>${LOG_DIR}/sdn4m1-example.log</file>
     <encoder>
       <charset>UTF-8</charset>
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] - %msg %n</pattern>
     </encoder>
  </appender>

  <logger name="com.example" level="DEBUG" />

  <logger name="org.hibernate" level="ERROR"/>
  <logger name="org.springframework" level="INFO"/>
  <logger name="org.thymeleaf" level="INFO"/>
  <logger name="org.eclipse.jetty" level="INFO"/>
  <logger name="org.apache.http" level="INFO"/>
  <root>
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE" />
  </root>
</configuration>

ビルド

この時点で動作検証を兼ねてビルドします。

package
> mvn package

ビルドが成功したら生成したjarファイルを実行します。
コマンドプロンプトに"Hello World!"と表示されれば成功です。

helloWorld
> cd target
> java -jar actor-1.0-SNAPSHOT.jar
Hello World!

アプリケーションの開発

App

エンドポイントとなるクラスを作成します。
すでにサンプルのApp.javaがありますので、このファイルを下記のように変更します。

App.java
package com.example.sdn4m1;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import com.example.sdn4m1.helper.MyDialect;

@SpringBootApplication
public class App {
  final static Logger logger = LoggerFactory.getLogger(App.class);

  public static void main( String[] args ) {
    SpringApplication.run(App.class, args);
  }

  //THYMELEAF Utility Object
  @Bean
  MyDialect myDialect() {
    return new MyDialect();
  }

}
Config

Neo4jの設定を行うConfigurationクラスを作成します。
Spring Data Neo4j バージョン4からはリモートサーバーのみで、組み込みサーバーは使用できないようです。

Neo4jConfig.java
package com.example.sdn4m1;

import java.io.IOException;
import java.util.Arrays;

import javax.annotation.PostConstruct;

import org.neo4j.ogm.session.Session;
import org.neo4j.ogm.session.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.env.Environment;
import org.springframework.data.neo4j.config.Neo4jConfiguration;
import org.springframework.data.neo4j.event.AfterDeleteEvent;
import org.springframework.data.neo4j.event.AfterSaveEvent;
import org.springframework.data.neo4j.event.BeforeDeleteEvent;
import org.springframework.data.neo4j.event.BeforeSaveEvent;
import org.springframework.data.neo4j.event.Neo4jDataManipulationEvent;
import org.springframework.data.neo4j.repository.config.EnableNeo4jRepositories;
import org.springframework.data.neo4j.server.Neo4jServer;
import org.springframework.data.neo4j.server.RemoteServer;
import org.springframework.data.neo4j.template.Neo4jOperations;
import org.springframework.data.neo4j.template.Neo4jTemplate;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.example.sdn4m1.domain.Entity;

@Configuration
@EnableNeo4jRepositories(basePackages = "com.example.sdn4m1.repository", queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
@EnableTransactionManagement
public class Neo4jConfig extends Neo4jConfiguration {
  final static Logger logger = LoggerFactory.getLogger(Neo4jConfig.class);

  private static final String NEO4J_HOST = "http://localhost:";
  private static final String NEO4J_PORT = "7474";

  @Autowired
  private Environment env;

  @Override
  @Bean
  public SessionFactory getSessionFactory() {
    String username = env.getProperty("neo4j.username");
    String password = env.getProperty("neo4j.password");
    System.setProperty("username", username);
    System.setProperty("password", password);
    SessionFactory sessionFactory = new SessionFactory("com.example.sdn4m1.domain");
    return sessionFactory;
  }

  @Override
  @Bean
  public Neo4jServer neo4jServer() {
    Neo4jServer neo4jServer = new RemoteServer(NEO4J_HOST + NEO4J_PORT);
    return neo4jServer;
  }

  @Override
  @Bean
  @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
  public Session getSession() throws Exception {
    Session session = super.getSession();
    return session;
  }

  @Bean
  public Neo4jOperations getNeo4jTemplate() throws Exception {
    return new Neo4jTemplate(getSession());
  }
}
Domain/Repository

Customer,Order,Product,Supplier,Categoryの各ノードのDomainクラスを作成します。
またこれらのノードに対応するRepositoryインターフェースを作成します。
GraphRepositoryインターフェースを継承することで基本的なノード操作が行えるようになります。

Customer

Customer.java
package com.example.sdn4m1.domain.northwind;

import java.util.HashSet;
import java.util.Set;

import org.neo4j.ogm.annotation.GraphId;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Property;
import org.neo4j.ogm.annotation.Relationship;

@NodeEntity(label = "Customer")
public class Customer {

  @GraphId
  public Long id;

  @Property(name = "customerID")
  public String customerID;

  public String contactName;
  public String contactTitle;
  public String country;
  public String region;
  public String city;
  public String address;
  public String postalCode;
  public String phone;
  public String fax;

  /**
   * (Customer)-[PURCHASED]->(Order)
   */
  @Relationship(type = "PURCHASED", direction = Relationship.OUTGOING)
  public Set<Order> orders = new HashSet<>();

  public Customer() {
  }

  public Customer(Long id, String customerID, String contactTitle, String contactName,
    String address, String city, String postalCode, String country, String region,
    String phone, String fax) {
    this.id = id;
    this.customerID = customerID;
    this.contactTitle = contactTitle;
    this.contactName = contactName;
    this.address = address;
    this.city = city;
    this.postalCode = postalCode;
    this.country = country;
    this.region = region;
    this.phone = phone;
    this.fax = fax;
  }

}
CustomerRepository.java
package com.example.sdn4m1.repository.northwind;

import java.util.Map;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Repository;

import com.example.sdn4m1.domain.northwind.Customer;

@Repository
public interface CustomerRepository extends GraphRepository<Customer> {

  @Query("MATCH (c:Customer) WHERE c.customerID = {0} RETURN c LIMIT 1")
  Customer findByCustomerID(String customerID);

  @Query("MATCH (c:Customer) RETURN c.customerID AS customerID, c.contactName AS contactName ORDER BY c.contactName ASC")
  Iterable<Map<String, Object>> customerIDs();

}

Order

Order.java
package com.example.sdn4m1.domain.northwind;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import org.neo4j.ogm.annotation.GraphId;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Property;
import org.neo4j.ogm.annotation.Relationship;
import org.neo4j.ogm.annotation.typeconversion.DateString;

@NodeEntity(label = "Order")
public class Order {

  @GraphId
  public Long id;

  @Property(name = "orderID")
  public Integer orderID;

  @DateString(value = "yyyy-MM-dd HH:mm:ss.SSS")
  public Date orderDate;
  public String shipName;
  public String shipCountry;
  public String shipRegion;
  public String shipCity;
  public String shipAddress;
  public String shipPostalCode;
  @DateString(value = "yyyy-MM-dd HH:mm:ss.SSS")
  public Date shippedDate;
  @DateString(value = "yyyy-MM-dd HH:mm:ss.SSS")
  public Date requiredDate;
  public String shipVia;
  public String freight;
  public String customerID;
  public Integer employeeID;

  /**
   * (Order)-[ORDERS]->(Product)
   */
  @Relationship(type = "ORDERS", direction = Relationship.OUTGOING)
  public Set<Product> products = new HashSet<>();

  /**
   * (Order)<-[PURCHASED]-(Customer)
   */
  @Relationship(type = "PURCHASED", direction = Relationship.INCOMING)
  public Customer customer;

  public Order() {
  }

  public Order(Long id, Integer orderID, Date orderDate, String shipAddress,
      String shipRegion, String freight, String shipCity, String shipCountry,
      String shipName, Date shippedDate, Date requiredDate, String shipPostalCode,
      String shipVia, String customerID, Integer employeeID) {
    this.id = id;
    this.orderID = orderID;
    this.orderDate = orderDate;
    this.shipAddress = shipAddress;
    this.shipRegion = shipRegion;
    this.freight = freight;
    this.shipCity = shipCity;
    this.shipCountry = shipCountry;
    this.shipName = shipName;
    this.shippedDate = shippedDate;
    this.requiredDate = requiredDate;
    this.shipPostalCode = shipPostalCode;
    this.shipVia = shipVia;
    this.customerID = customerID;
    this.employeeID = employeeID;
  }

}
OrderRepository.java
package com.example.sdn4m1.repository.northwind;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Repository;

import com.example.sdn4m1.domain.northwind.Order;

@Repository
public interface OrderRepository extends GraphRepository<Order> {

  @Query("MATCH (o:Order) WHERE o.shipName =~ {0} RETURN COUNT(o)")
  Long countByShipNameLike(String name);

  @Query("MATCH (o:Order) WHERE o.shipName =~ {0} RETURN o SKIP {1} LIMIT {2}")
  Iterable<Order> findByShipNameLike(String name, int skip, int size);

  @Query("MATCH (o:Order) RETURN MAX(o.orderID)")
  Integer maxOrderID();

}

Product

Product.java
package com.example.sdn4m1.domain.northwind;

import java.util.HashSet;
import java.util.Set;

import org.neo4j.ogm.annotation.GraphId;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Property;
import org.neo4j.ogm.annotation.Relationship;

@NodeEntity(label = "Product")
public class Product {

  @GraphId
  public Long id;

  @Property(name = "productID")
  public Integer productID;

  public String productName;
  public String quantityPerUnit;
  public Double unitPrice;
  public Integer unitsInStock;
  public Integer unitsOnOrder;
  public Integer reorderLevel;
  public Boolean discontinued;
  public Integer supplierID;
  public Integer categoryID;

  /**
   * (Product)<-[ORDERS]-(Order)
   */
  @Relationship(type = "ORDERS", direction = Relationship.INCOMING)
  public Set<Order> orders = new HashSet<>();

  /**
   * (Product)<-[SUPPLIES]-(Supplier)
   */
  @Relationship(type = "SUPPLIES", direction = Relationship.INCOMING)
  public Supplier supplier;

  /**
   * (Product)-[PART_OF]->(Category)
   */
  @Relationship(type = "PART_OF", direction = Relationship.OUTGOING)
  public Category category;

  public Product() {
  }

  public Product(Long id, Integer productID, String productName, Integer supplierID,
      Integer categoryID, String quantityPerUnit, Double unitPrice, Integer unitsInStock,
      Integer unitsOnOrder, Integer reorderLevel, Boolean discontinued) {
    this.id = id;
    this.productID = productID;
    this.productName = productName;
    this.supplierID = supplierID;
    this.categoryID = categoryID;
    this.quantityPerUnit = quantityPerUnit;
    this.unitPrice = unitPrice;
    this.unitsInStock = unitsInStock;
    this.unitsOnOrder = unitsOnOrder;
    this.reorderLevel = reorderLevel;
    this.discontinued = discontinued;
  }

}
ProductRepository.java
package com.example.sdn4m1.repository.northwind;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Repository;

import com.example.sdn4m1.domain.northwind.Product;

@Repository
public interface ProductRepository extends GraphRepository<Product> {

  @Query("MATCH (p:Product) WHERE p.productName =~ {0} RETURN COUNT(p)")
  Long countByNameLike(String name);

  @Query("MATCH (p:Product) WHERE p.productName =~ {0} RETURN p")
  Iterable<Product> findByNameLike(String name);

  @Query("MATCH (p:Product) RETURN MAX(p.productID)")
  Integer maxProductID();

}

Supplier

Supplier.java
package com.example.sdn4m1.domain.northwind;

import java.util.HashSet;
import java.util.Set;

import org.neo4j.ogm.annotation.GraphId;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Property;
import org.neo4j.ogm.annotation.Relationship;

@NodeEntity
public class Supplier {

  @GraphId
  public Long id;

  @Property(name = "supplierID")
  public Integer supplierID;

  public String companyName;
  public String contactName;
  public String contactTitle;
  public String homePage;
  public String country;
  public String region;
  public String city;
  public String address;
  public String postalCode;
  public String phone;
  public String fax;

  /**
   * (Supplier)-[SUPPLIES]->(Product)
   */
  @Relationship(type = "SUPPLIES", direction = Relationship.OUTGOING)
  public Set<Product> products = new HashSet<>();

  public Supplier() {
  }

  public Supplier(Long id, Integer supplierID, String contactTitle, String contactName,
      String homePage, String city, String postalCode, String country, String phone,
      String fax, String companyName, String region, String address) {
    this.id = id;
    this.supplierID = supplierID;
    this.contactTitle = contactTitle;
    this.contactName = contactName;
    this.homePage = homePage;
    this.city = city;
    this.postalCode = postalCode;
    this.country = country;
    this.phone = phone;
    this.fax = fax;
    this.companyName = companyName;
    this.region = region;
    this.address = address;
  }
}
SupplierRepository.java
package com.example.sdn4m1.repository.northwind;

import java.util.Map;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Repository;

import com.example.sdn4m1.domain.northwind.Supplier;

@Repository
public interface SupplierRepository extends GraphRepository<Supplier> {

  @Query("MATCH (s:Supplier) WHERE s.supplierID = {0} RETURN s LIMIT 1")
  Supplier findBySupplierID(Integer supplierID);

  @Query("MATCH (s:Supplier) RETURN s.supplierID AS supplierID, s.companyName AS companyName ORDER BY s.companyName ASC")
  Iterable<Map<String, Object>> supplierIDs();

  @Query("MATCH (s:Supplier) RETURN MAX(s.supplierID)")
  Integer maxSupplierID();

}

Category

Category.java
package com.example.sdn4m1.domain.northwind;

import java.util.HashSet;
import java.util.Set;

import org.neo4j.ogm.annotation.GraphId;
import org.neo4j.ogm.annotation.NodeEntity;
import org.neo4j.ogm.annotation.Property;
import org.neo4j.ogm.annotation.Relationship;

@NodeEntity(label = "Category")
public class Category {

  @GraphId
  public Long id;

  @Property(name = "categoryID")
  public Integer categoryID;
  public String categoryName;
  public String description;
  public String picture;

  /**
   * (Category)<-[PART_OF]-(Product)
   */
  @Relationship(type = "PART_OF", direction = Relationship.INCOMING)
  public Set<Product> products = new HashSet<>();

  public Category() {
  }

  public Category(Long id, Integer categoryID, String categoryName, String description,
      String picture) {
    this.id = id;
    this.categoryID = categoryID;
    this.categoryName = categoryName;
    this.description = description;
    this.picture = picture;
  }

}
CategoryRepository.java
package com.example.sdn4m1.repository.northwind;

import java.util.Map;

import org.springframework.data.neo4j.annotation.Query;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Repository;

import com.example.sdn4m1.domain.northwind.Category;

@Repository
public interface CategoryRepository extends GraphRepository<Category> {

  @Query("MATCH (c:Category) WHERE c.categoryID = {0} RETURN c LIMIT 1")
  Category findByCategoryID(Integer categoryID);

  @Query("MATCH (c:Category) RETURN c.categoryID AS categoryID, c.categoryName AS categoryName ORDER BY c.categoryName ASC")
  Iterable<Map<String, Object>> categoryIDs();

  @Query("MATCH (c:Category) RETURN MAX(c.categoryID)")
  Integer maxCategoryID();

}
Service

各サービスクラスが継承する基底クラスを作成します。

GenericCRUDService.java
package com.example.sdn4m1.service;

import java.util.Map;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.neo4j.repository.GraphRepository;

public abstract class GenericCRUDService<T,F> implements Pagination {

  private static final int DEPTH_LIST = 0;
  private static final int DEPTH_ENTITY = 1;

  public long count() {
    return getRepository().count();
  }

  public Iterable<T> findAll(int page, int size, String s) {
    Pageable pager = new PageRequest(currentPage(page), size, Direction.ASC, s);
    return getRepository().findAll(pager, DEPTH_LIST);
  }

  public T findOne(final Long id) {
    return getRepository().findOne(id, DEPTH_ENTITY);
  }

  public void findOneToForm(final Long id, F form) {
    T t = getRepository().findOne(id, DEPTH_ENTITY);
    convertToForm(t, form);
  }

  public T save(final F form, final int depth) {
    T t = convertToEntity(form);
    return getRepository().save(t, depth);
  }

  public void delete(final Long id) {
    getRepository().delete(id);
  }

  public abstract GraphRepository<T> getRepository();
  public abstract void convertToForm(T t, F form);
  public abstract T convertToEntity(F form);
  public abstract Iterable<Map<String, Object>> entityIDs();
  public abstract Integer maxEntityID();

}

ページング

ページング用の情報を保持するクラスとインタフェースを作成します。

PageBean.java
package com.example.sdn4m1.service;

public class PageBean {

  private int totalCount;
  private int currentPage;
  private int maxPage;

  public int getTotalCount() {
    return totalCount;
  }
  public void setTotalCount(int totalCount) {
    this.totalCount = totalCount;
  }
  public int getCurrentPage() {
    return currentPage;
  }
  public void setCurrentPage(int currentPage) {
    this.currentPage = currentPage;
  }
  public int getMaxPage() {
    return maxPage;
  }
  public void setMaxPage(int maxPage) {
    this.maxPage = maxPage;
  }
}
Pagination.java
package com.example.sdn4m1.service;

public interface Pagination {

  default int currentPage(final int pageNo) {
    int calcPage = pageNo - 1;
    if (calcPage < 0) {
      calcPage = 0;
    }
    return calcPage;
  }

  default int maxPage(final int max, final int pageSize) {
    int calcPage = max / pageSize;
    if (max % pageSize != 0) {
      calcPage++;
    };
    return calcPage;
  }

  default PageBean calcPage(final int max, final int pageNo, final int pageSize) {
    PageBean page = new PageBean();
    page.setTotalCount(max);
    page.setCurrentPage(currentPage(pageNo) + 1);
    page.setMaxPage(maxPage(max, pageSize));
    return page;
  }
}

上記のGenericCRUDService基底クラスを継承した、各ノード用のサービスクラスを作成します。

Customer

CustomerService.java
package com.example.sdn4m1.service.northwind;

import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Service;

import com.example.sdn4m1.domain.northwind.Customer;
import com.example.sdn4m1.repository.northwind.CustomerRepository;
import com.example.sdn4m1.service.GenericCRUDService;
import com.example.sdn4m1.web.northwind.CustomerForm;

@Service
public class CustomerService extends GenericCRUDService<Customer, CustomerForm> {
  final static Logger logger = LoggerFactory.getLogger(CustomerService.class);

  @Autowired
  CustomerRepository customerRepository;

  @Override
  public GraphRepository<Customer> getRepository() {
    return customerRepository;
  }

  @Override
  public Iterable<Map<String, Object>> entityIDs() {
    return customerRepository.customerIDs();
  }

  @Override
  public void convertToForm(Customer customer, CustomerForm form) {
    form.setId(customer.id.toString());
    form.setCustomerID(customer.customerID);
    form.setContactName(customer.contactName);
    form.setContactTitle(customer.contactTitle);
    form.setCountry(customer.country);
    form.setRegion(customer.region);
    form.setCity(customer.city);
    form.setAddress(customer.address);
    form.setPostalCode(customer.postalCode);
    form.setPhone(customer.phone);
    form.setFax(customer.fax);
  }

  @Override
  public Customer convertToEntity(CustomerForm form) {
    Customer customer = new Customer();
    if (StringUtils.isNotEmpty(form.getId())) {
      customer.id = Long.valueOf(form.getId());
    } else {
      customer.id = null; //new node
    }
    customer.customerID = form.getCustomerID();
    customer.contactName = form.getContactName();
    customer.contactTitle = ServiceUtils.nvl(form.getContactTitle());
    customer.country = form.getCountry();
    customer.region = form.getRegion();
    customer.city = form.getCity();
    customer.address = form.getAddress();
    customer.postalCode = ServiceUtils.nvl(form.getPostalCode());
    customer.phone = form.getPhone();
    customer.fax = ServiceUtils.nvl(form.getFax());
    return customer;
  }

  @Override
  public Integer maxEntityID() {
    return null;
  }
}

Order

OrderService.java
package com.example.sdn4m1.service.northwind;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Service;

import com.example.sdn4m1.domain.northwind.Order;
import com.example.sdn4m1.domain.northwind.Product;
import com.example.sdn4m1.repository.northwind.CustomerRepository;
import com.example.sdn4m1.repository.northwind.OrderRepository;
import com.example.sdn4m1.repository.northwind.ProductRepository;
import com.example.sdn4m1.service.GenericCRUDService;
import com.example.sdn4m1.web.northwind.OrderForm;

@Service
public class OrderService extends GenericCRUDService<Order, OrderForm> {
  final static Logger logger = LoggerFactory.getLogger(OrderService.class);

  @Autowired
  OrderRepository orderRepository;

  @Autowired
  CustomerRepository customerRepository;

  @Autowired
  ProductRepository productRepository;

  @Override
  public GraphRepository<Order> getRepository() {
    return orderRepository;
  }

  @Override
  public Iterable<Map<String, Object>> entityIDs() {
    return null;
  }

  @Override
  public void convertToForm(Order order, OrderForm form) {
    form.setId(order.id.toString());
    form.setOrderID(order.orderID.toString());
    form.setOrderDate(order.orderDate);
    form.setShipName(order.shipName);
    form.setShipCountry(order.shipCountry);
    form.setShipRegion(order.shipRegion);
    form.setShipCity(order.shipCity);
    form.setShipAddress(order.shipAddress);
    form.setShipPostalCode(order.shipPostalCode);
    form.setShippedDate(order.shippedDate);
    form.setRequiredDate(order.requiredDate);
    form.setShipVia(order.shipVia);
    form.setFreight(order.freight);
    form.setEmployeeID(ServiceUtils.nvl(order.employeeID));
    form.setCustomerID(order.customerID);

    if (order.products != null) {
      order.products.stream().forEach(p ->{
        form.getProducts().put(p.productID, p.productName);
      });
    }

  }

  @Override
  public Order convertToEntity(OrderForm form) {
    Order order = new Order();
    if (StringUtils.isNotEmpty(form.getId())) {
      order.id = Long.valueOf(form.getId());
    } else {
      order.id = null; //new node
    }
    if (StringUtils.isNotEmpty(form.getOrderID())) {
      order.orderID = Integer.valueOf(form.getOrderID());
    } else {
      order.orderID = maxEntityID() + 1; //new node
    }
    order.orderDate = form.getOrderDate();
    order.shipName = form.getShipName();
    order.shipCountry = form.getShipCountry();
    order.shipRegion = form.getShipRegion();
    order.shipCity = form.getShipCity();
    order.shipAddress = form.getShipAddress();
    order.shipPostalCode = ServiceUtils.nvl(form.getShipPostalCode());
    order.shippedDate = form.getShippedDate();
    order.requiredDate = form.getRequiredDate();
    order.shipVia = form.getShipVia();
    order.freight = form.getFreight();
    order.employeeID = ServiceUtils.nvlToInt(form.getEmployeeID());

    //retrieve customer
    if (StringUtils.isNotEmpty(form.getCustomerID())) {
      order.customerID = form.getCustomerID();
      order.customer = customerRepository.findByCustomerID(order.customerID);
    } else {
      throw new RuntimeException("Customer not found");
    }

    //retrieve products
    if (form.getProducts() != null) {
      Set<Product> p = new HashSet<>();
      form.getProducts().forEach((key,value)->{
        p.add(productRepository.findOne(Long.valueOf(key), 0));
      });
      order.products = p;
    } else {
      order.products = null;
    }

    return order;
  }

  public long countByShipNameLike(String name) {
    return orderRepository.countByShipNameLike(ServiceUtils.nameLike(name));
  }

  public Iterable<Order> findByShipNameLike(String name, int page, int size) {
    int skip = (page - 1) * size;
    return orderRepository.findByShipNameLike(ServiceUtils.nameLike(name), skip, size);
  }

  @Override
  public Integer maxEntityID() {
    return orderRepository.maxOrderID();
  }

}

Product

ProductService.java
package com.example.sdn4m1.service.northwind;

import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Service;

import com.example.sdn4m1.domain.northwind.Product;
import com.example.sdn4m1.repository.northwind.CategoryRepository;
import com.example.sdn4m1.repository.northwind.ProductRepository;
import com.example.sdn4m1.repository.northwind.SupplierRepository;
import com.example.sdn4m1.service.GenericCRUDService;
import com.example.sdn4m1.web.northwind.ProductForm;

@Service
public class ProductService extends GenericCRUDService<Product, ProductForm> {
  final static Logger logger = LoggerFactory.getLogger(ProductService.class);

  @Autowired
  ProductRepository productRepository;

  @Autowired
  CategoryRepository categoryRepository;

  @Autowired
  SupplierRepository supplierRepository;

  @Override
  public GraphRepository<Product> getRepository() {
    return productRepository;
  }

  @Override
  public Iterable<Map<String, Object>> entityIDs() {
    return null;
  }

  @Override
  public void convertToForm(Product product, ProductForm form) {
    form.setId(product.id.toString());
    form.setProductID(product.productID.toString());
    form.setProductName(product.productName);
    form.setQuantityPerUnit(product.quantityPerUnit);
    form.setUnitPrice(product.unitPrice.toString());
    form.setUnitsInStock(product.unitsInStock.toString());
    form.setUnitsOnOrder(product.unitsOnOrder.toString());
    form.setReorderLevel(product.reorderLevel.toString());
    form.setDiscontinued(product.discontinued.toString());
    form.setSupplierID(product.supplierID.toString());
    form.setCategoryID(product.categoryID.toString());
  }

  @Override
  public Product convertToEntity(ProductForm form) {
    Product product = new Product();
    if (StringUtils.isNotEmpty(form.getId())) {
      product.id = Long.valueOf(form.getId());
    } else {
      product.id = null; //new node
    }
    if (StringUtils.isNotEmpty(form.getProductID())) {
      product.productID = Integer.valueOf(form.getProductID());
    } else {
      product.productID = maxEntityID() + 1; //new node
    }
    product.productName = form.getProductName();
    product.quantityPerUnit = form.getQuantityPerUnit();
    product.unitPrice = Double.valueOf(form.getUnitPrice());
    product.unitsInStock = Integer.valueOf(form.getUnitsInStock());
    product.unitsOnOrder = Integer.valueOf(form.getUnitsOnOrder());
    product.reorderLevel = Integer.valueOf(form.getReorderLevel());
    product.discontinued = Boolean.valueOf(form.getDiscontinued());

    //retrieve supplier
    if (StringUtils.isNotEmpty(form.getSupplierID())) {
      product.supplierID = Integer.valueOf(form.getSupplierID());
      product.supplier = supplierRepository.findBySupplierID(product.supplierID);
    } else {
      throw new RuntimeException("supplier not found");
    }

    //retrieve category
    if (StringUtils.isNotEmpty(form.getCategoryID())) {
      product.categoryID = Integer.valueOf(form.getCategoryID());
      product.category = categoryRepository.findByCategoryID(product.categoryID);
    } else {
      throw new RuntimeException("category not found");
    }

    return product;
  }

  public long countByNameLike(String name) {
    return productRepository.countByNameLike(ServiceUtils.nameLike(name));
  }

  public Iterable<Product> findByNameLike(String name, int page, int size) {
    return productRepository.findByNameLike(ServiceUtils.nameLike(name));
  }

  @Override
  public Integer maxEntityID() {
    return productRepository.maxProductID();
  }

}

Supplier

SupplierService.java
package com.example.sdn4m1.service.northwind;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Service;

import com.example.sdn4m1.domain.northwind.Product;
import com.example.sdn4m1.domain.northwind.Supplier;
import com.example.sdn4m1.repository.northwind.ProductRepository;
import com.example.sdn4m1.repository.northwind.SupplierRepository;
import com.example.sdn4m1.service.GenericCRUDService;
import com.example.sdn4m1.web.northwind.SupplierForm;

@Service
public class SupplierService extends GenericCRUDService<Supplier, SupplierForm> {
  final static Logger logger = LoggerFactory.getLogger(SupplierService.class);

  @Autowired
  SupplierRepository supplierRepository;

  @Autowired
  ProductRepository productRepository;

  @Override
  public GraphRepository<Supplier> getRepository() {
    return supplierRepository;
  }

  @Override
  public Iterable<Map<String, Object>> entityIDs() {
    return supplierRepository.supplierIDs();
  }

  @Override
  public void convertToForm(Supplier supplier, SupplierForm form) {
    form.setId(supplier.id.toString());
    form.setSupplierID(supplier.supplierID.toString());
    form.setCompanyName(supplier.companyName);
    form.setContactName(supplier.contactName);
    form.setContactTitle(supplier.contactTitle);
    form.setHomePage(supplier.homePage);
    form.setCountry(supplier.country);
    form.setRegion(supplier.region);
    form.setCity(supplier.city);
    form.setAddress(supplier.address);
    form.setPostalCode(supplier.postalCode);
    form.setPhone(supplier.phone);
    form.setFax(supplier.fax);

    if (supplier.products != null) {
      supplier.products.stream().forEach(p ->{
        form.getProducts().put(p.productID, p.productName);
      });
    }

  }

  @Override
  public Supplier convertToEntity(SupplierForm form) {
    Supplier supplier = new Supplier();
    if (StringUtils.isNotEmpty(form.getId())) {
      supplier.id = Long.valueOf(form.getId());
    } else {
      supplier.id = null; //new node
    }
    if (StringUtils.isNotEmpty(form.getSupplierID())) {
      supplier.supplierID = Integer.valueOf(form.getSupplierID());
    } else {
      supplier.supplierID = maxEntityID() + 1; //new node
    }
    supplier.companyName = form.getCompanyName();
    supplier.contactName = form.getContactName();
    supplier.contactTitle = form.getContactTitle();
    supplier.homePage = ServiceUtils.nvl(form.getHomePage());
    supplier.country = form.getCountry();
    supplier.region = form.getRegion();
    supplier.city = form.getCity();
    supplier.address = form.getAddress();
    supplier.postalCode = ServiceUtils.nvl(form.getPostalCode());
    supplier.phone = form.getPhone();
    supplier.fax = ServiceUtils.nvl(form.getFax());

    //retrieve products
    if (form.getProducts() != null) {
      Set<Product> p = new HashSet<>();
      form.getProducts().forEach((key,value)->{
        p.add(productRepository.findOne(Long.valueOf(key), 0));
      });
      supplier.products = p;
    } else {
      supplier.products = null;
    }

    return supplier;
  }

  @Override
  public Integer maxEntityID() {
    return supplierRepository.maxSupplierID();
  }

}

Category

CategoryService.java
package com.example.sdn4m1.service.northwind;

import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.repository.GraphRepository;
import org.springframework.stereotype.Service;

import com.example.sdn4m1.domain.northwind.Category;
import com.example.sdn4m1.repository.northwind.CategoryRepository;
import com.example.sdn4m1.service.GenericCRUDService;
import com.example.sdn4m1.web.northwind.CategoryForm;

@Service
public class CategoryService extends GenericCRUDService<Category, CategoryForm> {
  final static Logger logger = LoggerFactory.getLogger(CategoryService.class);

  @Autowired
  CategoryRepository categoryRepository;

  @Override
  public GraphRepository<Category> getRepository() {
    return categoryRepository;
  }

  @Override
  public Iterable<Map<String, Object>> entityIDs() {
    return categoryRepository.categoryIDs();
  }

  @Override
  public void convertToForm(Category category, CategoryForm form) {
    form.setId(category.id.toString());
    form.setCategoryID(category.categoryID.toString());
    form.setCategoryName(category.categoryName);
    form.setDescription(category.description);
    form.setPicture(category.picture);
  }

  @Override
  public Category convertToEntity(CategoryForm form) {
    Category category = new Category();
    if (StringUtils.isNotEmpty(form.getId())) {
      category.id = Long.valueOf(form.getId());
    } else {
      category.id = null; //new node
    }
    if (StringUtils.isNotEmpty(form.getCategoryID())) {
      category.categoryID = Integer.valueOf(form.getCategoryID());
    } else {
      category.categoryID = maxEntityID() + 1; //new node
    }
    category.categoryName = form.getCategoryName();
    category.description = ServiceUtils.nvl(form.getDescription());
    category.picture = ServiceUtils.nvl(form.getPicture());
    return category;
  }

  @Override
  public Integer maxEntityID() {
    return categoryRepository.maxCategoryID();
  }

}

Recommend

RecommendService.java
package com.example.sdn4m1.service.northwind;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.neo4j.template.Neo4jOperations;
import org.springframework.stereotype.Service;

import com.example.sdn4m1.domain.northwind.Product;

@Service
public class RecommendService {
  final static Logger logger = LoggerFactory.getLogger(RecommendService.class);

  @Autowired
  Neo4jOperations neo4jOperations;

  final static String RECOMMEND =
  "MATCH (uc:Customer)-[:PURCHASED]->(uo:Order)-[:ORDERS]->(up:Product) " +
  "WHERE uc.customerID = {customerID} AND uo.orderID = {orderID} " +
  "WITH uc, up " +
  //同じ商品を注文したことがある他のカスタマーが注文した商品の一覧
  "MATCH (up)<-[:ORDERS]-(:Order)<-[:PURCHASED]-(ac:Customer)-[:PURCHASED]->(aco:Order)-[:ORDERS]->(acp:Product) " +
  //その商品から注文したことのある商品は除外
  "WHERE NOT (uc)-[:PURCHASED]->(:Order)-[:ORDERS]->(acp) " +
  "RETURN DISTINCT acp AS product, COUNT(*) AS cnt ORDER BY cnt LIMIT 5";

  public Iterable<Product> recommendProducts(final String customerID, final Integer orderID) {
    Map<String, Object> params = new HashMap<>();
    params.put("customerID", customerID);
    params.put("orderID", orderID);
    Iterable<Product> result = neo4jOperations.queryForObjects(Product.class, RECOMMEND, params);
    return result;
  }

}
ServiceUtils

サービスクラスが使用するユーティリティメソッドを集めたクラスを作成します。

ServiceUtils.java
package com.example.sdn4m1.service.northwind;

public class ServiceUtils {

  public static String nameLike(String name) {
    return "(?i).*" + (name != null ? name : "") + ".*";
  }

 public static String nvl(String s) {
   if (s != null && s.length() > 0) {
     return s;
   }
   return null;
 }

 public static String nvl(Integer i) {
   if (i != null && i > 0) {
     return i.toString();
   }
   return null;
 }

 public static Integer nvlToInt(String s) {
   if (s != null && s.length() > 0) {
     return Integer.valueOf(s);
   }
   return null;
 }

 public static boolean isNotEmpty(Integer i) {
   if (i != null && i > 0) {
     return true;
   }
   return false;
 }

}
Controller/Form

コントローラーの共通メソッドを定義したNorthWindControllerクラスを作成します。
各ノードのコントローラーはこのクラスを継承します。

NorthWindController.java
package com.example.sdn4m1.web.northwind;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;

import com.example.sdn4m1.service.PageBean;

@Controller
public class NorthWindController {

  @InitBinder
  public void initBinder(WebDataBinder binder) {
    binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    sdf.setLenient(false);
    binder.registerCustomEditor(Date.class, new CustomDateEditor(sdf, true));
  }

  void addPageAttr(final PageBean page, Model model) {
    model.addAttribute("totalCount", page.getTotalCount());
    model.addAttribute("currentPage", page.getCurrentPage());
    model.addAttribute("maxPage", page.getMaxPage());
  }

  void addHeaderAttr(final Map<String, String> msg, Model model) {
    model.addAttribute("header", msg);
  }

  Map<Integer, String> iteConv(final Iterable<Map<String, Object>> list,
      final String name, final String id) {
    Map<Integer, String> map = new HashMap<Integer, String>();
    list.forEach(action -> {
      Integer key = (Integer)action.get(id);
      String value = (String)action.get(name);
      map.put(key, value);
    });
    return map;
  }

  void addListIDsAttr(final Iterable<Map<String, Object>> items,
      final String itemName, final String name, final String id, Model model) {
    Map<Integer, String> ids = iteConv(items, name, id);
    model.addAttribute(itemName, ids);
  }

}

Customer

CustomerController.java
package com.example.sdn4m1.web.northwind;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.sdn4m1.domain.northwind.Customer;
import com.example.sdn4m1.service.northwind.CustomerService;

@Controller
@RequestMapping(value = "/customer")
public class CustomerController extends NorthWindController {
  final static Logger logger = LoggerFactory.getLogger(CustomerController.class);

  final static int PAGE_SIZE = 10;

  @Autowired
  CustomerService customerService;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String index(Model model) {
    return index(1, model);
  }

  @RequestMapping(value = "/{pageNo}", method = RequestMethod.GET)
  public String index(
    @PathVariable Integer pageNo,
    Model model) {
    Iterable<Customer> result = customerService.findAll(pageNo, PAGE_SIZE, "customerID");
    model.addAttribute("result", result);
    int totalCount = (int)customerService.count();
    addPageAttr(customerService.calcPage(totalCount, pageNo, PAGE_SIZE), model);
    addHeaderAttr(HEADER_INDEX, model);
    return "Customer/index";
  }

  @RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
  public String detail(
    @PathVariable Long id,
    Model model) {
    Customer customer = customerService.findOne(id);
    model.addAttribute("customer", customer);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Customer/detail";
  }

  @RequestMapping(value = "/create", method = RequestMethod.GET)
  public String create(
    CustomerForm form,
    Model model) {
    addHeaderAttr(HEADER_CREATE, model);
    return "Customer/create";
  }

  @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
  public String edit(
    @PathVariable Long id,
    CustomerForm form,
    Model model) {
    customerService.findOneToForm(id, form);
    addHeaderAttr(HEADER_EDIT, model);
    return "Customer/create";
  }

  @RequestMapping(value = "/save", method = RequestMethod.POST)
  public String save(
    @Validated @ModelAttribute CustomerForm form,
    BindingResult result,
    Model model) {
    if (result.hasErrors()) {
      model.addAttribute("errorMessage", "validation error");
      return create(form, model);
    }
    Customer customer = customerService.save(form, 0);
    model.addAttribute("customer", customer);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Customer/detail";
  }

  @RequestMapping(value = "/delete", method = RequestMethod.POST)
  public String delete(
    @RequestParam(required = true) Long id) {
    customerService.delete(id);
    return "redirect:/customer/";
  }

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_INDEX =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Customer");
        put("subtitle", "list");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_DETAIL =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Customer");
        put("subtitle", "detail");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_CREATE =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Customer");
        put("subtitle", "create");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_EDIT =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Customer");
        put("subtitle", "edit");
    }});

}
CustomerForm.java
package com.example.sdn4m1.web.northwind;

import java.io.Serializable;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

public class CustomerForm implements Serializable {

  private static final long serialVersionUID = -8517696470399982702L;

  //NodeID
  private String id;

  @NotNull
  private String customerID;

  @NotNull
  @Size(min=1, max=256)
  private String contactName;
  @Size(min=1, max=256)
  private String contactTitle;
  @NotNull
  private String country;
  @NotNull
  private String region;
  @NotNull
  private String city;
  @NotNull
  private String address;
  private String postalCode;
  @NotNull
  private String phone;
  private String fax;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getCustomerID() {
    return customerID;
  }
  public void setCustomerID(String customerID) {
    this.customerID = customerID;
  }
  public String getContactName() {
    return contactName;
  }
  public void setContactName(String contactName) {
    this.contactName = contactName;
  }
  public String getContactTitle() {
    return contactTitle;
  }
  public void setContactTitle(String contactTitle) {
    this.contactTitle = contactTitle;
  }
  public String getCountry() {
    return country;
  }
  public void setCountry(String country) {
    this.country = country;
  }
  public String getRegion() {
    return region;
  }
  public void setRegion(String region) {
    this.region = region;
  }
  public String getCity() {
    return city;
  }
  public void setCity(String city) {
    this.city = city;
  }
  public String getAddress() {
    return address;
  }
  public void setAddress(String address) {
    this.address = address;
  }
  public String getPostalCode() {
    return postalCode;
  }
  public void setPostalCode(String postalCode) {
    this.postalCode = postalCode;
  }
  public String getPhone() {
    return phone;
  }
  public void setPhone(String phone) {
    this.phone = phone;
  }
  public String getFax() {
    return fax;
  }
  public void setFax(String fax) {
    this.fax = fax;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }

}

Order

OrderController.java
package com.example.sdn4m1.web.northwind;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.sdn4m1.domain.northwind.Order;
import com.example.sdn4m1.domain.northwind.Product;
import com.example.sdn4m1.service.northwind.OrderService;
import com.example.sdn4m1.service.northwind.RecommendService;

@Controller
@RequestMapping(value = "/order")
public class OrderController extends NorthWindController {
  final static Logger logger = LoggerFactory.getLogger(OrderController.class);

  final static int PAGE_SIZE = 50;

  @Autowired
  OrderService orderService;

  @Autowired
  RecommendService recommendService;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String index(Model model) {
    return index(1, null, model);
  }

  @RequestMapping(value = "/{pageNo}", method = RequestMethod.GET)
  public String index(
    @PathVariable Integer pageNo,
    @RequestParam(required = false, defaultValue = "") String searchName,
    Model model) {
    int totalCount = 0;
    Iterable<Order> result;
    if (StringUtils.isNotEmpty(searchName)) {
      result = orderService.findByShipNameLike(searchName, pageNo, PAGE_SIZE);
      totalCount = (int)orderService.countByShipNameLike(searchName);
    } else {
      result = orderService.findAll(pageNo, PAGE_SIZE, "orderID");
      totalCount = (int)orderService.count();
    }
    model.addAttribute("result", result);
    addPageAttr(orderService.calcPage(totalCount, pageNo, PAGE_SIZE), model);
    model.addAttribute("searchName", searchName);
    addHeaderAttr(HEADER_INDEX, model);
    return "Order/index";
  }

  @RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
  public String detail(
    @PathVariable Long id,
    Model model) {
    Order order = orderService.findOne(id);
    model.addAttribute("order", order);
    Iterable<Product> recommend = recommendService.recommendProducts(order.customerID, order.orderID);
    model.addAttribute("recommend", recommend);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Order/detail";
  }

  @RequestMapping(value = "/create", method = RequestMethod.GET)
  public String create(
    OrderForm form,
    Model model) {
    addHeaderAttr(HEADER_CREATE, model);
    return "Order/create";
  }

  @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
  public String edit(
    @PathVariable Long id,
    OrderForm form,
    Model model) {
    orderService.findOneToForm(id, form);
    addHeaderAttr(HEADER_EDIT, model);
    return "Order/create";
  }

  @RequestMapping(value = "/save", method = RequestMethod.POST)
  public String save(
    @Validated @ModelAttribute OrderForm form,
    BindingResult result,
    Model model) {
    if (result.hasErrors()) {
      model.addAttribute("errorMessage", "validation error");
      return create(form, model);
    }
    Order order = orderService.save(form, 0);
    model.addAttribute("order", order);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Order/detail";
  }

  @RequestMapping(value = "/delete", method = RequestMethod.POST)
  public String delete(
    @RequestParam(required = true) Long id) {
    orderService.delete(id);
    return "redirect:/order/";
  }

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_INDEX =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Order");
        put("subtitle", "list");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_DETAIL =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Order");
        put("subtitle", "detail");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_CREATE =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Order");
        put("subtitle", "create");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_EDIT =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Order");
        put("subtitle", "edit");
    }});
}
OrderForm.java
package com.example.sdn4m1.web.northwind;

import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.springframework.format.annotation.DateTimeFormat;

public class OrderForm implements Serializable {

  private static final long serialVersionUID = -1268572984219044466L;

  //NodeID
  private String id;

  //EntityID
  @DecimalMin("0")
  @DecimalMax("999999")
  private String orderID;

  @NotNull
  @DateTimeFormat(pattern = "yyyy/MM/dd")
  private Date orderDate;
  @NotNull
  @Size(min=1, max=256)
  private String shipName;
  @NotNull
  private String shipCountry;
  @NotNull
  private String shipRegion;
  @NotNull
  private String shipCity;
  @NotNull
  private String shipAddress;
  private String shipPostalCode;
  @DateTimeFormat(pattern = "yyyy/MM/dd")
  private Date shippedDate;
  @DateTimeFormat(pattern = "yyyy/MM/dd")
  private Date requiredDate;
  @NotNull
  @Min(1)
  @Max(3)
  private String shipVia;
  @NotNull
  private String freight;
  @NotNull
  @Size(min=5, max=5)
  private String customerID;
  private String employeeID;

  private Map<Integer, String> products = new HashMap<Integer, String>();

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getOrderID() {
    return orderID;
  }
  public void setOrderID(String orderID) {
    this.orderID = orderID;
  }
  public Date getOrderDate() {
    return orderDate;
  }
  public void setOrderDate(Date orderDate) {
    this.orderDate = orderDate;
  }
  public String getShipName() {
    return shipName;
  }
  public void setShipName(String shipName) {
    this.shipName = shipName;
  }
  public String getShipCountry() {
    return shipCountry;
  }
  public void setShipCountry(String shipCountry) {
    this.shipCountry = shipCountry;
  }
  public String getShipRegion() {
    return shipRegion;
  }
  public void setShipRegion(String shipRegion) {
    this.shipRegion = shipRegion;
  }
  public String getShipCity() {
    return shipCity;
  }
  public void setShipCity(String shipCity) {
    this.shipCity = shipCity;
  }
  public String getShipAddress() {
    return shipAddress;
  }
  public void setShipAddress(String shipAddress) {
    this.shipAddress = shipAddress;
  }
  public String getShipPostalCode() {
    return shipPostalCode;
  }
  public void setShipPostalCode(String shipPostalCode) {
    this.shipPostalCode = shipPostalCode;
  }
  public Date getShippedDate() {
    return shippedDate;
  }
  public void setShippedDate(Date shippedDate) {
    this.shippedDate = shippedDate;
  }
  public Date getRequiredDate() {
    return requiredDate;
  }
  public void setRequiredDate(Date requiredDate) {
    this.requiredDate = requiredDate;
  }
  public String getShipVia() {
    return shipVia;
  }
  public void setShipVia(String shipVia) {
    this.shipVia = shipVia;
  }
  public String getFreight() {
    return freight;
  }
  public void setFreight(String freight) {
    this.freight = freight;
  }
  public String getCustomerID() {
    return customerID;
  }
  public void setCustomerID(String customerID) {
    this.customerID = customerID;
  }
  public String getEmployeeID() {
    return employeeID;
  }
  public void setEmployeeID(String employeeID) {
    this.employeeID = employeeID;
  }

  public Map<Integer, String> getProducts() {
    return products;
  }
  public void setProducts(Map<Integer, String> products) {
    this.products = products;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }

}

Product

ProductController.java
package com.example.sdn4m1.web.northwind;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.sdn4m1.domain.northwind.Product;
import com.example.sdn4m1.service.northwind.CategoryService;
import com.example.sdn4m1.service.northwind.ProductService;
import com.example.sdn4m1.service.northwind.SupplierService;

@Controller
@RequestMapping(value = "/product")
public class ProductController extends NorthWindController {
  final static Logger logger = LoggerFactory.getLogger(ProductController.class);

  final static int PAGE_SIZE = 20;

  @Autowired
  ProductService productService;

  @Autowired
  CategoryService categoryService;

  @Autowired
  SupplierService supplierService;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String index(Model model) {
    return index(1, null, model);
  }

  @RequestMapping(value = "/{pageNo}", method = RequestMethod.GET)
  public String index(
    @PathVariable Integer pageNo,
    @RequestParam(required = true) String searchName,
    Model model) {
    int totalCount = 0;
    Iterable<Product> result;
    if (StringUtils.isNotEmpty(searchName)) {
      result = productService.findByNameLike(searchName, pageNo, PAGE_SIZE);
      totalCount = (int)productService.countByNameLike(searchName);
    } else {
      result = productService.findAll(pageNo, PAGE_SIZE, "productID");
      totalCount = (int)productService.count();
    }
    model.addAttribute("result", result);
    addPageAttr(productService.calcPage(totalCount, pageNo, PAGE_SIZE), model);
    model.addAttribute("searchName", searchName);
    addHeaderAttr(HEADER_INDEX, model);
    return "Product/index";
  }

  @RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
  public String detail(
    @PathVariable Long id,
    Model model) {
    Product product = productService.findOne(id);
    model.addAttribute("product", product);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Product/detail";
  }

  @RequestMapping(value = "/create", method = RequestMethod.GET)
  public String create(
    ProductForm form,
    Model model) {
    Iterable<Map<String, Object>> categoryIDs = categoryService.entityIDs();
    addListIDsAttr(categoryIDs, "selectCategoryIDs", "categoryName" ,"categoryID", model);
    Iterable<Map<String, Object>> supplierIDs = supplierService.entityIDs();
    addListIDsAttr(supplierIDs, "selectSupplierIDs", "companyName" ,"supplierID", model);
    addHeaderAttr(HEADER_CREATE, model);
    return "Product/create";
  }

  @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
  public String edit(
    @PathVariable Long id,
    ProductForm form,
    Model model) {
    Iterable<Map<String, Object>> categoryIDs = categoryService.entityIDs();
    addListIDsAttr(categoryIDs, "selectCategoryIDs", "categoryName" ,"categoryID", model);
    Iterable<Map<String, Object>> supplierIDs = supplierService.entityIDs();
    addListIDsAttr(supplierIDs, "selectSupplierIDs", "companyName" ,"supplierID", model);
    productService.findOneToForm(id, form);
    addHeaderAttr(HEADER_EDIT, model);
    return "Product/create";
  }

  @RequestMapping(value = "/save", method = RequestMethod.POST)
  public String save(
    @Validated @ModelAttribute ProductForm form,
    BindingResult result,
    Model model) {
    if (result.hasErrors()) {
      model.addAttribute("errorMessage", "validation error");
      return create(form, model);
    }
    Product product = productService.save(form, 1);
    model.addAttribute("product", product);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Product/detail";
  }

  @RequestMapping(value = "/delete", method = RequestMethod.POST)
  public String delete(
    @RequestParam(required = true) Long id) {
    productService.delete(id);
    return "redirect:/product/";
  }

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_INDEX =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Product");
        put("subtitle", "list");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_DETAIL =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Product");
        put("subtitle", "detail");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_CREATE =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Product");
        put("subtitle", "create");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_EDIT =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Product");
        put("subtitle", "edit");
    }});

}
ProductForm.java
package com.example.sdn4m1.web.northwind;

import java.io.Serializable;

import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Digits;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

public class ProductForm implements Serializable {

  private static final long serialVersionUID = 6731626047624461251L;

  //NodeID
  private String id;

  //EntityID
  @DecimalMin("0")
  @DecimalMax("999999")
  private String productID;

  @NotNull
  @Size(min = 1, max = 256)
  private String productName;
  @NotNull
  @Size(min = 1, max = 256)
  private String quantityPerUnit;
  @NotNull
  @Digits(integer = 4, fraction = 2)
  private String unitPrice;
  @NotNull
  @DecimalMin("0")
  @DecimalMax("999999")
  private String unitsInStock;
  @NotNull
  @DecimalMin("0")
  @DecimalMax("999999")
  private String unitsOnOrder;
  @NotNull
  @DecimalMin("0")
  @DecimalMax("999999")
  private String reorderLevel;
  @NotNull
  @Pattern(regexp = "true|false")
  private String discontinued;
  @NotNull
  private String supplierID;
  @NotNull
  private String categoryID;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getProductID() {
    return productID;
  }
  public void setProductID(String productID) {
    this.productID = productID;
  }
  public String getProductName() {
    return productName;
  }
  public void setProductName(String productName) {
    this.productName = productName;
  }
  public String getQuantityPerUnit() {
    return quantityPerUnit;
  }
  public void setQuantityPerUnit(String quantityPerUnit) {
    this.quantityPerUnit = quantityPerUnit;
  }
  public String getUnitPrice() {
    return unitPrice;
  }
  public void setUnitPrice(String unitPrice) {
    this.unitPrice = unitPrice;
  }
  public String getUnitsInStock() {
    return unitsInStock;
  }
  public void setUnitsInStock(String unitsInStock) {
    this.unitsInStock = unitsInStock;
  }
  public String getUnitsOnOrder() {
    return unitsOnOrder;
  }
  public void setUnitsOnOrder(String unitsOnOrder) {
    this.unitsOnOrder = unitsOnOrder;
  }
  public String getReorderLevel() {
    return reorderLevel;
  }
  public void setReorderLevel(String reorderLevel) {
    this.reorderLevel = reorderLevel;
  }
  public String getDiscontinued() {
    return discontinued;
  }
  public void setDiscontinued(String discontinued) {
    this.discontinued = discontinued;
  }
  public String getSupplierID() {
    return supplierID;
  }
  public void setSupplierID(String supplierID) {
    this.supplierID = supplierID;
  }
  public String getCategoryID() {
    return categoryID;
  }
  public void setCategoryID(String categoryID) {
    this.categoryID = categoryID;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }

}

Supplier

SupplierController.java
package com.example.sdn4m1.web.northwind;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.sdn4m1.domain.northwind.Supplier;
import com.example.sdn4m1.service.northwind.SupplierService;

@Controller
@RequestMapping(value = "/supplier")
public class SupplierController extends NorthWindController {
  final static Logger logger = LoggerFactory.getLogger(SupplierController.class);

  final static int PAGE_SIZE = 10;

  @Autowired
  SupplierService supplierService;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String index(Model model) {
    return index(1, model);
  }

  @RequestMapping(value = "/{pageNo}", method = RequestMethod.GET)
  public String index(
    @PathVariable Integer pageNo,
    Model model) {
    Iterable<Supplier> result = supplierService.findAll(pageNo, PAGE_SIZE, "supplierID");
    model.addAttribute("result", result);
    int totalCount = (int)supplierService.count();
    addPageAttr(supplierService.calcPage(totalCount, pageNo, PAGE_SIZE), model);
    addHeaderAttr(HEADER_INDEX, model);
    return "Supplier/index";
  }

  @RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
  public String detail(
    @PathVariable Long id,
    Model model) {
    Supplier supplier = supplierService.findOne(id);
    model.addAttribute("supplier", supplier);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Supplier/detail";
  }

  @RequestMapping(value = "/create", method = RequestMethod.GET)
  public String create(
    SupplierForm form,
    Model model) {
    addHeaderAttr(HEADER_CREATE, model);
    return "Supplier/create";
  }

  @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
  public String edit(
    @PathVariable Long id,
    SupplierForm form,
    Model model) {
    supplierService.findOneToForm(id, form);
    addHeaderAttr(HEADER_EDIT, model);
    return "Supplier/create";
  }

  @RequestMapping(value = "/save", method = RequestMethod.POST)
  public String save(
    @Validated @ModelAttribute SupplierForm form,
    BindingResult result,
    Model model) {
    if (result.hasErrors()) {
      model.addAttribute("errorMessage", "validation error");
      return create(form, model);
    }
    Supplier supplier = supplierService.save(form, 0);
    model.addAttribute("supplier", supplier);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Supplier/detail";
  }

  @RequestMapping(value = "/delete", method = RequestMethod.POST)
  public String delete(
    @RequestParam(required = true) Long id) {
    supplierService.delete(id);
    return "redirect:/supplier/";
  }

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_INDEX =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Supplier");
        put("subtitle", "list");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_DETAIL =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Supplier");
        put("subtitle", "detail");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_CREATE =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Supplier");
        put("subtitle", "create");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_EDIT =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Supplier");
        put("subtitle", "edit");
    }});

}
SupplierForm.java
package com.example.sdn4m1.web.northwind;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

public class SupplierForm implements Serializable {

  private static final long serialVersionUID = 2726633575554072971L;

  //NodeID
  private String id;

  //EntityID
  @DecimalMin("0")
  @DecimalMax("999999")
  private String supplierID;

  @NotNull
  @Size(min=1, max=256)
  private String companyName;
  @NotNull
  @Size(min=1, max=256)
  private String contactName;
  @Size(min=1, max=256)
  private String contactTitle;
  private String homePage;
  @NotNull
  private String country;
  @NotNull
  private String region;
  @NotNull
  private String city;
  @NotNull
  private String address;
  private String postalCode;
  @NotNull
  private String phone;
  private String fax;

  private Map<Integer, String> products = new HashMap<Integer, String>();

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getSupplierID() {
    return supplierID;
  }
  public void setSupplierID(String supplierID) {
    this.supplierID = supplierID;
  }
  public String getCompanyName() {
    return companyName;
  }
  public void setCompanyName(String companyName) {
    this.companyName = companyName;
  }
  public String getContactName() {
    return contactName;
  }
  public void setContactName(String contactName) {
    this.contactName = contactName;
  }
  public String getContactTitle() {
    return contactTitle;
  }
  public void setContactTitle(String contactTitle) {
    this.contactTitle = contactTitle;
  }
  public String getHomePage() {
    return homePage;
  }
  public void setHomePage(String homePage) {
    this.homePage = homePage;
  }
  public String getCountry() {
    return country;
  }
  public void setCountry(String country) {
    this.country = country;
  }
  public String getRegion() {
    return region;
  }
  public void setRegion(String region) {
    this.region = region;
  }
  public String getCity() {
    return city;
  }
  public void setCity(String city) {
    this.city = city;
  }
  public String getAddress() {
    return address;
  }
  public void setAddress(String address) {
    this.address = address;
  }
  public String getPostalCode() {
    return postalCode;
  }
  public void setPostalCode(String postalCode) {
    this.postalCode = postalCode;
  }
  public String getPhone() {
    return phone;
  }
  public void setPhone(String phone) {
    this.phone = phone;
  }
  public String getFax() {
    return fax;
  }
  public void setFax(String fax) {
    this.fax = fax;
  }

  public Map<Integer, String> getProducts() {
    return products;
  }
  public void setProducts(Map<Integer, String> products) {
    this.products = products;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }

}

Category

CategoryController.java
package com.example.sdn4m1.web.northwind;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.example.sdn4m1.domain.northwind.Category;
import com.example.sdn4m1.service.northwind.CategoryService;

@Controller
@RequestMapping(value = "/category")
public class CategoryController extends NorthWindController {
  final static Logger logger = LoggerFactory.getLogger(CategoryController.class);

  final static int PAGE_SIZE = 5;

  @Autowired
  CategoryService categoryService;

  @RequestMapping(value = "/", method = RequestMethod.GET)
  public String index(Model model) {
    return index(1, model);
  }

  @RequestMapping(value = "/{pageNo}", method = RequestMethod.GET)
  public String index(
    @PathVariable Integer pageNo,
    Model model) {
    Iterable<Category> result = categoryService.findAll(pageNo, PAGE_SIZE, "categoryID");
    model.addAttribute("result", result);
    int totalCount = (int)categoryService.count();
    addPageAttr(categoryService.calcPage(totalCount, pageNo, PAGE_SIZE), model);
    addHeaderAttr(HEADER_INDEX, model);
    return "Category/index";
  }

  @RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
  public String detail(
    @PathVariable Long id,
    Model model) {
    Category category = categoryService.findOne(id);
    model.addAttribute("category", category);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Category/detail";
  }

  @RequestMapping(value = "/create", method = RequestMethod.GET)
  public String create(
    CategoryForm form,
    Model model) {
    addHeaderAttr(HEADER_CREATE, model);
    return "Category/create";
  }

  @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET)
  public String edit(
    @PathVariable Long id,
    CategoryForm form,
    Model model) {
    categoryService.findOneToForm(id, form);
    addHeaderAttr(HEADER_EDIT, model);
    return "Category/create";
  }

  @RequestMapping(value = "/save", method = RequestMethod.POST)
  public String save(
    @Validated @ModelAttribute CategoryForm form,
    BindingResult result,
    Model model) {
    if (result.hasErrors()) {
      model.addAttribute("errorMessage", "validation error");
      return create(form, model);
    }
    Category category = categoryService.save(form, 0);
    model.addAttribute("category", category);
    addHeaderAttr(HEADER_DETAIL, model);
    return "Category/detail";
  }

  @RequestMapping(value = "/delete", method = RequestMethod.POST)
  public String delete(
    @RequestParam(required = true) Long id) {
    categoryService.delete(id);
    return "redirect:/category/";
  }

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_INDEX =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Category");
        put("subtitle", "list");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_DETAIL =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Category");
        put("subtitle", "detail");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_CREATE =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Category");
        put("subtitle", "create");
    }});

  @SuppressWarnings("serial")
  final static Map<String, String> HEADER_EDIT =
    Collections.unmodifiableMap(new LinkedHashMap<String, String>() {{
        put("title", "Category");
        put("subtitle", "edit");
    }});

}
CategoryForm.java
package com.example.sdn4m1.web.northwind;

import java.io.Serializable;

import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

public class CategoryForm implements Serializable {

  private static final long serialVersionUID = 9168336542950240873L;

  //NodeID
  private String id;

  //EntityID
  @DecimalMin("0")
  @DecimalMax("999999")
  private String categoryID;

  @NotNull
  @Size(min=1, max=256)
  private String categoryName;
  private String description;
  private String picture;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getCategoryID() {
    return categoryID;
  }
  public void setCategoryID(String categoryID) {
    this.categoryID = categoryID;
  }
  public String getCategoryName() {
    return categoryName;
  }
  public void setCategoryName(String categoryName) {
    this.categoryName = categoryName;
  }
  public String getDescription() {
    return description;
  }
  public void setDescription(String description) {
    this.description = description;
  }
  public String getPicture() {
    return picture;
  }
  public void setPicture(String picture) {
    this.picture = picture;
  }

  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.DEFAULT_STYLE);
  }

}
ViewHelper

Thymeleafテンプレートエンジンで使用するヘルパークラスを作成します。

ViewHelper.java
package com.example.sdn4m1.helper;

public class ViewHelper {

  public String substring(String text, int max) {
    if (text != null && text.length() > 0) {
      if (text.length() > max) {
        return text.substring(0, max);
      } else {
        return text;
      }
    } else {
      return "";
    }
  }

}

このクラスのsubstringメソッドは指定文字列の先頭から指定する長さの文字を切り出します。

テンプレートファイル内からは下記のように使用することができます。

helper
<td th:text="${#helper.substring(category.picture, 30)}">picture</td>

上記のヘルパークラスをテンプレートから使用できるようにするには下記のようなクラスを作成します。
カスタム Utility Object を追加する ー Thymeleafを参考にさせて頂きました。

MyDialect.java
package com.example.sdn4m1.helper;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.thymeleaf.context.IProcessingContext;
import org.thymeleaf.dialect.AbstractDialect;
import org.thymeleaf.dialect.IExpressionEnhancingDialect;

public class MyDialect extends AbstractDialect implements IExpressionEnhancingDialect {
  private static final Map<String, Object> EXPRESSION_OBJECTS;

  static {
    Map<String, Object> objects = new HashMap<>();
    objects.put("helper", new ViewHelper());
    EXPRESSION_OBJECTS = Collections.unmodifiableMap(objects);
  }

  @Override
  public String getPrefix() {
    return null;
  }

  @Override
  public Map<String, Object> getAdditionalExpressionObjects(
      IProcessingContext arg0) {
    return EXPRESSION_OBJECTS;
  }

}
テンプレート

src/main/resources/templatesフォルダを作成します。
また、各ノード用のテンプレートファイルは下記の通り作成します。

tree
/templates
│  _nw_temp.html
│
├─Category
│      create.html
│      detail.html
│      index.html
│
├─Customer
│      create.html
│      detail.html
│      index.html
│
├─Order
│      create.html
│      detail.html
│      index.html
│
├─Product
│      create.html
│      detail.html
│      index.html
│
└─Supplier
        create.html
        detail.html
        index.html

_nw_temp.html

各ページの共通部分を_nw_temp.htmlにまとめます。

_nw_temp.html
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head th:fragment="header (title)">
  <title th:text="${title}">title</title>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link href="/vendor/bootstrap-3.3.5/css/bootstrap.min.css" th:href="@{/vendor/bootstrap-3.3.5/css/bootstrap.min.css}" rel="stylesheet" />
</head>
<body>
  <div class="container">
    <div th:fragment="nav" class="row">
      <div class="col-md-12">
        <ul class="nav nav-pills">
          <li role="presentation"><a href="/customer/" th:href="@{/customer/}">customer</a></li>
          <li role="presentation"><a href="/order/" th:href="@{/order/}">order</a></li>
          <li role="presentation"><a href="/product/" th:href="@{/product/}">product</a></li>
          <li role="presentation"><a href="/supplier/" th:href="@{/supplier/}">supplier</a></li>
          <li role="presentation"><a href="/category/" th:href="@{/category/}">category</a></li>
        </ul>
      </div>
    </div>
    <div th:fragment="footer" class="page-header">
      <div>footer</div>
    </div>
  </div>
  <div th:fragment="script">
    <script src="/vendor/jquery/jquery-1.11.3.js" th:src="@{/vendor/jquery/jquery-1.11.3.js}"></script>
    <script src="/vendor/bootstrap-3.3.5/js/bootstrap.js" th:src="@{/vendor/bootstrap-3.3.5/js/bootstrap.js}"></script>
  </div>
</body>
</html>

Customer

一覧ページ

customer_index.png

index.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('CUSTOMER INDEX')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-12">
        <form action="/customer/create" th:action="@{/customer/create}" method="get">
          <button class="btn btn-primary" type="submit">
            Create
          </button>
        </form>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <nav>
          <ul class="pagination">
            <li>
              <a href="/customer/1" th:href="@{/customer/} + (${currentPage} == 1 ? 1 : ${currentPage} - 1)" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
              </a>
            </li>
            <li th:class="${i} == ${currentPage} ? 'active' : ''" th:each="i : ${#numbers.sequence(1, maxPage)}">
              <a href="/customer/1" th:href="@{/customer/} + ${i}" th:text="${i}">1</a>
            </li>
            <li>
              <a href="/customer/999" th:href="@{/customer/} + (${currentPage} == ${maxPage} ? ${maxPage} : ${currentPage} + 1)"  aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
              </a>
            </li>
          </ul>
        </nav>
        <table class="table table-striped table-bordered" th:if="${result}">
          <thead>
            <tr>
              <th>index</th>
              <th>id</th>
              <th>customerID</th>
              <th>contactName</th>
              <th>contactTitle</th>
              <th>country</th>
              <th>region</th>
              <th>city</th>
              <th>address</th>
              <th>postalCode</th>
              <th>phone</th>
              <th>fax</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="customer, status : ${result}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/customer/detail/1" th:href="@{/customer/detail} + '/' + ${customer.id}" th:text="${customer.id}">id</a>
              </td>
              <td th:text="${customer.customerID}">customerID</td>
              <td th:text="${customer.contactName}">contactName</td>
              <td th:text="${customer.contactTitle}">contactTitle</td>
              <td th:text="${customer.country}">country</td>
              <td th:text="${customer.region}">region</td>
              <td th:text="${customer.city}">city</td>
              <td th:text="${customer.address}">address</td>
              <td th:text="${customer.postalCode}">postalCode</td>
              <td th:text="${customer.phone}">phone</td>
              <td th:text="${customer.fax}">fax</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        total <span class="badge" th:text="${totalCount}">totalCount</span> currentPage <span class="badge" th:text="${currentPage}">currentPage</span> maxPage <span class="badge" th:text="${maxPage}">maxPage</span>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

詳細ページ

customer_detail.png

detail.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('CUSTOMER DETAIL')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-1">
        <form action="/customer/edit/0" th:action="@{/customer/edit/} + ${customer.id}" method="get">
          <button class="btn btn-primary" type="submit">Edit</button>
        </form>
      </div>
      <div class="col-md-1">
        <form action="/customer/delete" th:action="@{/customer/delete}" method="post">
          <input type="hidden" name="id" value="0" th:value="${customer.id}"/>
          <button class="btn btn-primary" type="submit">Delete</button>
        </form>
      </div>
      <div class="col-md-10">
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h2>Customer</h2>
        <table class="table table-striped table-bordered" th:if="${customer}" th:object="${customer}">
          <tr>
            <th class="col-md-3">NodeID</th>
            <td class="col-md-9" th:text="*{id}">id</td>
          </tr>
          <tr>
            <th>customerID</th>
            <td th:text="*{customerID}">customerID</td>
          </tr>
          <tr>
            <th>contactName</th>
            <td th:text="*{contactName}">contactName</td>
          </tr>
          <tr>
            <th>contactTitle</th>
            <td th:text="*{contactTitle}">contactTitle</td>
          </tr>
          <tr>
            <th>country</th>
            <td th:text="*{country}">country</td>
          </tr>
          <tr>
            <th>region</th>
            <td th:text="*{region}">region</td>
          </tr>
          <tr>
            <th>city</th>
            <td th:text="*{city}">city</td>
          </tr>
          <tr>
            <th>address</th>
            <td th:text="*{address}">address</td>
          </tr>
          <tr>
            <th>postalCode</th>
            <td th:text="*{postalCode}">postalCode</td>
          </tr>
          <tr>
            <th>phone</th>
            <td th:text="*{phone}">phone</td>
          </tr>
          <tr>
            <th>fax</th>
            <td th:text="*{fax}">fax</td>
          </tr>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h3>Orders</h3>
        <table class="table table-striped table-bordered" th:if="${customer.orders}">
          <caption th:text="${customer.orders.size() + ' orders'}">size</caption>
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>orderID</th>
              <th>orderDate</th>
              <th>shipName</th>
              <th>shipCountry</th>
              <th>shipRegion</th>
              <th>shipCity</th>
              <th>shipAddress</th>
              <th>shipPostalCode</th>
              <th>shippedDate</th>
              <th>requiredDate</th>
              <th>freight</th>
              <th>shipVia</th>
              <th>customerID</th>
              <th>employeeID</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="order, status : ${customer.orders}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/order/detail/1" th:href="@{/order/detail} + '/' + ${order.id}" th:text="${order.id}">id</a>
              </td>
              <td th:text="${order.orderID}">orderID</td>
              <td th:text="${order.orderDate != null}? ${#dates.format(order.orderDate,'yyyy/MM/dd')} : '-'">orderDate</td>
              <td th:text="${order.shipName}">shipName</td>
              <td th:text="${order.shipCountry}">shipCountry</td>
              <td th:text="${order.shipRegion}">shipRegion</td>
              <td th:text="${order.shipCity}">shipCity</td>
              <td th:text="${order.shipAddress}">shipAddress</td>
              <td th:text="${order.shipPostalCode}">shipPostalCode</td>
              <td th:text="${order.shippedDate != null}? ${#dates.format(order.shippedDate,'yyyy/MM/dd')} : '-'">shippedDate</td>
              <td th:text="${order.requiredDate != null}? ${#dates.format(order.requiredDate,'yyyy/MM/dd')} : '-'">requiredDate</td>
              <td th:text="${order.shipVia}">shipVia</td>
              <td th:text="${order.freight}">freight</td>
              <td th:text="${order.customerID}">customerID</td>
              <td th:text="${order.employeeID}">employeeID</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

編集ページ

customer_create.png

create.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('CUSTOMER CREATE')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-12">
        <form class="form-horizontal" role="form" action="/customer/save" th:action="@{/customer/save}" th:object="${customerForm}" method="post">
          <!-- NodeID -->
          <div class="form-group">
            <label for="id_ID" class="col-sm-2 control-label">
              (hidden)nodeID
            </label>
            <div class="col-sm-10">
              <input id="id_ID" class="form-control" type="text" name="id" th:field="*{id}" />
              <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" class="help-block">error!</span>
            </div>
          </div>
          <!-- customerID -->
          <div class="form-group">
            <label for="id_customerID" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> customerID
            </label>
            <div class="col-sm-10">
              <input id="id_customerID" class="form-control" type="text" name="customerID" th:field="*{customerID}" />
              <span th:if="${#fields.hasErrors('customerID')}" th:errors="*{customerID}" class="help-block">error!</span>
            </div>
          </div>
          <!-- contactName -->
          <div class="form-group">
            <label for="id_contactName" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> contactName
            </label>
            <div class="col-sm-10">
              <input id="id_contactName" class="form-control" type="text" name="contactName" th:field="*{contactName}" />
              <span th:if="${#fields.hasErrors('contactName')}" th:errors="*{contactName}" class="help-block">error!</span>
            </div>
          </div>
          <!-- contactTitle -->
          <div class="form-group">
            <label for="id_contactTitle" class="col-sm-2 control-label">
              contactTitle
            </label>
            <div class="col-sm-10">
              <input id="id_contactTitle" class="form-control" type="text" name="contactTitle" th:field="*{contactTitle}" />
              <span th:if="${#fields.hasErrors('contactTitle')}" th:errors="*{contactTitle}" class="help-block">error!</span>
            </div>
          </div>
          <!-- country -->
          <div class="form-group">
            <label for="id_country" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> country
            </label>
            <div class="col-sm-10">
              <input id="id_country" class="form-control" type="text" name="country" th:field="*{country}" />
              <span th:if="${#fields.hasErrors('country')}" th:errors="*{country}" class="help-block">error!</span>
            </div>
          </div>
          <!-- region -->
          <div class="form-group">
            <label for="id_region" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> region
            </label>
            <div class="col-sm-10">
              <input id="id_region" class="form-control" type="text" name="region" th:field="*{region}" />
              <span th:if="${#fields.hasErrors('region')}" th:errors="*{region}" class="help-block">error!</span>
            </div>
          </div>
          <!-- city -->
          <div class="form-group">
            <label for="id_city" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> city
            </label>
            <div class="col-sm-10">
              <input id="id_city" class="form-control" type="text" name="city" th:field="*{city}" />
              <span th:if="${#fields.hasErrors('city')}" th:errors="*{city}" class="help-block">error!</span>
            </div>
          </div>
          <!-- address -->
          <div class="form-group">
            <label for="id_address" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> address
            </label>
            <div class="col-sm-10">
              <input id="id_address" class="form-control" type="text" name="address" th:field="*{address}" />
              <span th:if="${#fields.hasErrors('address')}" th:errors="*{address}" class="help-block">error!</span>
            </div>
          </div>
          <!-- postalCode -->
          <div class="form-group">
            <label for="id_postalCode" class="col-sm-2 control-label">
              postalCode
            </label>
            <div class="col-sm-10">
              <input id="id_postalCode" class="form-control" type="text" name="postalCode" th:field="*{postalCode}" />
              <span th:if="${#fields.hasErrors('postalCode')}" th:errors="*{postalCode}" class="help-block">error!</span>
            </div>
          </div>
          <!-- phone -->
          <div class="form-group">
            <label for="id_phone" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> phone
            </label>
            <div class="col-sm-10">
              <input id="id_phone" class="form-control" type="text" name="phone" th:field="*{phone}" />
              <span th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}" class="help-block">error!</span>
            </div>
          </div>
          <!-- fax -->
          <div class="form-group">
            <label for="id_fax" class="col-sm-2 control-label">
              fax
            </label>
            <div class="col-sm-10">
              <input id="id_fax" class="form-control" type="text" name="fax" th:field="*{fax}" />
              <span th:if="${#fields.hasErrors('fax')}" th:errors="*{fax}" class="help-block">error!</span>
            </div>
          </div>
          <div class="form-group">
            <input class="btn btn-default" type="submit" value="save" />
          </div>
        </form>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

Order

一覧ページ

order_index.png

index.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('ORDER INDEX')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-6">
        <form action="/order/1" th:action="@{/order/1}" method="get">
          <div class="input-group">
            <input type="text" name="searchName" class="form-control" th:value="${searchName}" placeholder="Search for..." />
            <span class="input-group-btn">
              <input class="btn btn-default" type="submit" value="Search!" />
            </span>
          </div>
        </form>
      </div>
      <div class="col-md-1">
      </div>
      <div class="col-md-5">
        <form action="/order/create" th:action="@{/order/create}" method="get">
          <button class="btn btn-primary" type="submit">
            Create
          </button>
        </form>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <nav>
          <ul class="pagination">
            <li>
              <a href="/order/1?searchName=" th:href="@{/order/} + (${currentPage} == 1 ? 1 : ${currentPage} - 1) + '?searchName=' + (${searchName != null}? ${searchName}: '')" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
              </a>
            </li>
            <li th:class="${i} == ${currentPage} ? 'active' : ''" th:each="i : ${#numbers.sequence(1, maxPage)}">
              <a href="/order/1?searchName=" th:href="@{/order/} + ${i} + '?searchName=' + (${searchName != null}? ${searchName}: '')" th:text="${i}">1</a>
            </li>
            <li>
              <a href="/order/999?searchName=" th:href="@{/order/} + (${currentPage} == ${maxPage} ? ${maxPage} : ${currentPage} + 1) + '?searchName=' + (${searchName != null}? ${searchName}: '')"  aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
              </a>
            </li>
          </ul>
        </nav>
        <table class="table table-striped table-bordered" th:if="${result}">
          <thead>
            <tr>
              <th>index</th>
              <th>id</th>
              <th>orderID</th>
              <th>orderDate</th>
              <th>shipName</th>
              <th>shipCountry</th>
              <th>shipRegion</th>
              <th>shipCity</th>
              <th>shipAddress</th>
              <th>shipPostalCode</th>
              <th>shippedDate</th>
              <th>requiredDate</th>
              <th>shipVia</th>
              <th>freight</th>
              <th>customerID</th>
              <th>employeeID</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="order, status : ${result}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/order/detail/1" th:href="@{/order/detail} + '/' + ${order.id}" th:text="${order.id}">id</a>
              </td>
              <td th:text="${order.orderID}">orderID</td>
              <td th:text="${order.orderDate != null}? ${#dates.format(order.orderDate,'yyyy/MM/dd')} : '-'">orderDate</td>
              <td th:text="${order.shipName}">shipName</td>
              <td th:text="${order.shipCountry}">shipCountry</td>
              <td th:text="${order.shipRegion}">freight</td>
              <td th:text="${order.shipCity}">shipCity</td>
              <td th:text="${order.shipAddress}">shipAddress</td>
              <td th:text="${order.shipPostalCode}">shipPostalCode</td>
              <td th:text="${order.shippedDate != null}? ${#dates.format(order.shippedDate,'yyyy/MM/dd')} : '-'">shippedDate</td>
              <td th:text="${order.requiredDate != null}? ${#dates.format(order.requiredDate,'yyyy/MM/dd')} : '-'">requiredDate</td>
              <td th:text="${order.shipVia}">shipVia</td>
              <td th:text="${order.freight}">freight</td>
              <td th:text="${order.customerID}">customerID</td>
              <td th:text="${order.employeeID}">employeeID</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        total <span class="badge" th:text="${totalCount}">totalCount</span> currentPage <span class="badge" th:text="${currentPage}">currentPage</span> maxPage <span class="badge" th:text="${maxPage}">maxPage</span>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

詳細ページ

order_detail.png

detail.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('ORDER DETAIL')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-1">
        <form action="/order/edit/0" th:action="@{/order/edit/} + ${order.id}" method="get">
          <button class="btn btn-primary" type="submit">Edit</button>
        </form>
      </div>
      <div class="col-md-1">
        <form action="/order/delete" th:action="@{/order/delete}" method="post">
          <input type="hidden" name="id" value="0" th:value="${order.id}"/>
          <button class="btn btn-primary" type="submit">Delete</button>
        </form>
      </div>
      <div class="col-md-10">
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h2>Order</h2>
        <table class="table table-striped table-bordered" th:if="${order}" th:object="${order}">
          <tr>
            <th class="col-md-3">NodeID</th>
            <td class="col-md-9" th:text="*{id}">id</td>
          </tr>
          <tr>
            <th>orderID</th>
            <td th:text="*{orderID}">orderID</td>
          </tr>
          <tr>
            <th>orderDate</th>
            <td th:text="*{orderDate != null}? *{#dates.format(orderDate,'yyyy/MM/dd')} : '-'">orderDate</td>
          </tr>
          <tr>
            <th>shipName</th>
            <td th:text="*{shipName}">shipName</td>
          </tr>
          <tr>
            <th>shipCountry</th>
            <td th:text="*{shipCountry}">shipCountry</td>
          </tr>
          <tr>
            <th>shipRegion</th>
            <td th:text="*{shipRegion}">shipRegion</td>
          </tr>
          <tr>
            <th>shipCity</th>
            <td th:text="*{shipCity}">shipCity</td>
          </tr>
          <tr>
            <th>shipAddress</th>
            <td th:text="*{shipAddress}">shipAddress</td>
          </tr>
          <tr>
            <th>shipPostalCode</th>
            <td th:text="*{shipPostalCode}">shipPostalCode</td>
          </tr>
          <tr>
            <th>shippedDate</th>
            <td th:text="*{shippedDate != null}? *{#dates.format(shippedDate,'yyyy/MM/dd')} : '-'">shippedDate</td>
          </tr>
          <tr>
            <th>requiredDate</th>
            <td th:text="*{requiredDate != null}? *{#dates.format(requiredDate,'yyyy/MM/dd')} : '-'">requiredDate</td>
          </tr>
          <tr>
            <th>shipVia</th>
            <td th:text="*{shipVia}">shipVia</td>
          </tr>
          <tr>
            <th>freight</th>
            <td th:text="*{freight}">freight</td>
          </tr>
          <tr>
            <th>customerID</th>
            <td th:text="*{customerID}">customerID</td>
          </tr>
          <tr>
            <th>employeeID</th>
            <td th:text="*{employeeID}">employeeID</td>
          </tr>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h3>Customer</h3>
        <table class="table table-striped table-bordered" th:if="${order.customer}" th:object="${order.customer}">
          <tr>
            <th class="col-md-3">NodeID</th>
            <td class="col-md-9" th:text="*{id}">id</td>
          </tr>
          <tr>
            <th>customerID</th>
            <td th:text="*{customerID}">customerID</td>
          </tr>
          <tr>
            <th>contactName</th>
            <td th:text="*{contactName}">contactName</td>
          </tr>
          <tr>
            <th>contactTitle</th>
            <td th:text="*{contactTitle}">contactTitle</td>
          </tr>
          <tr>
            <th>country</th>
            <td th:text="*{country}">country</td>
          </tr>
          <tr>
            <th>region</th>
            <td th:text="*{region}">region</td>
          </tr>
          <tr>
            <th>city</th>
            <td th:text="*{city}">city</td>
          </tr>
          <tr>
            <th>address</th>
            <td th:text="*{address}">address</td>
          </tr>
          <tr>
            <th>postalCode</th>
            <td th:text="*{postalCode}">postalCode</td>
          </tr>
          <tr>
            <th>phone</th>
            <td th:text="*{phone}">phone</td>
          </tr>
          <tr>
            <th>fax</th>
            <td th:text="*{fax}">fax</td>
          </tr>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h3>Products</h3>
        <table class="table table-striped table-bordered" th:if="${order.products}">
          <caption th:text="${order.products.size() + ' products'}">size</caption>
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>productID</th>
              <th>productName</th>
              <th>quantityPerUnit</th>
              <th>unitPrice</th>
              <th>unitsInStock</th>
              <th>unitsOnOrder</th>
              <th>reorderLevel</th>
              <th>discontinued</th>
              <th>supplierID</th>
              <th>categoryID</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="product, status : ${order.products}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/product/detail/1" th:href="@{/product/detail} + '/' + ${product.id}" th:text="${product.id}">id</a>
              </td>
              <td th:text="${product.productID}">productID</td>
              <td th:text="${product.productName}">productName</td>
              <td th:text="${product.quantityPerUnit}">quantityPerUnit</td>
              <td th:text="${product.unitPrice}">unitPrice</td>
              <td th:text="${product.unitsInStock}">unitsInStock</td>
              <td th:text="${product.unitsOnOrder}">unitsOnOrder</td>
              <td th:text="${product.reorderLevel}">reorderLevel</td>
              <td th:text="${product.discontinued}">discontinued</td>
              <td th:text="${product.supplierID}">supplierID</td>
              <td th:text="${product.categoryID}">categoryID</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h2>Recommend</h2>
        <table class="table table-striped table-bordered" th:if="${recommend}">
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>productID</th>
              <th>productName</th>
              <th>quantityPerUnit</th>
              <th>unitPrice</th>
              <th>unitsInStock</th>
              <th>unitsOnOrder</th>
              <th>reorderLevel</th>
              <th>discontinued</th>
              <th>supplierID</th>
              <th>categoryID</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="product, status : ${recommend}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/product/detail/1" th:href="@{/product/detail} + '/' + ${product.id}" th:text="${product.id}">id</a>
              </td>
              <td th:text="${product.productID}">productID</td>
              <td th:text="${product.productName}">productName</td>
              <td th:text="${product.quantityPerUnit}">quantityPerUnit</td>
              <td th:text="${product.unitPrice}">unitPrice</td>
              <td th:text="${product.unitsInStock}">unitsInStock</td>
              <td th:text="${product.unitsOnOrder}">unitsOnOrder</td>
              <td th:text="${product.reorderLevel}">reorderLevel</td>
              <td th:text="${product.discontinued}">discontinued</td>
              <td th:text="${product.supplierID}">supplierID</td>
              <td th:text="${product.categoryID}">categoryID</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

編集ページ

order_create.png

create.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('ORDER CREATE')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-12">
        <form class="form-horizontal" role="form" action="/order/save" th:action="@{/order/save}" th:object="${orderForm}" method="post">
          <!-- NodeID -->
          <div class="form-group">
            <label for="id_ID" class="col-sm-2 control-label">
              (hidden)NodeID
            </label>
            <div class="col-sm-10">
              <input id="id_ID" class="form-control" type="text" name="id" th:field="*{id}" />
              <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" class="help-block">error!</span>
            </div>
          </div>
          <!-- orderID -->
          <div class="form-group">
            <label for="id_orderID" class="col-sm-2 control-label">
              (hidden)orderID
            </label>
            <div class="col-sm-10">
              <input id="id_orderID" class="form-control" type="text" name="orderID" th:field="*{orderID}" />
              <span th:if="${#fields.hasErrors('orderID')}" th:errors="*{orderID}" class="help-block">error!</span>
            </div>
          </div>
          <!-- orderDate -->
          <div class="form-group">
            <label for="id_orderDate" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> orderDate
            </label>
            <div class="col-sm-10">
              <input id="id_orderDate" class="form-control" type="text" name="orderDate" th:field="*{orderDate}" />
              <span th:if="${#fields.hasErrors('orderDate')}" th:errors="*{orderDate}" class="help-block">error!</span>
            </div>
          </div>
          <!-- shipName -->
          <div class="form-group">
            <label for="id_shipName" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> shipName
            </label>
            <div class="col-sm-10">
              <input id="id_shipName" class="form-control" type="text" name="shipName" th:field="*{shipName}" />
              <span th:if="${#fields.hasErrors('shipName')}" th:errors="*{shipName}" class="help-block">error!</span>
            </div>
          </div>
          <!-- shipCountry -->
          <div class="form-group">
            <label for="id_shipCountry" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> shipCountry
            </label>
            <div class="col-sm-10">
              <input id="id_shipCountry" class="form-control" type="text" name="shipCountry" th:field="*{shipCountry}" />
              <span th:if="${#fields.hasErrors('shipCountry')}" th:errors="*{shipCountry}" class="help-block">error!</span>
            </div>
          </div>
          <!-- shipRegion -->
          <div class="form-group">
            <label for="id_shipRegion" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> shipRegion
            </label>
            <div class="col-sm-10">
              <input id="id_shipRegion" class="form-control" type="text" name="shipRegion" th:field="*{shipRegion}" />
              <span th:if="${#fields.hasErrors('shipRegion')}" th:errors="*{shipRegion}" class="help-block">error!</span>
            </div>
          </div>
          <!-- shipCity -->
          <div class="form-group">
            <label for="id_shipCity" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> shipCity
            </label>
            <div class="col-sm-10">
              <input id="id_shipCity" class="form-control" type="text" name="shipCity" th:field="*{shipCity}" />
              <span th:if="${#fields.hasErrors('shipCity')}" th:errors="*{picture}" class="help-block">error!</span>
            </div>
          </div>
          <!-- shipAddress -->
          <div class="form-group">
            <label for="id_shipAddress" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> shipAddress
            </label>
            <div class="col-sm-10">
              <input id="id_shipAddress" class="form-control" type="text" name="shipAddress" th:field="*{shipAddress}" />
              <span th:if="${#fields.hasErrors('shipAddress')}" th:errors="*{shipAddress}" class="help-block">error!</span>
            </div>
          </div>
          <!-- shipPostalCode -->
          <div class="form-group">
            <label for="id_shipPostalCode" class="col-sm-2 control-label">
              shipPostalCode
            </label>
            <div class="col-sm-10">
              <input id="id_shipPostalCode" class="form-control" type="text" name="shipPostalCode" th:field="*{shipPostalCode}" />
              <span th:if="${#fields.hasErrors('shipPostalCode')}" th:errors="*{shipPostalCode}" class="help-block">error!</span>
            </div>
          </div>
          <!-- shippedDate -->
          <div class="form-group">
            <label for="id_shippedDate" class="col-sm-2 control-label">
              shippedDate
            </label>
            <div class="col-sm-10">
              <input id="id_shippedDate" class="form-control" type="text" name="shippedDate" th:field="*{shippedDate}" />
              <span th:if="${#fields.hasErrors('shippedDate')}" th:errors="*{shippedDate}" class="help-block">error!</span>
            </div>
          </div>
          <!-- requiredDate -->
          <div class="form-group">
            <label for="id_requiredDate" class="col-sm-2 control-label">
              requiredDate
            </label>
            <div class="col-sm-10">
              <input id="id_requiredDate" class="form-control" type="text" name="requiredDate" th:field="*{requiredDate}" />
              <span th:if="${#fields.hasErrors('requiredDate')}" th:errors="*{requiredDate}" class="help-block">error!</span>
            </div>
          </div>
          <!-- shipVia -->
          <div class="form-group">
            <label for="id_shipVia" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> shipVia
            </label>
            <div class="col-sm-10">
              <input id="id_shipVia" class="form-control" type="text" name="shipVia" th:field="*{shipVia}" />
              <span th:if="${#fields.hasErrors('shipVia')}" th:errors="*{shipVia}" class="help-block">error!</span>
            </div>
          </div>
          <!-- freight -->
          <div class="form-group">
            <label for="id_freight" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> freight
            </label>
            <div class="col-sm-10">
              <input id="id_freight" class="form-control" type="text" name="freight" th:field="*{freight}" />
              <span th:if="${#fields.hasErrors('freight')}" th:errors="*{freight}" class="help-block">error!</span>
            </div>
          </div>
          <!-- customerID -->
          <div class="form-group">
            <label for="id_customerID" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> customerID
            </label>
            <div class="col-sm-10">
              <input id="id_customerID" class="form-control" type="text" name="customerID" th:field="*{customerID}" />
              <span th:if="${#fields.hasErrors('customerID')}" th:errors="*{customerID}" class="help-block">error!</span>
            </div>
          </div>
          <!-- employeeID -->
          <div class="form-group">
            <label for="id_employeeID" class="col-sm-2 control-label">
              employeeID
            </label>
            <div class="col-sm-10">
              <input id="id_employeeID" class="form-control" type="text" name="employeeID" th:field="*{employeeID}" />
              <span th:if="${#fields.hasErrors('employeeID')}" th:errors="*{employeeID}" class="help-block">error!</span>
            </div>
          </div>
          <div class="form-group">
            <input class="btn btn-default" type="submit" value="save" />
          </div>
        </form>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

Product

一覧ページ

product_index.png

index.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('PRODUCT INDEX')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-6">
        <form action="/product/1" th:action="@{/product/1}" method="get">
          <div class="input-group">
            <input type="text" name="searchName" class="form-control" th:value="${searchName}" placeholder="Search for..." />
            <span class="input-group-btn">
              <input class="btn btn-default" type="submit" value="Search!" />
            </span>
          </div>
        </form>
      </div>
      <div class="col-md-1">
      </div>
      <div class="col-md-5">
        <form action="/product/create" th:action="@{/product/create}" method="get">
          <input type="hidden" name="productID" value="" />
          <button class="btn btn-primary" type="submit">
            Create
          </button>
        </form>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <nav>
          <ul class="pagination">
            <li>
              <a href="/product/1?searchName=" th:href="@{/product/} + (${currentPage} == 1 ? 1 : ${currentPage} - 1) + '?searchName='+ (${searchName != null}? ${searchName}: '')" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
              </a>
            </li>
            <li th:class="${i} == ${currentPage} ? 'active' : ''" th:each="i : ${#numbers.sequence(1, maxPage)}">
              <a href="/product/1?searchName=" th:href="@{/product/} + ${i} + '?searchName=' + (${searchName != null}? ${searchName}: '')" th:text="${i}">1</a>
            </li>
            <li>
              <a href="/product/999?searchName=" th:href="@{/product/} + (${currentPage} == ${maxPage} ? ${maxPage} : ${currentPage} + 1) + '?searchName=' + (${searchName != null}? ${searchName}: '')" aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
              </a>
            </li>
          </ul>
        </nav>
        <table class="table table-striped table-bordered" th:if="${result}">
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>productID</th>
              <th>productName</th>
              <th>quantityPerUnit</th>
              <th>unitPrice</th>
              <th>unitsInStock</th>
              <th>unitsOnOrder</th>
              <th>reorderLevel</th>
              <th>discontinued</th>
              <th>supplierID</th>
              <th>categoryID</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="product, status : ${result}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/product/detail/1" th:href="@{/product/detail} + '/' + ${product.id}" th:text="${product.id}">id</a>
              </td>
              <td th:text="${product.productID}">productID</td>
              <td th:text="${product.productName}">productName</td>
              <td th:text="${product.quantityPerUnit}">quantityPerUnit</td>
              <td th:text="${product.unitPrice}">unitPrice</td>
              <td th:text="${product.unitsInStock}">unitsInStock</td>
              <td th:text="${product.unitsOnOrder}">unitsOnOrder</td>
              <td th:text="${product.reorderLevel}">reorderLevel</td>
              <td th:text="${product.discontinued}">discontinued</td>
              <td th:text="${product.supplierID}">supplierID</td>
              <td th:text="${product.categoryID}">categoryID</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        total <span class="badge" th:text="${totalCount}">totalCount</span> currentPage <span class="badge" th:text="${currentPage}">currentPage</span> maxPage <span class="badge" th:text="${maxPage}">maxPage</span>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

詳細ページ

product_detail.png

detail.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('PRODUCT DETAIL')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-1">
        <form action="/product/edit/0" th:action="@{/product/edit/} + ${product.id}" method="get">
          <button class="btn btn-primary" type="submit">Edit</button>
        </form>
      </div>
      <div class="col-md-1">
        <form action="/product/delete" th:action="@{/product/delete}" method="post">
          <input type="hidden" name="id" value="0" th:value="${product.id}"/>
          <button class="btn btn-primary" type="submit">Delete</button>
        </form>
      </div>
      <div class="col-md-10">
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h2>Product</h2>
        <table class="table table-striped table-bordered" th:if="${product}" th:object="${product}">
          <tr>
            <th class="col-md-3">NodeID</th>
            <td class="col-md-9" th:text="*{id}">id</td>
          </tr>
          <tr>
            <th>productID</th>
            <td th:text="*{productID}">1</td>
          </tr>
          <tr>
            <th>productName</th>
            <td th:text="*{productName}">1</td>
          </tr>
          <tr>
            <th>supplierID</th>
            <td th:text="*{supplierID}">1</td>
          </tr>
          <tr>
            <th>categoryID</th>
            <td th:text="*{categoryID}">1</td>
          </tr>
          <tr>
            <th>quantityPerUnit</th>
            <td th:text="*{quantityPerUnit}">1</td>
          </tr>
          <tr>
            <th>unitPrice</th>
            <td th:text="*{unitPrice}">1</td>
          </tr>
          <tr>
            <th>unitsInStock</th>
            <td th:text="*{unitsInStock}">1</td>
          </tr>
          <tr>
            <th>unitsOnOrder</th>
            <td th:text="*{unitsOnOrder}">1</td>
          </tr>
          <tr>
            <th>reorderLevel</th>
            <td th:text="*{reorderLevel}">1</td>
          </tr>
          <tr>
            <th>discontinued</th>
            <td th:text="*{discontinued}">1</td>
          </tr>
        </table>
      </div>
    </div>

     <div class="row">
      <div class="col-md-12">
        <h3>Supplier</h3>
        <table class="table table-striped table-bordered" th:if="${product.supplier}" th:object="${product.supplier}">
          <tr>
            <th class="col-md-3">NodeID</th>
            <td class="col-md-9">
              <a href="/supplier/detail/0" th:href="@{/supplier/detail/} + *{id}" th:text="*{id}">id</a>
            </td>
          </tr>
          <tr>
            <th>supplierID</th>
            <td th:text="*{supplierID}">supplierID</td>
          </tr>
          <tr>
            <th>companyName</th>
            <td th:text="*{companyName}">companyName</td>
          </tr>
          <tr>
            <th>contactName</th>
            <td th:text="*{contactName}">contactName</td>
          </tr>
          <tr>
            <th>contactTitle</th>
            <td th:text="*{contactTitle}">contactTitle</td>
          </tr>
          <tr>
            <th>homePage</th>
            <td th:text="*{homePage}">homePage</td>
          </tr>
          <tr>
            <th>country</th>
            <td th:text="*{country}">country</td>
          </tr>
          <tr>
            <th>region</th>
            <td th:text="*{region}">region</td>
          </tr>
          <tr>
            <th>city</th>
            <td th:text="*{city}">city</td>
          </tr>
          <tr>
            <th>address</th>
            <td th:text="*{address}">address</td>
          </tr>
          <tr>
            <th>postalCode</th>
            <td th:text="*{postalCode}">postalCode</td>
          </tr>
          <tr>
            <th>phone</th>
            <td th:text="*{phone}">phone</td>
          </tr>
          <tr>
            <th>fax</th>
            <td th:text="*{fax}">fax</td>
          </tr>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h3>Category</h3>
        <table class="table table-striped table-bordered" th:if="${product.category}" th:object="${product.category}">
          <tr>
            <th class="col-md-3">NodeID</th>
            <td class="col-md-9" th:text="*{id}">id</td>
          </tr>
          <tr>
            <th>categoryID</th>
            <td th:text="*{categoryID}">categoryID</td>
          </tr>
          <tr>
            <th>categoryName</th>
            <td th:text="*{categoryName}">categoryName</td>
          </tr>
          <tr>
            <th>description</th>
            <td th:text="*{description}">description</td>
          </tr>
          <tr>
            <th>picture</th>
            <td th:text="*{#helper.substring(picture, 30)}">picture</td>
          </tr>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h3>Orders</h3>
        <table class="table table-striped table-bordered" th:if="${product.orders}">
          <caption th:text="${product.orders.size()} + ' orders'">size</caption>
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>orderID</th>
              <th>orderDate</th>
              <th>shipName</th>
              <th>shipCountry</th>
              <th>shipRegion</th>
              <th>shipCity</th>
              <th>shipAddress</th>
              <th>shipPostalCode</th>
              <th>shippedDate</th>
              <th>requiredDate</th>
              <th>shipVia</th>
              <th>freight</th>
              <th>customerID</th>
              <th>employeeID</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="order, status : ${product.orders}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/order/detail/1" th:href="@{/order/detail} + '/' + ${order.id}" th:text="${order.id}">id</a>
              </td>
              <td th:text="${order.orderID}">orderID</td>
              <td th:text="${order.orderDate != null}? ${#dates.format(order.orderDate,'yyyy/MM/dd')} : '-'">orderDate</td>
              <td th:text="${order.shipName}">shipName</td>
              <td th:text="${order.shipCountry}">shipCountry</td>
              <td th:text="${order.shipRegion}">freight</td>
              <td th:text="${order.shipCity}">shipCity</td>
              <td th:text="${order.shipAddress}">shipAddress</td>
              <td th:text="${order.shipPostalCode}">shipPostalCode</td>
              <td th:text="${order.shippedDate != null}? ${#dates.format(order.shippedDate,'yyyy/MM/dd')} : '-'">shippedDate</td>
              <td th:text="${order.requiredDate != null}? ${#dates.format(order.requiredDate,'yyyy/MM/dd')} : '-'">requiredDate</td>
              <td th:text="${order.shipVia}">shipVia</td>
              <td th:text="${order.freight}">freight</td>
              <td th:text="${order.customerID}">customerID</td>
              <td th:text="${order.employeeID}">employeeID</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

編集ページ

product_create.png

create.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('PRODUCT CREATE')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-12">
        <form class="form-horizontal" role="form" action="/product/save" th:action="@{/product/save}" th:object="${productForm}" method="post">
          <!-- NodeID -->
          <div class="form-group">
            <label for="id_ID" class="col-sm-2 control-label">
              (hidden)NodeID
            </label>
            <div class="col-sm-10">
              <input id="id_ID" class="form-control" type="text" name="id" th:field="*{id}" />
              <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" class="help-block">error!</span>
            </div>
          </div>
          <!-- productID -->
          <div class="form-group">
            <label for="id_productID" class="col-sm-2 control-label">
              (hidden)productID
            </label>
            <div class="col-sm-10">
              <input id="id_productID" class="form-control" type="text" name="productID" th:field="*{productID}" />
              <span th:if="${#fields.hasErrors('productID')}" th:errors="*{productID}" class="help-block">error!</span>
            </div>
          </div>
          <!-- productName -->
          <div class="form-group">
            <label for="id_productName" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> productName
            </label>
            <div class="col-sm-10">
              <input id="id_productName" class="form-control" type="text" name="productName" th:field="*{productName}" />
              <span th:if="${#fields.hasErrors('productName')}" th:errors="*{productName}" class="help-block">error!</span>
            </div>
          </div>
          <!-- quantityPerUnit -->
          <div class="form-group">
            <label for="id_quantityPerUnit" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> quantityPerUnit
            </label>
            <div class="col-sm-10">
              <input id="id_quantityPerUnit" class="form-control" type="text" name="quantityPerUnit" th:field="*{quantityPerUnit}" />
              <span th:if="${#fields.hasErrors('quantityPerUnit')}" th:errors="*{quantityPerUnit}" class="help-block">error!</span>
            </div>
          </div>
          <!-- unitPrice -->
          <div class="form-group">
            <label for="id_unitPrice" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> unitPrice
            </label>
            <div class="col-sm-10">
              <input id="id_unitPrice" class="form-control" type="text" name="unitPrice" th:field="*{unitPrice}" />
              <span th:if="${#fields.hasErrors('unitPrice')}" th:errors="*{unitPrice}" class="help-block">error!</span>
            </div>
          </div>
          <!-- unitsInStock -->
          <div class="form-group">
            <label for="id_unitsInStock" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> unitsInStock
            </label>
            <div class="col-sm-10">
              <input id="id_unitsInStock" class="form-control" type="text" name="unitsInStock" th:field="*{unitsInStock}" />
              <span th:if="${#fields.hasErrors('unitsInStock')}" th:errors="*{unitsInStock}" class="help-block">error!</span>
            </div>
          </div>
          <!-- unitsOnOrder -->
          <div class="form-group">
            <label for="id_unitsOnOrder" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> unitsOnOrder
            </label>
            <div class="col-sm-10">
              <input id="id_unitsOnOrder" class="form-control" type="text" name="unitsOnOrder" th:field="*{unitsOnOrder}" />
              <span th:if="${#fields.hasErrors('unitsOnOrder')}" th:errors="*{unitsOnOrder}" class="help-block">error!</span>
            </div>
          </div>
          <!-- reorderLevel -->
          <div class="form-group">
            <label for="id_reorderLevel" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> reorderLevel
            </label>
            <div class="col-sm-10">
              <input id="id_reorderLevel" class="form-control" type="text" name="reorderLevel" th:field="*{reorderLevel}" />
              <span th:if="${#fields.hasErrors('reorderLevel')}" th:errors="*{reorderLevel}" class="help-block">error!</span>
            </div>
          </div>
          <!-- discontinued -->
          <div class="form-group">
            <label for="id_discontinued" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> discontinued
            </label>
            <div class="col-sm-10 radio">
              <label>
                <input type="radio" name="discontinued" th:value="true" th:field="*{discontinued}" />true
              </label>
              <label>
                <input type="radio" name="discontinued" th:value="false" th:field="*{discontinued}" />false
              </label>
              <span th:if="${#fields.hasErrors('discontinued')}" th:errors="*{discontinued}" class="help-block">error!</span>
            </div>
          </div>
          <!-- supplierID -->
          <div class="form-group">
            <label for="id_supplierID" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> supplierID
            </label>
            <div class="col-sm-10">
              <select class="form-control" id="id_supplierID" name="supplierID">
                <option th:each="item : ${selectSupplierIDs}" th:value="${item.key}" th:text="${item.value}" th:selected="${item.key} == *{supplierID}">pulldown</option>
              </select>
              <span th:if="${#fields.hasErrors('supplierID')}" th:errors="*{supplierID}" class="help-block">error!</span>
            </div>
          </div>
          <!-- categoryID -->
          <div class="form-group">
            <label for="id_categoryID" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> categoryID
            </label>
            <div class="col-sm-10">
              <select class="form-control" id="id_categoryID" name="categoryID">
                <option th:each="item : ${selectCategoryIDs}" th:value="${item.key}" th:text="${item.value}" th:selected="${item.key} == *{categoryID}">pulldown</option>
              </select>
              <span th:if="${#fields.hasErrors('categoryID')}" th:errors="*{categoryID}" class="help-block">error!</span>
            </div>
          </div>
          <div class="form-group">
            <input class="btn btn-default" type="submit" value="save" />
          </div>
        </form>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

Supplier

一覧ページ

supplier_index.png

index.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('SUPPLIER INDEX')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-12">
        <form action="/supplier/create" th:action="@{/supplier/create}" method="get">
          <button class="btn btn-primary" type="submit">
            Create
          </button>
        </form>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <nav>
          <ul class="pagination">
            <li>
              <a href="/supplier/1" th:href="@{/supplier/} + (${currentPage} == 1 ? 1 : ${currentPage} - 1)" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
              </a>
            </li>
            <li th:class="${i} == ${currentPage} ? 'active' : ''" th:each="i : ${#numbers.sequence(1, maxPage)}">
              <a href="/supplier/1" th:href="@{/supplier/} + ${i}" th:text="${i}">1</a>
            </li>
            <li>
              <a href="/supplier/999" th:href="@{/supplier/} + (${currentPage} == ${maxPage} ? ${maxPage} : ${currentPage} + 1)"  aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
              </a>
            </li>
          </ul>
        </nav>
        <table class="table table-striped table-bordered" th:if="${result}">
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>supplierID</th>
              <th>companyName</th>
              <th>contactName</th>
              <th>contactTitle</th>
              <th>homePage</th>
              <th>country</th>
              <th>region</th>
              <th>city</th>
              <th>address</th>
              <th>postalCode</th>
              <th>phone</th>
              <th>fax</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="supplier, status : ${result}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/supplier/detail/1" th:href="@{/supplier/detail} + '/' + ${supplier.id}" th:text="${supplier.id}">id</a>
              </td>
              <td th:text="${supplier.supplierID}">supplierID</td>
              <td th:text="${supplier.companyName}">companyName</td>
              <td th:text="${supplier.contactName}">contactName</td>
              <td th:text="${supplier.contactTitle}">contactTitle</td>
              <td th:text="${#helper.substring(supplier.homePage,20)}">homePage</td>
              <td th:text="${supplier.country}">country</td>
              <td th:text="${supplier.region}">region</td>
              <td th:text="${supplier.city}">city</td>
              <td th:text="${supplier.address}">address</td>
              <td th:text="${supplier.postalCode}">postalCode</td>
              <td th:text="${supplier.phone}">phone</td>
              <td th:text="${supplier.fax}">fax</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        total <span class="badge" th:text="${totalCount}">totalCount</span> currentPage <span class="badge" th:text="${currentPage}">currentPage</span> maxPage <span class="badge" th:text="${maxPage}">maxPage</span>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

詳細ページ

supplier_detail.png

detail.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('SUPPLIER DETAIL')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-1">
        <form action="/supplier/edit/0" th:action="@{/supplier/edit/} + ${supplier.id}" method="get">
          <button class="btn btn-primary" type="submit">Edit</button>
        </form>
      </div>
      <div class="col-md-1">
        <form action="/supplier/delete" th:action="@{/supplier/delete}" method="post">
          <input type="hidden" name="id" value="0" th:value="${supplier.id}"/>
          <button class="btn btn-primary" type="submit">Delete</button>
        </form>
      </div>
      <div class="col-md-10">
      </div>
    </div>

     <div class="row">
      <div class="col-md-12">
        <h2>Supplier</h2>
        <table class="table table-striped table-bordered" th:if="${supplier}" th:object="${supplier}">
          <tr>
            <th class="col-md-3">NodeID</th>
            <td class="col-md-9" th:text="*{id}">id</td>
          </tr>
          <tr>
            <th>supplierID</th>
            <td th:text="*{supplierID}">supplierID</td>
          </tr>
          <tr>
            <th>companyName</th>
            <td th:text="*{companyName}">companyName</td>
          </tr>
          <tr>
            <th>contactName</th>
            <td th:text="*{contactName}">contactName</td>
          </tr>
          <tr>
            <th>contactTitle</th>
            <td th:text="*{contactTitle}">contactTitle</td>
          </tr>
          <tr>
            <th>homePage</th>
            <td th:text="*{homePage}">homePage</td>
          </tr>
          <tr>
            <th>country</th>
            <td th:text="*{country}">country</td>
          </tr>
          <tr>
            <th>region</th>
            <td th:text="*{region}">region</td>
          </tr>
          <tr>
            <th>city</th>
            <td th:text="*{city}">city</td>
          </tr>
          <tr>
            <th>address</th>
            <td th:text="*{address}">address</td>
          </tr>
          <tr>
            <th>postalCode</th>
            <td th:text="*{postalCode}">postalCode</td>
          </tr>
          <tr>
            <th>phone</th>
            <td th:text="*{phone}">phone</td>
          </tr>
          <tr>
            <th>fax</th>
            <td th:text="*{fax}">fax</td>
          </tr>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h3>Products</h3>
        <table class="table table-striped table-bordered" th:if="${supplier.products}">
          <caption th:text="${supplier.products.size()} + ' products'">size</caption>
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>productID</th>
              <th>productName</th>
              <th>quantityPerUnit</th>
              <th>unitPrice</th>
              <th>unitsInStock</th>
              <th>unitsOnOrder</th>
              <th>reorderLevel</th>
              <th>discontinued</th>
              <th>supplierID</th>
              <th>categoryID</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="product, status : ${supplier.products}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/product/detail/1" th:href="@{/product/detail} + '/' + ${product.id}" th:text="${product.id}">id</a>
              </td>
              <td th:text="${product.productID}">productID</td>
              <td th:text="${product.productName}">productName</td>
              <td th:text="${product.quantityPerUnit}">quantityPerUnit</td>
              <td th:text="${product.unitPrice}">unitPrice</td>
              <td th:text="${product.unitsInStock}">unitsInStock</td>
              <td th:text="${product.unitsOnOrder}">unitsOnOrder</td>
              <td th:text="${product.reorderLevel}">reorderLevel</td>
              <td th:text="${product.discontinued}">discontinued</td>
              <td th:text="${product.supplierID}">supplierID</td>
              <td th:text="${product.categoryID}">categoryID</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

編集ページ

supplier_create.png

create.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('SUPPLIER CREATE')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-12">
        <form class="form-horizontal" role="form" action="/supplier/save" th:action="@{/supplier/save}" th:object="${supplierForm}" method="post">
          <!-- NodeID -->
          <div class="form-group">
            <label for="id_ID" class="col-sm-2 control-label">
              (hidden)nodeID
            </label>
            <div class="col-sm-10">
              <input id="id_ID" class="form-control" type="text" name="id" th:field="*{id}" />
              <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" class="help-block">error!</span>
            </div>
          </div>
          <!-- supplierID -->
          <div class="form-group">
            <label for="id_supplierID" class="col-sm-2 control-label">
              (hidden)supplierID
            </label>
            <div class="col-sm-10">
              <input id="id_supplierID" class="form-control" type="text" name="supplierID" th:field="*{supplierID}" />
              <span th:if="${#fields.hasErrors('supplierID')}" th:errors="*{supplierID}" class="help-block">error!</span>
            </div>
          </div>
          <!-- companyName -->
          <div class="form-group">
            <label for="id_companyName" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> companyName
            </label>
            <div class="col-sm-10">
              <input id="id_companyName" class="form-control" type="text" name="companyName" th:field="*{companyName}" />
              <span th:if="${#fields.hasErrors('companyName')}" th:errors="*{companyName}" class="help-block">error!</span>
            </div>
          </div>
          <!-- contactName -->
          <div class="form-group">
            <label for="id_contactName" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> contactName
            </label>
            <div class="col-sm-10">
              <input id="id_contactName" class="form-control" type="text" name="contactName" th:field="*{contactName}" />
              <span th:if="${#fields.hasErrors('contactName')}" th:errors="*{contactName}" class="help-block">error!</span>
            </div>
          </div>
          <!-- contactTitle -->
          <div class="form-group">
            <label for="id_contactTitle" class="col-sm-2 control-label">
              contactTitle
            </label>
            <div class="col-sm-10">
              <input id="id_contactTitle" class="form-control" type="text" name="contactTitle" th:field="*{contactTitle}" />
              <span th:if="${#fields.hasErrors('contactTitle')}" th:errors="*{contactTitle}" class="help-block">error!</span>
            </div>
          </div>
          <!-- homePage -->
          <div class="form-group">
            <label for="id_homePage" class="col-sm-2 control-label">
              homePage
            </label>
            <div class="col-sm-10">
              <input id="id_homePage" class="form-control" type="text" name="homePage" th:field="*{homePage}" />
              <span th:if="${#fields.hasErrors('homePage')}" th:errors="*{homePage}" class="help-block">error!</span>
            </div>
          </div>
          <!-- country -->
          <div class="form-group">
            <label for="id_country" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> country
            </label>
            <div class="col-sm-10">
              <input id="id_country" class="form-control" type="text" name="country" th:field="*{country}" />
              <span th:if="${#fields.hasErrors('country')}" th:errors="*{country}" class="help-block">error!</span>
            </div>
          </div>
          <!-- region -->
          <div class="form-group">
            <label for="id_region" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> region
            </label>
            <div class="col-sm-10">
              <input id="id_region" class="form-control" type="text" name="region" th:field="*{region}" />
              <span th:if="${#fields.hasErrors('region')}" th:errors="*{region}" class="help-block">error!</span>
            </div>
          </div>
          <!-- city -->
          <div class="form-group">
            <label for="id_city" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> city
            </label>
            <div class="col-sm-10">
              <input id="id_city" class="form-control" type="text" name="city" th:field="*{city}" />
              <span th:if="${#fields.hasErrors('city')}" th:errors="*{city}" class="help-block">error!</span>
            </div>
          </div>
          <!-- address -->
          <div class="form-group">
            <label for="id_address" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> address
            </label>
            <div class="col-sm-10">
              <input id="id_address" class="form-control" type="text" name="address" th:field="*{address}" />
              <span th:if="${#fields.hasErrors('address')}" th:errors="*{address}" class="help-block">error!</span>
            </div>
          </div>
          <!-- postalCode -->
          <div class="form-group">
            <label for="id_postalCode" class="col-sm-2 control-label">
              postalCode
            </label>
            <div class="col-sm-10">
              <input id="id_postalCode" class="form-control" type="text" name="postalCode" th:field="*{postalCode}" />
              <span th:if="${#fields.hasErrors('postalCode')}" th:errors="*{postalCode}" class="help-block">error!</span>
            </div>
          </div>
          <!-- phone -->
          <div class="form-group">
            <label for="id_phone" class="col-sm-2 control-label">
              <abbr title="required">*</abbr> phone
            </label>
            <div class="col-sm-10">
              <input id="id_phone" class="form-control" type="text" name="phone" th:field="*{phone}" />
              <span th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}" class="help-block">error!</span>
            </div>
          </div>
          <!-- fax -->
          <div class="form-group">
            <label for="id_fax" class="col-sm-2 control-label">
              fax
            </label>
            <div class="col-sm-10">
              <input id="id_fax" class="form-control" type="text" name="fax" th:field="*{fax}" />
              <span th:if="${#fields.hasErrors('fax')}" th:errors="*{fax}" class="help-block">error!</span>
            </div>
          </div>
          <div class="form-group">
            <input class="btn btn-default" type="submit" value="save" />
          </div>
        </form>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

Category

一覧ページ

category_index.png

index.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('CATEGORY INDEX')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-12">
        <form action="/category/create" th:action="@{/category/create}" method="get">
          <button class="btn btn-primary" type="submit">
            Create
          </button>
        </form>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <nav>
          <ul class="pagination">
            <li>
              <a href="/category/1" th:href="@{/category/} + (${currentPage} == 1 ? 1 : ${currentPage} - 1)" aria-label="Previous">
                <span aria-hidden="true">&laquo;</span>
              </a>
            </li>
            <li th:class="${i} == ${currentPage} ? 'active' : ''" th:each="i : ${#numbers.sequence(1, maxPage)}">
              <a href="/category/1" th:href="@{/category/} + ${i}" th:text="${i}">1</a>
            </li>
            <li>
              <a href="/category/999" th:href="@{/category/} + (${currentPage} == ${maxPage} ? ${maxPage} : ${currentPage} + 1)"  aria-label="Next">
                <span aria-hidden="true">&raquo;</span>
              </a>
            </li>
          </ul>
        </nav>
        <table class="table table-striped table-bordered" th:if="${result}">
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>categoryID</th>
              <th>categoryName</th>
              <th>description</th>
              <th>picture</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="category, status : ${result}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/category/detail/1" th:href="@{/category/detail} + '/' + ${category.id}" th:text="${category.id}">id</a>
              </td>
              <td th:text="${category.categoryID}">categoryID</td>
              <td th:text="${category.categoryName}">categoryName</td>
              <td th:text="${category.description}">description</td>
              <td th:text="${#helper.substring(category.picture, 30)}">picture</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        total <span class="badge" th:text="${totalCount}">totalCount</span> currentPage <span class="badge" th:text="${currentPage}">currentPage</span> maxPage <span class="badge" th:text="${maxPage}">maxPage</span>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

詳細ページ

category_detail.png

detail.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('CATEGORY DETAIL')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-1">
        <form action="/category/edit/0" th:action="@{/category/edit/} + ${category.id}" method="get">
          <button class="btn btn-primary" type="submit">Edit</button>
        </form>
      </div>
      <div class="col-md-1">
        <form action="/category/delete" th:action="@{/category/delete}" method="post">
          <input type="hidden" name="id" value="0" th:value="${category.id}"/>
          <button class="btn btn-primary" type="submit">Delete</button>
        </form>
      </div>
      <div class="col-md-10">
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h2>Category</h2>
        <table class="table table-striped table-bordered" th:if="${category}" th:object="${category}">
          <tr>
            <th class="col-md-3">NodeID</th>
            <td class="col-md-9" th:text="*{id}">id</td>
          </tr>
          <tr>
            <th>categoryID</th>
            <td th:text="*{categoryID}">categoryID</td>
          </tr>
          <tr>
            <th>categoryName</th>
            <td th:text="*{categoryName}">categoryName</td>
          </tr>
          <tr>
            <th>description</th>
            <td th:text="*{description}">description</td>
          </tr>
          <tr>
            <th>picture</th>
            <td th:text="*{#helper.substring(picture, 30)}">picture</td>
          </tr>
        </table>
      </div>
    </div>

    <div class="row">
      <div class="col-md-12">
        <h3>Products</h3>
        <table class="table table-striped table-bordered" th:if="${category.products}">
          <caption th:text="${category.products.size() + ' products'}">size</caption>
          <thead>
            <tr>
              <th>idx</th>
              <th>NodeID</th>
              <th>productID</th>
              <th>productName</th>
              <th>quantityPerUnit</th>
              <th>unitPrice</th>
              <th>unitsInStock</th>
              <th>unitsOnOrder</th>
              <th>reorderLevel</th>
              <th>discontinued</th>
              <th>supplierID</th>
              <th>categoryID</th>
            </tr>
          </thead>
          <tbody>
            <tr th:each="product, status : ${category.products}">
              <td th:text="${status.count}">1</td>
              <td>
                <a href="/product/detail/1" th:href="@{/product/detail} + '/' + ${product.id}" th:text="${product.id}">id</a>
              </td>
              <td th:text="${product.productID}">productID</td>
              <td th:text="${product.productName}">productName</td>
              <td th:text="${product.quantityPerUnit}">quantityPerUnit</td>
              <td th:text="${product.unitPrice}">unitPrice</td>
              <td th:text="${product.unitsInStock}">unitsInStock</td>
              <td th:text="${product.unitsOnOrder}">unitsOnOrder</td>
              <td th:text="${product.reorderLevel}">reorderLevel</td>
              <td th:text="${product.discontinued}">discontinued</td>
              <td th:text="${product.supplierID}">supplierID</td>
              <td th:text="${product.categoryID}">categoryID</td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

編集ページ

category_create.png

create.html
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head th:replace="_nw_temp :: header ('CATEGORY CREATE')">
</head>
<body>

  <div class="container">
    <div class="page-header">
      <h1 th:inline="text">
        [[${header.title}]]
        <small th:text="${header.subtitle}">subtitle</small>
      </h1>
    </div>

    <div th:replace="_nw_temp :: nav"></div>

    <div class="row">
      <div class="col-md-12">

        <form class="form-horizontal" role="form" action="/caetgory/save" th:action="@{/category/save}" th:object="${categoryForm}" method="post">
          <!-- NodeID -->
          <div class="form-group">
            <label for="id_ID" class="col-sm-2 control-label">
              (hidden)nodeID
            </label>
            <div class="col-sm-10">
              <input id="id_ID" class="form-control" type="text" name="id" th:field="*{id}" />
              <span th:if="${#fields.hasErrors('id')}" th:errors="*{id}" class="help-block">error!</span>
            </div>
          </div>
          <!-- categoryID -->
          <div class="form-group">
            <label for="id_categoryID" class="col-sm-2 control-label">
              (hidden)categoryID
            </label>
            <div class="col-sm-10">
              <input id="id_categoryID" class="form-control" type="text" name="categoryID" th:field="*{categoryID}" />
              <span th:if="${#fields.hasErrors('categoryID')}" th:errors="*{categoryID}" class="help-block">error!</span>
            </div>
          </div>
          <!-- categoryName -->
          <div class="form-group">
            <label for="id_categoryName" class="col-sm-2 control-label">
              <abbr title="required">*</abbr>categoryName
            </label>
            <div class="col-sm-10">
              <input id="id_categoryName" class="form-control" type="text" name="categoryName" th:field="*{categoryName}" />
              <span th:if="${#fields.hasErrors('categoryName')}" th:errors="*{categoryName}" class="help-block">error!</span>
            </div>
          </div>
          <!-- description -->
          <div class="form-group">
            <label for="id_description" class="col-sm-2 control-label">
              description
            </label>
            <div class="col-sm-10">
              <input id="id_description" class="form-control" type="text" name="description" th:field="*{description}" />
              <span th:if="${#fields.hasErrors('description')}" th:errors="*{description}" class="help-block">error!</span>
            </div>
          </div>
          <!-- picture -->
          <div class="form-group">
            <label for="id_picture" class="col-sm-2 control-label">
              picture
            </label>
            <div class="col-sm-10">
              <input id="id_picture" class="form-control" type="text" name="picture" th:field="*{picture}" />
              <span th:if="${#fields.hasErrors('picture')}" th:errors="*{picture}" class="help-block">error!</span>
            </div>
          </div>
          <div class="form-group">
            <input class="btn btn-default" type="submit" value="save" />
          </div>
        </form>

      </div>
    </div>

    <div th:replace="_nw_temp :: footer"></div>
  </div>

  <div th:include="_nw_temp :: script"></div>
</body>
</html>

エラーページ

エラーページはデフォルトのものを使用しますので今回はなにも作成しません。

メッセージリソース

メッセージリソースは使用しませんので今回はなにも作成しません。

静的リソース

静的リソース(css,js,img等)はプロジェクトフォルダ(/actor)の直下にstaticフォルダを作成しそこへ配置します。
このアプリケーションではbootstrapとjQueryを使用しますので、それらのファイルをstatic以下にコピーします。

  • bootstrapは、"static/vendor/bootstrap-3.3.5"に配置しました。
  • jQueryは、"static/vendor/jquery"に配置しました。
tree
ROOT
├─src
├─static
│  └─vendor
│      ├─bootstrap-3.3.5
│      │  ├─css
│      │  ├─fonts
│      │  └─js
│      └─jquery
└─target

実行する

> mvn spring-boot:run

アプリケーションが起動したら下記のURLにアクセスしてカスタマー一覧ページが表示されるか確認します。

http://localhost:9000/customer/