Skip to content

Commit daea019

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

File tree

11 files changed

+143
-1
lines changed

11 files changed

+143
-1
lines changed

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

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

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

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

53+
@Value("${country.count_countries_by_slug}")
54+
private String countBySlugSql;
55+
5356
@Value("${country.count_countries_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/AddCountryForm.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.AddCountryDto;
3030
import ru.mystamps.web.validation.jsr303.UniqueCountryName;
3131
import ru.mystamps.web.validation.jsr303.UniqueCountryName.Lang;
32+
import ru.mystamps.web.validation.jsr303.UniqueCountrySlug;
3233

3334
import static ru.mystamps.web.validation.ValidationRules.COUNTRY_NAME_EN_REGEXP;
3435
import static ru.mystamps.web.validation.ValidationRules.COUNTRY_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 AddCountryForm implements AddCountryDto {
5254

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

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public interface CountryService {
2929
LinkEntityDto findOneAsLinkEntity(String slug, String lang);
3030
long countAll();
3131
long countCountriesOf(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/CountryServiceImpl.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ public long countCountriesOf(Integer collectionId) {
110110
return countryDao.countCountriesOfCollection(collectionId);
111111
}
112112

113+
@Override
114+
@Transactional(readOnly = true)
115+
public long countBySlug(String slug) {
116+
Validate.isTrue(slug != null, "Country slug must be non null");
117+
118+
return countryDao.countBySlug(slug);
119+
}
120+
113121
@Override
114122
@Transactional(readOnly = true)
115123
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 <[email protected]>
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 = UniqueCountrySlugValidator.class)
35+
@Documented
36+
public @interface UniqueCountrySlug {
37+
String message() default "{ru.mystamps.web.validation.jsr303.UniqueCountrySlug.message}";
38+
Class<?>[] groups() default {};
39+
Class<? extends Payload>[] payload() default {};
40+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright (C) 2009-2016 Slava Semushin <[email protected]>
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.CountryService;
26+
import ru.mystamps.web.util.SlugUtils;
27+
28+
@RequiredArgsConstructor
29+
public class UniqueCountrySlugValidator implements ConstraintValidator<UniqueCountrySlug, String> {
30+
31+
private final CountryService countryService;
32+
33+
@Override
34+
public void initialize(UniqueCountrySlug annotation) {
35+
// Intentionally empty: nothing to initialize
36+
}
37+
38+
@Override
39+
public boolean isValid(String value, ConstraintValidatorContext ctx) {
40+
41+
if (value == null) {
42+
return true;
43+
}
44+
45+
String slug = SlugUtils.slugify(value);
46+
47+
return countryService.countBySlug(slug) == 0;
48+
}
49+
50+
}

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.UniqueCountrySlug.message = Country with similar name already exists
1213
ru.mystamps.web.validation.jsr303.UniqueCategorySlug.message = Category with similar name already exists
1314
ru.mystamps.web.validation.jsr303.ExistingActivationKey.message = Wrong activation key
1415
ru.mystamps.web.validation.jsr303.Email.message = Wrong e-mail address

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.UniqueCountrySlug.message = Страна с похожим названием уже есть в базе
1213
ru.mystamps.web.validation.jsr303.UniqueCategorySlug.message = Категория с похожим названием уже есть в базе
1314
ru.mystamps.web.validation.jsr303.ExistingActivationKey.message = Неправильный код активации
1415
ru.mystamps.web.validation.jsr303.Email.message = Неправильный адрес электронной почты

src/main/resources/sql/country_dao_queries.properties

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

26+
country.count_countries_by_slug = \
27+
SELECT COUNT(*) \
28+
FROM countries \
29+
WHERE slug = :slug
30+
2631
country.count_countries_by_name = \
2732
SELECT COUNT(*) \
2833
FROM countries \

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

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

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

0 commit comments

Comments
 (0)