Conversion service

Convertisseurs globaux dans Spring

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Dans l'article consacré à des formulaires dans Spring, on a abordé la question de conversion des types de données (property editors). Cependant, ce n'est pas la seule fonctionnalité du Spring qui peut se charger de convertir un objet d'un type à un autre. Le concept qui aide à supporter cela s'appelle conversion service.

Qu'est-ce que c'est un conversion service dans Spring ?

La version 3 du Spring a amené une nouveauté cachée dans le package org.springframework.core.convert.converter. Il s'agit d'un Service Provider Interface (SPI), c'est-à-dire d'une spécification visant à définir les règles de son implémentation.

Conversion service est basée sur l'implémentation de l'interface Converter <S, T> où S signifie la classe d'origine et T la classe en laquelle l'objet devra être converti. La méthode convert() de cette interface prend comme objet le fichier source et le transforme en celui souhaité. Regardons cela sur l'exemple d'un convertisseur qui traduit l'instance org.springframework.security.core.userdetails.User en Subscriber :

public class UserToSubscriberConverter implements Converter<User, Subscriber> {
    @Autowired
    private SubscriberService subscriberService;

    public Subscriber convert(User user) {
        return subscriberService.loadByUsername(user.getUsername());
    }
}

L'exemple est banal mais il illustre bien la logique d'implémentation de l'interface Converter. Dans ce cas, la méthode convert() retourne l'instance Subscriber à partir de l'instance User. La conversion s'effectue via la recherche de l'utilisateur dans une des méthodes du subscriberService.

Quand utiliser un conversion service ?

L'illustration de conversion présentée dans le paragraphe précédent rend bien l'utilité des convertisseurs. On stocke dans la session l'instance de la classe User tandis que souvent dans le cas, par exemple pour modifier les paramètres d'un compte, on veut utiliser l'instance Subscriber. Grâce à cette dernière on peut faire appel à des méthodes des repositories (subscriber.save()) pour mettre à jour les informations nécessaires. Le code ci-dessous affiche cette utilité :

// SubscriberController.java
@Controller
public class SubscriberController extends FrontendController {
    public String modifyAvatarHandle(@ModelAttribute("subscriber") @Validated({SubscriberAvatarCheck.class}) 
    Subscriber subscriber, BindingResult binRes, @LoggedUser AuthenticationFrontendUserDetails user, 
    Model layout, RedirectAttributes redAtt) {
        logger.info("Received POST request " + subscriber);
        if (binRes.hasErrors()) {
            redAtt.addFlashAttribute("error", true);
            redAtt.addFlashAttribute("subscriber", subscriber);
            redAtt.addFlashAttribute("errors", binRes);
        } else {
            try {
                Subscriber subFromUser = conversionService.convert(user, Subscriber.class);
                subFromUser.setAvatarFile(subscriber.getAvatarFile());
                subscriberService.addAvatar(subFromUser);
                redAtt.addFlashAttribute("success", true);
            } catch (Exception e) {
                binRes.addError(getExceptionError("subscriber"));
                redAtt.addFlashAttribute("error", true);
                redAtt.addFlashAttribute("subscriber", subscriber);
                redAtt.addFlashAttribute("errors", binRes);
            }
        }
        return "redirect:/account/avatar";
    }
    // ...
}

// SubscriberServiceImpl.java
@Service("subscriberService")
public class SubscriberServiceImpl implements SubscriberService {
    @Override
    public Subscriber addAvatar(Subscriber subscriber) throws Exception {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);
        try {
            Map uploadResult = imageTool.uploadFile("avatar", subscriber.getAvatarFile(), subscriber.getLogin());
            if (((Boolean)uploadResult.get("uploadResult")) == true) {
                subscriber.setAvatar((String)uploadResult.get("fileBasename"));
                subscriber = subscriberRepository.save(subscriber);
            
                transactionManager.commit(status);
            } else {
                subscriber = null;
            }
        } catch(Exception e) {
            logger.error("An error occured on adding subscriber avatar", e);
            transactionManager.rollback(status);
            subscriber = null;
            throw new Exception(e);
        }
        return subscriber;
    }
    // ...
}

Comment implémenter conversion service sous Spring ?

Conversion service est un objet sans propriétaire, initialisé au moment de démarrage de l'application. Ensuite il est partagé par tous les threads. Pour l'enregistrer dans le fichier de contexte, il faut le déterminer avec une liste des convertisseurs qu'il supporte :

    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean id="userToSubscriberConv" class="library.converter.UserToSubscriberConverter"/>
                <bean id="userToAdminConv" class="library.converter.UserToAdminConverter"/>
                <bean id="bookToStringConv" class="library.converter.BookToStringConverter"/>
                <bean id="stringToLangConv" class="library.converter.StringToLangConverter"/>
                <bean id="stringToQuestionConv" class="library.converter.StringToQuestionConverter"/>
                <bean id="longToLangConv" class="library.converter.LongToLangConverter"/>
                <bean id="writerLangFormToListConv" class="library.converter.WriterLangFormToListConverter"/>
                <bean id="stringToPaymentMethodConv" class="library.converter.StringToPaymentMethodConverter"/>
                <bean id="stringToPenaltyConv" class="library.converter.StringToPenaltyConverter"/>
                <bean id="stringToSubscriberConv" class="library.converter.StringToSubscriberConverter"/>
                <bean id="stringToSubscriptionConv" class="library.converter.StringToSubscriptionConverter"/>
                <bean id="setToBeanPropertyBindingResultConv" class="library.converter.SetToBeanPropertyBindingResultConverter"/>
                <bean id="authenticationFrontendUserDetailsToUsernamePasswordAuthenticationTokenConv" class="library.converter.AuthenticationFrontendUserDetailsToUsernamePasswordAuthenticationTokenConverter"/>
            </set>
        </property>
    </bean>

La définition est donc simple. On précise un bean< contenant la classe FormattingConversionServiceFactoryBean. Elle permet de spécifier non seulement un Set des convertisseurs mais aussi un Set des formateurs.

En plus de cela, on rajoute <mvc:annotation-driven conversion-service="conversionService"> dans le fichier de contexte. Grâce à cela, toutes les conversions seront gérées automatiquement dans le contrôleur pendant la transmission des données.

L'utilisation d'un conversion service se fait alors via l'injection du bean dans un contrôleur, comme ici :

@Controller
public class SubscriberController extends FrontendController {
    @Autowired
    private ConversionService conversionService;
    // ...
}

Une question ? Une remarque ?

*

*

Un conseil PHP

Comment créer une signature pour OAuth

La création d'une signature (paramètre oauth_signature) pour la requête OAuth est réalisée avec l'algorithme HMAC-SHA1. Son implémentation chez PHP se présente ainsi :

hash_hmac('sha1', "Message to hashed", "Secret key", true)
Le résultat de cet algorithme doit ensuite être encodé avec Base64. Un exemple d'utilisation chez le protocle OAuth peut se présenter de la manière suivante :
public function getSignature($baseString, $signatureKey)
{
  return base64_encode(hash_hmac('sha1', $baseString, $signatureKey, true));   
}