Skip to content

Commit 48a6772

Browse files
committed
/category/add: add validation for unique slug.
This fixes DuplicateKeyException when name contains mix of spaces and hyphens. Fix #486
1 parent 11ae55f commit 48a6772

File tree

12 files changed

+147
-1
lines changed

12 files changed

+147
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
public interface CategoryDao {
2727
Integer add(AddCategoryDbDto category);
2828
long countAll();
29+
long countBySlug(String slug);
2930
long countByName(String name);
3031
long countByNameRu(String name);
3132
long countCategoriesOfCollection(Integer collectionId);

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public class JdbcCategoryDao implements CategoryDao {
5050
@Value("${category.count_all_categories}")
5151
private String countAllSql;
5252

53+
@Value("${category.count_categories_by_slug}")
54+
private String countBySlugSql;
55+
5356
@Value("${category.count_categories_by_name}")
5457
private String countByNameSql;
5558

@@ -108,6 +111,15 @@ public long countAll() {
108111
);
109112
}
110113

114+
@Override
115+
public long countBySlug(String slug) {
116+
return jdbcTemplate.queryForObject(
117+
countBySlugSql,
118+
Collections.singletonMap("slug", slug),
119+
Long.class
120+
);
121+
}
122+
111123
@Override
112124
public long countByName(String name) {
113125
return jdbcTemplate.queryForObject(

src/main/java/ru/mystamps/web/model/AddCategoryForm.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import ru.mystamps.web.service.dto.AddCategoryDto;
3030
import ru.mystamps.web.validation.jsr303.UniqueCategoryName;
3131
import ru.mystamps.web.validation.jsr303.UniqueCategoryName.Lang;
32+
import ru.mystamps.web.validation.jsr303.UniqueCategorySlug;
3233

3334
import static ru.mystamps.web.validation.ValidationRules.CATEGORY_NAME_EN_REGEXP;
3435
import static ru.mystamps.web.validation.ValidationRules.CATEGORY_NAME_MAX_LENGTH;
@@ -46,7 +47,8 @@
4647
Group.Level3.class,
4748
Group.Level4.class,
4849
Group.Level5.class,
49-
Group.Level6.class
50+
Group.Level6.class,
51+
Group.Level7.class
5052
})
5153
public class AddCategoryForm implements AddCategoryDto {
5254

@@ -81,6 +83,7 @@ public class AddCategoryForm implements AddCategoryDto {
8183
)
8284
})
8385
@UniqueCategoryName(lang = Lang.EN, groups = Group.Level6.class)
86+
@UniqueCategorySlug(groups = Group.Level7.class)
8487
private String name;
8588

8689
@NotEmpty(groups = Group.Level1.class)

