To-Read sites 4.0

Sometimes, when creating web application, we want to enable user to upload files to a server.

The uploaded file can be an image, document or any other regular file. Developing our application using Spring should be relatively simple, so we need simple method for uploading files using this beautiful framework :-).

In our example I’ll use a Company entity, which among the others will contain an Image object with Data of the image and its name.

My Company entity looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Entity
@Table
public class Company {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long id;

	@ManyToOne(cascade = CascadeType.ALL)
	private Address address;

	@NotEmpty
	private String name;

	@OneToOne(cascade = CascadeType.ALL)
	private Image image;
    ... accessors ommitted
}

As you can see I need company Address, Name and Image. In this example image is identified by name and data (content of image file), but you can do whatever you want with uploaded data, during conversion process.

On Spring Framework’s documentation sites we can see a usage of custom editor support to convert file uploaded using HTML form into regular Java object.

I went in a different way and used ConversionService to easily convert file from HTTP POST request, which is more comfortable for me. If you want to use ConversionService you’ll need to create a converter, which implements org.springframework.core.convert.converter.Converter interface.

Let’s create converter then:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
...
import org.springframework.core.convert.converter.Converter;
import org.springframework.web.multipart.MultipartFile;
...
public class MultipartFileToImageConverter implements Converter<MultipartFile, Image> {
	@Override
	public Image convert(final MultipartFile source) {
		final Image image = new Image();
                image.setData(source.getBytes());
		image.setName(source.getOriginalFilename());
		return image;
	}
}

We need to register our converter in conversion service, so in application context we add:

1
2
3
4
5
6
7
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
	<property name="converters">
		<set>
			<bean class="pl.greenpath.converter.MultipartFileToImageConverter">
		</set>
	</property>
</bean>

I use annotation-based configuration of controllers, so configuration of controller is pretty straightforward:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    @Autowired
    CompanyService companyService;

    ...

	@RequestMapping(value = "", method = RequestMethod.POST)
	public String doEdit(@Valid final Company company, final Errors errors) {
		if (errors.hasErrors()) {
			return "admin/companies/edit";
		} else {
			companyService.save(company);
			return "redirect:/admin/companies/" + company.getId();
		}
	}

Going this way we have company object with filled data from HTML form. To obtain image data we just need to invoke e.g. company.getImage().getName();

The main advantage of this solution is its simplicity. You don’t need to create configuration of editors in your controller. All you need is to create a converter and register it in ConversionService. Spring will do all other stuff for you.

I hope it will help simplifying your web app :-).

Walidacja identyczności haseł – Hibernate Validator, Spring Framework

Natknąłem się na problem walidacji identyczności haseł w formularzu rejestracji użytkownika. Korzystam ze Spring Frameworka, a do walidacji wykorzystuję bibliotekę Hibernate Validator w wersji 4.

Pierwszym problemem okazał się brak pola confirmPassword w klasie User. Poszperałem na forach i znalazłem w rozwiązania tego problemu:

  • Stworzyć dodatkowe pole w klasie User – confirmPassword
  • Stworzyć dodatkową klasę, przypuśćmy CreateUserForm, mniej więcej tak:
1
2
3
4
5
class CreateUserForm {
    String confirmPassword;
    User user;
    (... )
}

Rozwiązanie to wydaje mi się “ładniejsze” niż pierwsze, gdyż nie tworzymy w klasie, wykorzystywanej przy każdym requescie pola, które w ogóle nam jest niepotrzebne.

Kolejnym problemem okazała się walidacja. Chcieliśmy w 100% korzystać z funkcjonalności, jakie dają nam adnotacje z Hibernate Validatora. Brakuje jednak tam adnotacji umożliwiającej porównywanie ze sobą 2 pól.

Tutaj też skorzystałem z forów. Jedna osoba z teamu hibernate poleciła mi wykorzystanie ograniczeń (constraints) przypisanych do klas i zmianę domyślnego błędu zwracanego przez ten walidator.

Poniżej przedstawię rozwiązanie od jakiego doszedłem:

Poniżej znajduje się definicja interfejsu odpowiedzialnego za adnotację @SamePassword:

1
2
3
4
5
6
7
8
@Target(TYPE)
@Retention(RUNTIME)
@Constraint(validatedBy = SamePasswordValidator.class)
public @interface SamePassword {
    String message() default "{pl.aetas.gamestore.validator.constraint.SamePassword}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Sama klasa walidatora wygląda tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class SamePasswordValidator implements ConstraintValidator<SamePassword, Object> {

    SamePassword constraintAnnotation;

    public boolean isValid(Object value, ConstraintValidatorContext context) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(constraintAnnotation.message()).addNode("confirmPassword").addConstraintViolation();
        CreateUserForm u = (CreateUserForm) value;
        if (u.getConfirmPassword().equals(u.getUser().getPassword())) {
            return true;
        }
        return false;
    }

    public void initialize(SamePassword constraintAnnotation) {
        this.constraintAnnotation = constraintAnnotation;
    }
}

Aby skorzystać z w/w rozwiązania wystarczy w klasie CreateUserForm dodać adnotację @SamePassword (na poziomie klasy) i .. tyle.

Mam nadzieję, że komuś się to przyda. W naszym projekcie adnotacje znacznie zwiększyły czytelność kodu, a jest to bardzo istotne przy pracy grupowej.