概要
グラフデータベースにNeo4j、WEBアプリケーションフレームワークにSpring Boot 1.3.0.M3とSpring Data Neo4j 4.0.0-RC1を使用した検索アプリケーションを開発します。
検索機能をメインに開発しますが一部更新系も実装します。
このアプリケーションで使用するデータセットは、Neo4jに用意されてるNorthwind Graph
を使わせて貰いました。
環境
- Windows7 (64bit)
- Java 1.8.0_45
- [Spring Boot] (https://github.com/spring-projects/spring-boot/wiki) 1.3.0.M3
- Spring Data Neo4j 4.0.0-RC1
- [Neo4j OGM] (https://github.com/neo4j/neo4j-ogm) 1.1.0
- [Neo4j Community Edition] (http://neo4j.com/) 2.2.2
- Eclipse 4.4
- Maven 3.3.3
参考
下記のサイトを参考にさせていただきました。
Neo4j
- [Neo4j] (http://neo4j.com/)
- [Neo4j - the World's Leading Graph Database - Youtube] (https://www.youtube.com/channel/UCvze3hU6OZBkB1vkhH2lH9Q)
Spring Data Neo4J
- [Spring Data Neo4j] (http://projects.spring.io/spring-data-neo4j/)
- [Neo4j OGM - An Object Graph Mapping Library for Neo4j] (http://neo4j.com/docs/ogm/java/stable/)
- [Spring Data Neo4j - Spring JIRA] (https://jira.spring.io/browse/DATAGRAPH/?selectedTab=com.atlassian.jira.jira-projects-plugin:summary-panel)
Spring Data Neo4j Version 4.0
- [*** Spring Data Neo4j - Parent *** 4.0.0.RC1 API] (http://docs.spring.io/spring-data/neo4j/docs/4.0.0.RC1/api/)
- [Good Relationships: The Spring Data Neo4j Guide Book] (http://docs.spring.io/autorepo/docs/spring-data-neo4j/4.0.x-SNAPSHOT/reference/html/)
- [Announcing Spring Data Neo4j 4.0] (http://graphaware.com/neo4j/2015/03/24/announcing-spring-data-neo4j-4-0.html)
- [The essence of Spring Data Neo4j 4] (https://www.airpair.com/neo4j/posts/the-essence-of-spring-data-neo4j-4)
- [Error creating bean with name 'getSessionFactory' under Windows (sdn4) #270] (https://github.com/spring-projects/spring-data-neo4j/issues/270)
Stack Overflow
- [Newest 'spring-data-neo4j-4' Questions - Stack Overflow] (http://stackoverflow.com/questions/tagged/spring-data-neo4j-4)
sdn4 example
- [Test project for the Northwind dataset with SDN4] (https://github.com/neo4j-examples/sdn4-northwind)
- [Hilly Fields Technical College] (https://github.com/neo4j-examples/sdn4-university)
- [Cineasts.net] (https://github.com/neo4j-examples/sdn4-cineasts)
Github
ソースコードは[sdn4rc2-example] (https://github.com/rubytomato/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]
の様に角括弧で表現しました。
(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
データ作成方法についての説明画面が表示されますので、その説明に従ってデータを作成します。
上記の手順でデータの作成が終わったら下記の処理を実行してデータの修正を行います。
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の編集
<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を作成します。
# 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"に指定しました。
<?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>
ビルド
この時点で動作検証を兼ねてビルドします。
> mvn package
ビルドが成功したら生成したjarファイルを実行します。
コマンドプロンプトに"Hello World!"と表示されれば成功です。
> cd target
> java -jar actor-1.0-SNAPSHOT.jar
Hello World!
アプリケーションの開発
App
エンドポイントとなるクラスを作成します。
すでにサンプルの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からはリモートサーバーのみで、組み込みサーバーは使用できないようです。
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
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;
}
}
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
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;
}
}
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
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;
}
}
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
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;
}
}
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
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;
}
}
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
各サービスクラスが継承する基底クラスを作成します。
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();
}
ページング
ページング用の情報を保持するクラスとインタフェースを作成します。
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;
}
}
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
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
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
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
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
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
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
サービスクラスが使用するユーティリティメソッドを集めたクラスを作成します。
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
クラスを作成します。
各ノードのコントローラーはこのクラスを継承します。
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
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");
}});
}
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
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");
}});
}
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
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");
}});
}
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
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");
}});
}
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
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");
}});
}
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テンプレートエンジンで使用するヘルパークラスを作成します。
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
メソッドは指定文字列の先頭から指定する長さの文字を切り出します。
テンプレートファイル内からは下記のように使用することができます。
<td th:text="${#helper.substring(category.picture, 30)}">picture</td>
上記のヘルパークラスをテンプレートから使用できるようにするには下記のようなクラスを作成します。
[カスタム Utility Object を追加する ー Thymeleaf] (http://dev.classmethod.jp/client-side/thymeleaf/custome-utility-objects/)を参考にさせて頂きました。
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フォルダを作成します。
また、各ノード用のテンプレートファイルは下記の通り作成します。
/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
にまとめます。
<!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
一覧ページ
<!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">«</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">»</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>
詳細ページ
<!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>
編集ページ
<!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
一覧ページ
<!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">«</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">»</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>
詳細ページ
<!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>
編集ページ
<!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
一覧ページ
<!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">«</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">»</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>
詳細ページ
<!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>
編集ページ
<!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
一覧ページ
<!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">«</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">»</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>
詳細ページ
<!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>
編集ページ
<!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
一覧ページ
<!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">«</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">»</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>
詳細ページ
<!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>
編集ページ
<!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"に配置しました。
ROOT
├─src
├─static
│ └─vendor
│ ├─bootstrap-3.3.5
│ │ ├─css
│ │ ├─fonts
│ │ └─js
│ └─jquery
└─target
実行する
> mvn spring-boot:run
アプリケーションが起動したら下記のURLにアクセスしてカスタマー一覧ページが表示されるか確認します。