src/main/java/ru/mystamps/web/model/Group.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,7 @@ interface Level5 {
3737
interface Level6 {
3838
}
3939

40+
interface Level7 {
41+
}
42+
4043
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public interface CategoryService {
2929
LinkEntityDto findOneAsLinkEntity(String slug, String lang);
3030
long countAll();
3131
long countCategoriesOf(Integer collectionId);
32+
long countBySlug(String slug);
3233
long countByName(String name);
3334
long countByNameRu(String name);
3435
long countAddedSince(Date date);

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ public long countCategoriesOf(Integer collectionId) {
109109
return categoryDao.countCategoriesOfCollection(collectionId);
110110
}
111111

112+
@Override
113+
@Transactional(readOnly = true)
114+
public long countBySlug(String slug) {
115+
Validate.isTrue(slug != null, "Category slug must be non null");
116+
117+
return categoryDao.countBySlug(slug);
118+
}
119+
112120
@Override
113121
@Transactional(readOnly = true)
114122
public long countByName(String name) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (C) 2009-2016 Slava Semushin <slava.semushin@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.validation.jsr303;
19+
20+
import java.lang.annotation.Documented;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.Target;
23+
24+
import javax.validation.Constraint;
25+
import javax.validation.Payload;
26+
27+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
28+
import static java.lang.annotation.ElementType.FIELD;
29+
import static java.lang.annotation.ElementType.METHOD;
30+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
31+
32+
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
33+
@Retention(RUNTIME)
34+
@Constraint(validatedBy = UniqueCategorySlugValidator.class)
35+
@Documented
36+
public @interface UniqueCategorySlug {
37+
String message() default "{ru.mystamps.web.validation.jsr303.UniqueCategorySlug.message}";
38+
Class<?>[] groups() default {};
39+
Class<? extends Payload>[] payload() default {};
40+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (C) 2009-2016 Slava Semushin <slava.semushin@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program; if not, write to the Free Software
16+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
package ru.mystamps.web.validation.jsr303;
19+
20+
import javax.validation.ConstraintValidator;
21+
import javax.validation.ConstraintValidatorContext;
22+
23+
import lombok.RequiredArgsConstructor;
24+
25+
import ru.mystamps.web.service.CategoryService;
26+
import ru.mystamps.web.util.SlugUtils;
27+
28+
@RequiredArgsConstructor
29+
public class UniqueCategorySlugValidator
30+
implements ConstraintValidator<UniqueCategorySlug, String> {
31+
32+
private final CategoryService categoryService;
33+
34+
@Override
35+
public void initialize(UniqueCategorySlug annotation) {
36+
// Intentionally empty: nothing to initialize
37+
}
38+
39+
@Override
40+
public boolean isValid(String value, ConstraintValidatorContext ctx) {
41+
42+
if (value == null) {
43+
return true;
44+
}
45+
46+
String slug = SlugUtils.slugify(value);
47+
48+
return categoryService.countBySlug(slug) == 0;
49+
}
50+
51+
}

src/main/resources/ru/mystamps/i18n/ValidationMessages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ru.mystamps.web.validation.jsr303.FieldsMatch.message = Field '{second}' must ma
99
ru.mystamps.web.validation.jsr303.UniqueLogin.message = Login already exists
1010
ru.mystamps.web.validation.jsr303.UniqueCountryName.message = Country already exists
1111
ru.mystamps.web.validation.jsr303.UniqueCategoryName.message = Category already exists
12+
ru.mystamps.web.validation.jsr303.UniqueCategorySlug.message = Category with similar name already exists
1213
ru.mystamps.web.validation.jsr303.ExistingActivationKey.message = Wrong activation key
1314
ru.mystamps.web.validation.jsr303.Email.message = Wrong e-mail address
1415
ru.mystamps.web.validation.jsr303.NotEmptyFilename.message = Value must not be empty

src/main/resources/ru/mystamps/i18n/ValidationMessages_ru.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ru.mystamps.web.validation.jsr303.FieldsMatch.message = Поле '{second}' до
99
ru.mystamps.web.validation.jsr303.UniqueLogin.message = Логин уже существует
1010
ru.mystamps.web.validation.jsr303.UniqueCountryName.message = Страна уже есть в базе
1111
ru.mystamps.web.validation.jsr303.UniqueCategoryName.message = Категория уже есть в базе
12+
ru.mystamps.web.validation.jsr303.UniqueCategorySlug.message = Категория с похожим названием уже есть в базе
1213
ru.mystamps.web.validation.jsr303.ExistingActivationKey.message = Неправильный код активации
1314
ru.mystamps.web.validation.jsr303.Email.message = Неправильный адрес электронной почты
1415
ru.mystamps.web.validation.jsr303.ReleaseDateIsNotInFuture.message = Дата выпуска не может быть в будущем

src/main/resources/sql/category_dao_queries.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ category.count_all_categories = \
2323
SELECT COUNT(*) \
2424
FROM categories
2525

26+
category.count_categories_by_slug = \
27+
SELECT COUNT(*) \
28+
FROM categories \
29+
WHERE slug = :slug
30+
2631
category.count_categories_by_name = \
2732
SELECT COUNT(*) \
2833
FROM categories \

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,26 @@ class CategoryServiceImplTest extends Specification {
312312
}) >> 0L
313313
}
314314

315+
//
316+
// Tests for countBySlug()
317+
//
318+
319+
def "countBySlug() should throw exception when slug is null"() {
320+
when:
321+
service.countBySlug(null)
322+
then:
323+
thrown IllegalArgumentException
324+
}
325+
326+
def "countBySlug() should call dao"() {
327+
given:
328+
categoryDao.countBySlug(_ as String) >> 3L
329+
when:
330+
long result = service.countBySlug('any-slug')
331+
then:
332+
result == 3L
333+
}
334+
315335
//
316336
// Tests for countByName()
317337
//

0 commit comments

Comments
 (0)