Skip to content

Commit 221b8e5

Browse files
committed
Add human-readable URLs for categories' pages.
Fix GH #51
1 parent 9301202 commit 221b8e5

22 files changed

+80
-58
lines changed

src/main/java/ru/mystamps/web/Url.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public final class Url {
5252
public static final String INFO_SERIES_PAGE = "/series/{id}";
5353

5454
public static final String ADD_CATEGORY_PAGE = "/category/add";
55-
public static final String INFO_CATEGORY_PAGE = "/category/{id}";
55+
public static final String INFO_CATEGORY_PAGE = "/category/{id}/{slug}";
5656

5757
public static final String ADD_COUNTRY_PAGE = "/country/add";
5858
public static final String INFO_COUNTRY_PAGE = "/country/{id}/{slug}";

src/main/java/ru/mystamps/web/controller/CategoryController.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public String processInput(
7474
Category category = categoryService.add(form, currentUser);
7575

7676
String dstUrl = UriComponentsBuilder.fromUriString(Url.INFO_CATEGORY_PAGE)
77-
.buildAndExpand(category.getId())
77+
.buildAndExpand(category.getId(), category.getSlug())
7878
.toString();
7979

8080
return "redirect:" + dstUrl;

src/main/java/ru/mystamps/web/controller/SeriesController.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
import ru.mystamps.web.service.CollectionService;
5656
import ru.mystamps.web.service.CountryService;
5757
import ru.mystamps.web.service.SeriesService;
58-
import ru.mystamps.web.service.dto.EntityInfoDto;
5958
import ru.mystamps.web.service.dto.SelectEntityDto;
6059
import ru.mystamps.web.support.spring.security.SecurityContextUtils;
6160
import ru.mystamps.web.util.CatalogUtils;
@@ -98,7 +97,7 @@ public Map<Integer, Integer> getYears() {
9897
}
9998

10099
@ModelAttribute("categories")
101-
public Iterable<EntityInfoDto> getCategories(Locale userLocale) {
100+
public Iterable<SelectEntityDto> getCategories(Locale userLocale) {
102101
String lang = LocaleUtils.getLanguageOrNull(userLocale);
103102
return categoryService.findAll(lang);
104103
}

src/main/java/ru/mystamps/web/dao/CategoryDao.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,19 @@
2222
import org.springframework.data.repository.query.Param;
2323

2424
import ru.mystamps.web.entity.Category;
25-
import ru.mystamps.web.service.dto.EntityInfoDto;
25+
import ru.mystamps.web.service.dto.SelectEntityDto;
2626

2727
public interface CategoryDao extends PagingAndSortingRepository<Category, Integer> {
2828
int countByName(String name);
2929
int countByNameRu(String name);
3030

3131
@Query(
32-
"SELECT NEW ru.mystamps.web.service.dto.EntityInfoDto("
32+
"SELECT NEW ru.mystamps.web.service.dto.SelectEntityDto("
3333
+ "c.id, "
3434
+ "CASE WHEN (:lang = 'ru') THEN c.nameRu ELSE c.name END"
3535
+ ") "
3636
+ "FROM Category c "
3737
+ "ORDER BY c.name"
3838
)
39-
Iterable<EntityInfoDto> findAllAsSelectEntries(@Param("lang") String lang);
39+
Iterable<SelectEntityDto> findAllAsSelectEntries(@Param("lang") String lang);
4040
}

src/main/java/ru/mystamps/web/dao/SeriesDao.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public interface SeriesDao extends CrudRepository<Series, Integer> {
4646
@Query(
4747
"SELECT NEW ru.mystamps.web.service.dto.SeriesInfoDto("
4848
+ "s.id, "
49-
+ "cat.id, CASE WHEN (:lang = 'ru') THEN cat.nameRu ELSE cat.name END, "
49+
+ "cat.id, cat.slug, CASE WHEN (:lang = 'ru') THEN cat.nameRu ELSE cat.name END, "
5050
+ "c.id, c.slug, CASE WHEN (:lang = 'ru') THEN c.nameRu ELSE c.name END, "
5151
+ "s.releaseDay, "
5252
+ "s.releaseMonth, "
@@ -67,7 +67,7 @@ Iterable<SeriesInfoDto> findByAsSeriesInfo(
6767
@Query(
6868
"SELECT NEW ru.mystamps.web.service.dto.SeriesInfoDto("
6969
+ "s.id, "
70-
+ "cat.id, CASE WHEN (:lang = 'ru') THEN cat.nameRu ELSE cat.name END, "
70+
+ "cat.id, cat.slug, CASE WHEN (:lang = 'ru') THEN cat.nameRu ELSE cat.name END, "
7171
+ "c.id, c.slug, CASE WHEN (:lang = 'ru') THEN c.nameRu ELSE c.name END, "
7272
+ "s.releaseDay, "
7373
+ "s.releaseMonth, "
@@ -88,7 +88,7 @@ Iterable<SeriesInfoDto> findByAsSeriesInfo(
8888
@Query(
8989
"SELECT NEW ru.mystamps.web.service.dto.SeriesInfoDto("
9090
+ "s.id, "
91-
+ "cat.id, CASE WHEN (:lang = 'ru') THEN cat.nameRu ELSE cat.name END, "
91+
+ "cat.id, cat.slug, CASE WHEN (:lang = 'ru') THEN cat.nameRu ELSE cat.name END, "
9292
+ "c.id, c.slug, CASE WHEN (:lang = 'ru') THEN c.nameRu ELSE c.name END, "
9393
+ "s.releaseDay, "
9494
+ "s.releaseMonth, "

src/main/java/ru/mystamps/web/dao/impl/SeriesInfoDtoRowMapper.java

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public SeriesInfoDto mapRow(ResultSet resultSet, int i) throws SQLException {
3535
Integer quantity = resultSet.getInt("quantity");
3636
Boolean perforated = resultSet.getBoolean("perforated");
3737
Integer categoryId = resultSet.getInt("category_id");
38+
String categorySlug = resultSet.getString("category_slug");
3839
String categoryName = resultSet.getString("category_name");
3940
Integer countryId = JdbcUtils.getInteger(resultSet, "country_id");
4041
String countrySlug = resultSet.getString("country_slug");
@@ -43,6 +44,7 @@ public SeriesInfoDto mapRow(ResultSet resultSet, int i) throws SQLException {
4344
return new SeriesInfoDto(
4445
seriesId,
4546
categoryId,
47+
categorySlug,
4648
categoryName,
4749
countryId,
4850
countrySlug,

src/main/java/ru/mystamps/web/entity/Category.java

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
public class Category implements LocalizedEntity {
4141

4242
public static final int NAME_LENGTH = 50;
43+
public static final int SLUG_LENGTH = NAME_LENGTH;
4344

4445
@Id
4546
@GeneratedValue
@@ -51,6 +52,9 @@ public class Category implements LocalizedEntity {
5152
@Column(name = "name_ru", length = NAME_LENGTH, unique = true, nullable = false)
5253
private String nameRu;
5354

55+
@Column(length = SLUG_LENGTH, nullable = false)
56+
private String slug;
57+
5458
@Embedded
5559
private MetaInfo metaInfo; // NOPMD
5660

src/main/java/ru/mystamps/web/service/CategoryService.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
import ru.mystamps.web.entity.Collection;
2222
import ru.mystamps.web.entity.User;
2323
import ru.mystamps.web.service.dto.AddCategoryDto;
24-
import ru.mystamps.web.service.dto.EntityInfoDto;
24+
import ru.mystamps.web.service.dto.SelectEntityDto;
2525

2626
public interface CategoryService {
2727
Category add(AddCategoryDto dto, User user);
28-
Iterable<EntityInfoDto> findAll(String lang);
28+
Iterable<SelectEntityDto> findAll(String lang);
2929
long countAll();
3030
long countCategoriesOf(Collection collection);
3131
int countByName(String name);

src/main/java/ru/mystamps/web/service/CategoryServiceImpl.java

+7-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
import ru.mystamps.web.entity.User;
3636
import ru.mystamps.web.dao.CategoryDao;
3737
import ru.mystamps.web.service.dto.AddCategoryDto;
38-
import ru.mystamps.web.service.dto.EntityInfoDto;
38+
import ru.mystamps.web.service.dto.SelectEntityDto;
39+
import ru.mystamps.web.util.SlugUtils;
3940

4041
@RequiredArgsConstructor
4142
public class CategoryServiceImpl implements CategoryService {
@@ -57,6 +58,10 @@ public Category add(AddCategoryDto dto, User user) {
5758
category.setName(dto.getName());
5859
category.setNameRu(dto.getNameRu());
5960

61+
String slug = SlugUtils.slugify(dto.getName());
62+
Validate.isTrue(slug != null, "Slug for string '%s' is null", dto.getName());
63+
category.setSlug(slug);
64+
6065
Date now = new Date();
6166
category.getMetaInfo().setCreatedAt(now);
6267
category.getMetaInfo().setUpdatedAt(now);
@@ -72,7 +77,7 @@ public Category add(AddCategoryDto dto, User user) {
7277

7378
@Override
7479
@Transactional(readOnly = true)
75-
public Iterable<EntityInfoDto> findAll(String lang) {
80+
public Iterable<SelectEntityDto> findAll(String lang) {
7681
return categoryDao.findAllAsSelectEntries(lang);
7782
}
7883

src/main/java/ru/mystamps/web/service/dto/EntityInfoDto.java

-30
This file was deleted.

src/main/java/ru/mystamps/web/service/dto/SeriesInfoDto.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class SeriesInfoDto {
2626
private final Integer id;
2727

2828
@SuppressWarnings("PMD.SingularField")
29-
private final EntityInfoDto category;
29+
private final LinkEntityDto category;
3030

3131
@SuppressWarnings("PMD.SingularField")
3232
private final LinkEntityDto country;
@@ -49,13 +49,13 @@ public class SeriesInfoDto {
4949
@SuppressWarnings({"checkstyle:parameternumber", "PMD.ExcessiveParameterList"})
5050
public SeriesInfoDto(
5151
Integer id,
52-
Integer categoryId, String categoryName,
52+
Integer categoryId, String categorySlug, String categoryName,
5353
Integer countryId, String countrySlug, String countryName,
5454
Integer releaseDay, Integer releaseMonth, Integer releaseYear,
5555
Integer quantity,
5656
Boolean perforated) {
5757
this.id = id;
58-
this.category = new EntityInfoDto(categoryId, categoryName);
58+
this.category = new LinkEntityDto(categoryId, categorySlug, categoryName);
5959
this.country = new LinkEntityDto(countryId, countrySlug, countryName);
6060
this.releaseDay = releaseDay;
6161
this.releaseMonth = releaseMonth;

src/main/resources/liquibase/version/0.3.xml

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@
1111
<include file="0.3/2014-08-16--users_activation_lang.xml" relativeToChangelogFile="true" />
1212
<include file="0.3/2014-08-30--collections.xml" relativeToChangelogFile="true" />
1313
<include file="0.3/2014-09-17--country_slug.xml" relativeToChangelogFile="true" />
14+
<include file="0.3/2014-09-17--category_slug.xml" relativeToChangelogFile="true" />
1415

1516
</databaseChangeLog>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<databaseChangeLog
3+
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
6+
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
7+
8+
<changeSet id="add-slug-column-to-categories-table" author="php-coder" context="scheme">
9+
<comment>Adds slug column to categories table</comment>
10+
11+
<addColumn tableName="categories">
12+
<column name="slug" type="VARCHAR(50)" afterColumn="name_ru" />
13+
</addColumn>
14+
15+
</changeSet>
16+
17+
<changeSet id="update-categories-slug" author="php-coder" context="test-data,prod-data">
18+
<comment>Sets value of slug field to value of name field in lower case</comment>
19+
20+
<update tableName="categories">
21+
<column name="slug" valueComputed="LOWER(REPLACE(name, ' ', '-'))" />
22+
</update>
23+
24+
</changeSet>
25+
26+
<changeSet id="make-slug-field-not-nullable" author="php-coder" context="scheme">
27+
<comment>Marks categories.slug field as NOT NULL</comment>
28+
29+
<addNotNullConstraint tableName="categories" columnName="slug" columnDataType="VARCHAR(50)" />
30+
31+
</changeSet>
32+
33+
</databaseChangeLog>

src/main/resources/sql/series_dao_queries.properties

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ series.find_last_added_sql = \
66
, s.quantity \
77
, s.perforated \
88
, cat.id AS category_id \
9+
, cat.slug AS category_slug \
910
, cat.name AS category_name \
1011
, count.id AS country_id \
1112
, count.slug AS country_slug \
@@ -26,6 +27,7 @@ series.find_last_added_ru_sql = \
2627
, s.quantity \
2728
, s.perforated \
2829
, cat.id AS category_id \
30+
, cat.slug AS category_slug \
2931
, cat.name_ru AS category_name \
3032
, count.id AS country_id \
3133
, count.slug AS country_slug \

src/main/webapp/WEB-INF/views/category/info.html

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<link rel="stylesheet" href="http://yandex.st/bootstrap/2.3.1/css/bootstrap.min.css" th:href="${BOOTSTRAP_CSS}" />
1111
<link rel="stylesheet" href="http://yandex.st/bootstrap/2.3.1/css/bootstrap-responsive.min.css" th:href="${BOOTSTRAP_RESPONSIVE_CSS}" />
1212
<link rel="stylesheet" href="../../static/styles/main.css" th:href="${MAIN_CSS}" />
13+
<link rel="canonical" href="" th:href="@{${PUBLIC_URL} + ${INFO_CATEGORY_PAGE}(id=${category.id},slug=${category.slug})}" />
1314
</head>
1415
<body lang="en" th:lang="${#locale.language == 'ru' ? 'ru' : 'en'}">
1516
<div class="row-fluid">

src/main/webapp/WEB-INF/views/collection/info.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ <h3 th:text="#{t_collection_of(${ownerName})}">
8282
<ul th:if="${not #lists.isEmpty(seriesOfCollection)}" th:remove="all-but-first">
8383
<li th:each="series : ${seriesOfCollection}">
8484
<span th:if="${series.category.id != null}" th:remove="tag">
85-
<a href="../category/info.html" th:href="@{${INFO_CATEGORY_PAGE}(id=${series.category.id})}" th:text="${series.category.name}">Animals</a>&nbsp;&raquo;
85+
<a href="../category/info.html" th:href="@{${INFO_CATEGORY_PAGE}(id=${series.category.id},slug=${series.category.slug})}" th:text="${series.category.name}">Animals</a>&nbsp;&raquo;
8686
</span>
8787

8888
<span th:if="${series.country.id != null}" th:remove="tag">

src/main/webapp/WEB-INF/views/country/info.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ <h3 th:text="${country.getLocalizedName(#locale)}">
6060
<ul th:if="${not #lists.isEmpty(seriesOfCountry)}" th:remove="all-but-first">
6161
<li th:each="series : ${seriesOfCountry}">
6262
<span th:if="${series.category.id != null}" th:remove="tag">
63-
<a href="../category/info.html" th:href="@{${INFO_CATEGORY_PAGE}(id=${series.category.id})}" th:text="${series.category.name}">Animals</a>&nbsp;&raquo;
63+
<a href="../category/info.html" th:href="@{${INFO_CATEGORY_PAGE}(id=${series.category.id},slug=${series.category.slug})}" th:text="${series.category.name}">Animals</a>&nbsp;&raquo;
6464
</span>
6565

6666
<a href="../series/info.html" th:href="@{${INFO_SERIES_PAGE}(id=${series.id})}">

src/main/webapp/WEB-INF/views/series/info.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
</dt>
6565
<dd id="category_name">
6666
<a href="../category/info.html"
67-
th:href="@{${INFO_CATEGORY_PAGE}(id=${series.category.id})}"
67+
th:href="@{${INFO_CATEGORY_PAGE}(id=${series.category.id},slug=${series.category.slug})}"
6868
th:text="${series.category.getLocalizedName(#locale)}">
6969
Animals
7070
</a>

src/main/webapp/WEB-INF/views/site/index.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
<div th:remove="all-but-first">
7979
<p th:each="series : ${recentlyAddedSeries}">
8080
<span th:if="${series.category.id != null}" th:remove="tag">
81-
<a href="../category/info.html" th:href="@{${INFO_CATEGORY_PAGE}(id=${series.category.id})}" th:text="${series.category.name}">Animals</a>&nbsp;&raquo;
81+
<a href="../category/info.html" th:href="@{${INFO_CATEGORY_PAGE}(id=${series.category.id},slug=${series.category.slug})}" th:text="${series.category.name}">Animals</a>&nbsp;&raquo;
8282
</span>
8383

8484
<span th:if="${series.country.id != null}" th:remove="tag">

src/test/groovy/ru/mystamps/web/service/CategoryServiceImplTest.groovy

+5-5
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import ru.mystamps.web.dao.JdbcCategoryDao
2525
import ru.mystamps.web.entity.Category
2626
import ru.mystamps.web.entity.User
2727
import ru.mystamps.web.model.AddCategoryForm
28-
import ru.mystamps.web.service.dto.EntityInfoDto
28+
import ru.mystamps.web.service.dto.SelectEntityDto
2929
import ru.mystamps.web.tests.DateUtils
3030

3131
class CategoryServiceImplTest extends Specification {
@@ -163,15 +163,15 @@ class CategoryServiceImplTest extends Specification {
163163

164164
def "findAll(String) should call dao"() {
165165
given:
166-
EntityInfoDto category1 = new EntityInfoDto(1, 'First Category')
166+
SelectEntityDto category1 = new SelectEntityDto(1, 'First Category')
167167
and:
168-
EntityInfoDto category2 = new EntityInfoDto(2, 'Second Category')
168+
SelectEntityDto category2 = new SelectEntityDto(2, 'Second Category')
169169
and:
170-
List<EntityInfoDto> expectedCategories = [ category1, category2 ]
170+
List<SelectEntityDto> expectedCategories = [ category1, category2 ]
171171
and:
172172
categoryDao.findAllAsSelectEntries(_ as String) >> expectedCategories
173173
when:
174-
Iterable<EntityInfoDto> resultCategories = service.findAll('fr')
174+
Iterable<SelectEntityDto> resultCategories = service.findAll('fr')
175175
then:
176176
resultCategories == expectedCategories
177177
}

src/test/java/ru/mystamps/web/tests/cases/WhenAdminAddCategory.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ public void categoryNameRuShouldBeStripedFromLeadingAndTrailingSpaces() {
203203
public void shouldBeRedirectedToPageWithInfoAboutCategoryAfterCreation() {
204204
page.addCategory(TEST_CATEGORY_NAME_EN, TEST_CATEGORY_NAME_RU);
205205

206-
String expectedUrl = Url.INFO_CATEGORY_PAGE.replace("{id}", "\\d+");
206+
String expectedUrl = Url.INFO_CATEGORY_PAGE
207+
.replace("{id}", "\\d+")
208+
.replace("{slug}", TEST_CATEGORY_NAME_EN.toLowerCase());
207209

208210
assertThat(page.getCurrentUrl()).matches(expectedUrl);
209211
assertThat(page.getHeader()).isEqualTo(TEST_CATEGORY_NAME_EN);

src/test/java/ru/mystamps/web/tests/cases/WhenAnonymousUserOpenNotExistingCategoryPage.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ public WhenAnonymousUserOpenNotExistingCategoryPage() {
4040
@Test(groups = "logic")
4141
public void shouldShow404Page() {
4242
String absentCategoryId = "888";
43-
page.open(Url.INFO_CATEGORY_PAGE.replace("{id}", absentCategoryId));
43+
String url = Url.INFO_CATEGORY_PAGE
44+
.replace("{id}", absentCategoryId)
45+
.replace("slug", "category-404-error-test");
46+
page.open(url);
4447

4548
checkStandardStructure();
4649

0 commit comments

Comments
 (0)