Création d’un projet Spring + hibernate + web service REST + Spring security. 4 – Création d’un Web Service
Dans les trois premiers billets de cette série, on a vu comment créer le projet et ses objets métiers, créer les DAO et enfin, comment créer les services.
On va maintenant voir comment on peut créer un web service REST, avec Restlet à partir du projet.
La première configuration à faire se fait au niveau du web.xml de l’application web. Il ressemble maintenant à ceci :
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Rest WS --> <servlet> <servlet-name>rest</servlet-name> <servlet-class> com.noelios.restlet.ext.spring.RestletFrameworkServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>rest</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping> </web-app>
On peut voir qu’on déclare une servlet qui va intercepter les urls commençant par « rest ».
Le format des ressources pour le Web Service sera le XML. J’ai donc décidé d’utiliser une librairie qui permet de sérialiser simplement les objets Java : XStream. Le principe sera simple : le client demande une ressource, on la charge côté serveur et on la sérialise, puis on l’envoie.
Pour cela, j’ai créé une classe permettant la sérialisation des objets :
public class XMLUtils { public static Document convertToXml(Object object) { Document document = createDocument(); try { HibernateProxyCleaner.cleanObject(object, null); } catch (Exception e) { e.printStackTrace(); } XStream xstream = new XStream(new DomDriver()); xstream.setMode(XStream.ID_REFERENCES); xstream.autodetectAnnotations(true); xstream.alias("note", Note.class); xstream.alias("user", User.class); xstream.alias("file", File.class); xstream.alias("file_type", FileType.class); try { xstream.marshal(object, new DomWriter(document)); } catch (Exception e) { e.printStackTrace(); } return document; } public static Document createDocument() { Document document; try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); document = builder.newDocument(); } catch (Exception ex) { ex.printStackTrace(); return null; } return document; } }
On voit que d’abord, la méthode convertToXml fait appel à la classe HibernateProxyCleaner. Cette classe (qui n’est pas de moi, voir ici, et que j’ai modifiée un peu pour mes besoins) permet d’enlever tous les proxies hibernate, qui pourraient lancer des exceptions lors de la sérialisation (XStream essaie d’accéder aux proxies = exception). On indique ensuite à XStream quelques alias (si on ne le fait pas, XStream utilise le nom complet des classes comme nom de node XML), et enfin, on marshalle l’objet.
Voici la classe HibernateProxyCleaner :
package fr.mael.jfreenote.transverse.util; /** * Copyright 2008, Maher Kilani * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.hibernate.proxy.HibernateProxy; import fr.mael.jfreenote.transverse.om.base.OMBase; /** * This class is implemented to solve the hibernate lazy loading problem with * flex which doesnt support lazy loading in the meantime. * * @author Maher Kilani * */ public class HibernateProxyCleaner { /** * This function would take as a prameter any kind of object and recursively * access all of its member and clean it from any uninitialized variables. * The function will stop the recursion if the member variable is not of * type baseBean (defined in the application) and if not of type collection * * @param listObj * @throws ClassNotFoundException * @throws IllegalAccessException * @throws IllegalArgumentException * @throws InvocationTargetException * @throws InstantiationException */ @SuppressWarnings("unchecked") public static void cleanObject(Object listObj, HashSet visitedBeansSet) throws IllegalArgumentException, IllegalAccessException, ClassNotFoundException, InstantiationException, InvocationTargetException { if (visitedBeansSet == null) visitedBeansSet = new HashSet(); if (listObj == null) return; // to handle the case of abnormal return consisting of array Object // case if hybrid bean if (listObj instanceof Object[]) { Object[] objArray = (Object[]) listObj; for (int z = 0; z < objArray.length; z++) { cleanObject(objArray[z], visitedBeansSet); } } else { Iterator itOn = null; if (listObj instanceof List) { itOn = ((List) listObj).iterator(); } else if (listObj instanceof Set) { itOn = ((Set) listObj).iterator(); } else if (listObj instanceof Map) { itOn = ((Map) listObj).values().iterator(); } if (itOn != null) { while (itOn.hasNext()) { cleanObject(itOn.next(), visitedBeansSet); } } else { if (!visitedBeansSet.contains(listObj)) { visitedBeansSet.add(listObj); processBean(listObj, visitedBeansSet); } } } } /** * Remove the un-initialized proxies from the given object * * @param objBean * @throws Exception * @throws IllegalAccessException * @throws ClassNotFoundException * @throws IllegalArgumentException * @throws InvocationTargetException * @throws InstantiationException */ @SuppressWarnings("unchecked") private static void processBean(Object objBean, HashSet visitedBeans) throws IllegalAccessException, IllegalArgumentException, ClassNotFoundException, InstantiationException, InvocationTargetException { Class tmpClass = objBean.getClass(); Field[] classFields = null; while (tmpClass != null && tmpClass != OMBase.class && tmpClass != Object.class) { classFields = tmpClass.getDeclaredFields(); cleanFields(objBean, classFields, visitedBeans); tmpClass = tmpClass.getSuperclass(); } } @SuppressWarnings("unchecked") private static void cleanFields(Object objBean, Field[] classFields, HashSet visitedBeans) throws ClassNotFoundException, IllegalArgumentException, IllegalAccessException, InstantiationException, InvocationTargetException { boolean accessModifierFlag = false; for (int z = 0; z < classFields.length; z++) { Field field = classFields[z]; accessModifierFlag = false; if (!field.isAccessible()) { field.setAccessible(true); accessModifierFlag = true; } Object fieldValue = field.get(objBean); if (fieldValue instanceof HibernateProxy) { String className = ((HibernateProxy) fieldValue).getHibernateLazyInitializer().getEntityName(); Class clazz = Class.forName(className); Class[] constArgs = {Integer.class}; Constructor construct = null; OMBase baseBean = null; try { construct = clazz.getConstructor(constArgs); } catch (NoSuchMethodException e) { // LOG.debug("No such method for base bean " + className); } if (construct != null) { baseBean = (OMBase) construct.newInstance((Integer) ((HibernateProxy) fieldValue).getHibernateLazyInitializer().getIdentifier()); } field.set(objBean, baseBean); } else { if (fieldValue instanceof org.hibernate.collection.PersistentCollection) { // checking if it is a set, list, or bag (simply if it is a // collection) if (!((org.hibernate.collection.PersistentCollection) fieldValue).wasInitialized()) field.set(objBean, null); else { cleanObject((fieldValue), visitedBeans); } } else { if (fieldValue instanceof OMBase || fieldValue instanceof Collection) cleanObject(fieldValue, visitedBeans); } } if (accessModifierFlag) field.setAccessible(false); } } }
Voilà pour la sérialisation. On va maintenant déclarer la classe qui va s’occuper de recevoir les requêtes de l’utilisateur.
Voici cette classe :
public class NoteWs extends Resource { private NoteService noteService; private Note note; private String noteId; @Override public boolean allowDelete() { return true; } @Override public boolean allowGet() { return true; } @Override public boolean allowPost() { return true; } @Override public boolean allowPut() { return true; } public NoteWs() { this.getVariants().add(new Variant(MediaType.TEXT_XML)); } private void parseRequest() { noteId = (String) getRequest().getAttributes().get("id"); if (noteId != null) { try { note = noteService.load(Integer.valueOf(noteId)); } catch (Exception e) { e.printStackTrace(); getResponse().setStatus(Status.SUCCESS_OK); } } } public Representation represent(Variant variant) { parseRequest(); Representation resource = null; if (note == null) { try { if (noteId == null) { return representError("Provide an ID !"); } else { return representError("Note not found !"); } } catch (ResourceException e) { e.printStackTrace(); } } else { try { resource = new DomRepresentation(MediaType.TEXT_XML); // User user = (User) // SecurityContextHolder.getContext().getAuthentication().getPrincipal(); ((DomRepresentation) resource).setDocument(XMLUtils.marshal(note)); } catch (Exception e) { e.printStackTrace(); } } return resource; } @Override public void acceptRepresentation(Representation entity) throws ResourceException { parseRequest(); Form form = new Form(entity); String content = form.getFirstValue("content"); try { note.setContent(content); noteService.save(note); getResponse().setStatus(Status.SUCCESS_OK); } catch (Exception e) { e.printStackTrace(); getResponse().setStatus(Status.SERVER_ERROR_INTERNAL); } } @Override public void storeRepresentation(Representation entity) throws ResourceException { parseRequest(); Form form = new Form(entity); String content = form.getFirstValue("content"); try { Note note = new Note(); note.setContent(content); //note.setUser(SecurityUtils.getConnectedUser()); noteService.save(note); getResponse().setStatus(Status.SUCCESS_OK); } catch (Exception e) { e.printStackTrace(); getResponse().setStatus(Status.SERVER_ERROR_INTERNAL); } } @Override public void removeRepresentations() throws ResourceException { parseRequest(); if (note == null) { if (noteId == null) { getResponse().setEntity(representError("Provide an ID !")); } else { getResponse().setEntity(representError("Note note found")); } getResponse().setStatus(Status.CLIENT_ERROR_BAD_REQUEST); return; } else { try { noteService.delete(note); getResponse().setStatus(Status.SUCCESS_OK); } catch (Exception e) { getResponse().setStatus(Status.SERVER_ERROR_INTERNAL); e.printStackTrace(); } } } private Representation representError(String msg) throws ResourceException { Representation result = null; try { result = new DomRepresentation(MediaType.TEXT_XML); Document doc = ((DomRepresentation) result).getDocument(); Element root = doc.createElement("error"); root.setTextContent(msg); } catch (IOException e) { e.printStackTrace(); } return result; } public void setNoteService(NoteService noteService) { this.noteService = noteService; } }
Cette classe est un peu longue, mais je vais l’expliquer.
Déjà, elle hérite de Resource, ce qui permet à restlet de la reconnaitre comme une classe de Web Service.
On déclare tout d’abord 4 méthodes :
@Override public boolean allowDelete() { return true; } @Override public boolean allowGet() { return true; } @Override public boolean allowPost() { return true; } @Override public boolean allowPut() { return true; }
Ces méthodes permettent d’indiquer à Restlet quelles méthodes le web service accepte (GET, DELETE, POST, PUT etc.).
On déclare ensuite une méthode « parseRequest ». Elle va permettre de récupérer la note à partir de l’identifiant qui est (éventuellement) passé en paramètre dans la requête. Elle charge la note à partir du ServiceNote qui est injecté via spring (on le verra plus tard).
On déclare ensuite 4 méthodes, chacune correspondant à une méthode que le web service accepte :
- represent(Variant) : c’est cette méthode qui est appelée lors d’un appel GET. Ici, elle permet d’afficher une note à l’utilisateur. Elle appelle tout d’abord la méthode parseRequest(), pour récupérer la note que l’utilisateur veut consulter. Puis ensuite, elle crée une « Representation » de type Document dom qu’elle renvoie par le web service. Le Document est crée grâce à XStream.
- acceptRepresentation(Representation) : c’est cette méthode qui est appelée lors d’un appel POST. Ici, elle permet de mettre à jour une note. Elle fait également appel à parseRequest(), puis récupère la valeur du paramètre « content » (paramètre du POST), elle met ensuite à jour la note avec sa nouvelle valeur pour « content ».
- storeRepresentation(Representation) : c’est cette méthode qui est appelée lors d’un appel PUT. Ici, elle permet d’ajouter une note. Le fonctionnement est similaire à la méthode précédente. Attention, cette méthode ne fonctionnera pas tant qu’on n’aura pas mis en place la sécurité (prochain billet), car une note doit contenir un utilisateur, ici l’utilisateur en session, qu’on ne peut pas récupérer tant que la sécurité n’a pas été activée.
- removeRepresentations() : c’est cette méthoe qui est appelée lors d’un appel DELETE. Ici, elle permet de supprimer une note. La méthode fait juste appel à la méthode delete du NoteService
Voilà pour les explications sur cette classe. Le fonctionnement est somme toute très simple.
Voyons maintenant la configuration de Spring.
La configuration doit être indiquée dans un fichier nommé « rest-servlet.xml », situé dans le dossier WEB-INF de l’application web. Il y a peut-être un moyen de l’appeler autrement et le mettre à un autre endroit, mais je n’ai pas trouvé comment.
Voici le contenu du fichier :
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <bean id="root" class="org.restlet.ext.spring.SpringRouter"> <property name="attachments"> <map> <entry key="/note/{id}"> <bean class="org.restlet.ext.spring.SpringFinder"> <lookup-method name="createResource" bean="noteWs" /> </bean> </entry> <entry key="/note"> <bean class="org.restlet.ext.spring.SpringFinder"> <lookup-method name="createResource" bean="noteWs" /> </bean> </entry> </map> </property> </bean> <bean id="noteWs" class="fr.mael.jfreenote.ws.impl.NoteWs" scope="prototype" > <property name="noteService" ref="noteService"/> </bean> </beans>
On déclare donc tout d’abord un bean qui construit une instance de la classe précédemment créée : noteWs. On lui injecte noteService pour faire les requêtes.
Puis on crée un bean nommé root. A ce bean, on indique quelles urls doivent être « écoutées », et quel bean est responsable d’écouter ces urls. L’url est indiquée dans l’attribut « key ». Ici, on indique donc que le bean noteWs va être responsable des urls de la forme « /note/{id} » (par exemple : http://localhost:8080/JFreeNote/rest/note/1) et de la forme « /note » (par exemple : http://localhost:8080/JFreeNote/rest/note).
On doit enfin compléter le fichier applicationContext.xml afin de lui indiquer le fichier rest-servlet.xml :
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- Base de donnée --> <import resource="applicationContext-db.xml" /> <!-- DAO --> <import resource="applicationContext-dao.xml" /> <!--Services --> <import resource="applicationContext-service.xml" /> <import resource="rest-servlet.xml" /> </beans>
Et voilà pour ce qui est de la configuration du web service.
Prochain et dernier chapitre : la sécurisation du web service.