/* * JBoss, Home of Professional Open Source * Copyright 2008, Red Hat Middleware LLC, and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.seam.resteasy; import java.io.InputStream; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.annotation.Annotation; import java.net.URI; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.PathParam; import javax.ws.rs.Path; import javax.ws.rs.core.Context; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.MessageBodyReader; import org.jboss.resteasy.core.Headers; import org.jboss.resteasy.core.StringParameterInjector; import org.jboss.seam.Entity; import org.jboss.seam.annotations.Create; import org.jboss.seam.framework.Home; import static javax.ws.rs.core.Response.Status.UNSUPPORTED_MEDIA_TYPE; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; /** * This component exposes EntityHome and HibernateEntityHome components as a * REST resource. * * @author Jozef Hartinger * @param <T> Entity class * @param <T2> Entity id class */ // Empty @Path because it's ignored by second-stage bootstrap if not subclassed or in components.xml // but we need it as a marker so we'll find components.xml declarations during first stage of bootstrap. @Path("") public class ResourceHome<T, T2> extends AbstractResource<T> { private EntityHomeWrapper<T> entityHome = null; @Context private UriInfo uriInfo; @Context private Headers headers; @HeaderParam("Content-Type") private MediaType requestContentType; private Class entityIdClass = null; private boolean readonly; private static final PathParamAnnotation pathParamAnnotation = new PathParamAnnotation(); /** * Called at component instantiation. EntityHome component must be set in * order for component to be created. */ @Create public void create() { setEntityHome(getEntityHome()); if (entityHome == null) { throw new IllegalStateException("entityHome is not set"); } } /** * Called by RESTEasy when HTTP GET request is received. String form of * entity identifier is passed as a parameter. Returns a response containing * database entity. * * @param rawId String form of entity identifier * @return response * @see #getEntity */ @Path("/{id}") @GET public Response getResource(@PathParam("id") String rawId) { MediaType selectedMediaType = selectResponseMediaType(); if (selectedMediaType == null) { return Response.status(UNSUPPORTED_MEDIA_TYPE).build(); } T2 id = unmarshallId(rawId); T entity = getEntity(id); return Response.ok(new GenericEntity(entity, getEntityClass()) { }, selectedMediaType).build(); } /** * Retrieve an entity identified by id parameter. * * @param id entity identifier * @return entity database entity */ public T getEntity(T2 id) { entityHome.setId(id); return entityHome.find(); } /** * Called by RESTEasy when HTTP POST request is received. Persists received * entity and returns 201 HTTP status code with location header set to new * URI if operation succeeds. * * @param messageBody HTTP request body * @return response * @see #createEntity */ @POST public Response createResource(InputStream messageBody) { if (readonly) { return Response.status(405).build(); } // check if we accept this content type if (!isMediaTypeCompatible(requestContentType)) { return Response.status(UNSUPPORTED_MEDIA_TYPE).build(); } T entity = unmarshallEntity(messageBody); T2 id = createEntity(entity); URI uri = uriInfo.getAbsolutePathBuilder().path(id.toString()).build(); return Response.created(uri).build(); } /** * Store entity passed as a parameter in the database. * * @param entity Object to be persisted * @return id identifier assigned to the entity */ public T2 createEntity(T entity) { entityHome.setInstance(entity); entityHome.persist(); return (T2) entityHome.getId(); } /** * Called by RESTEasy when HTTP PUT request is received. Merges the state of * the database entity with the received representation. * * @param rawId String form of entity identifier * @param messageBody HTTP request body * @return response * @see #updateEntity */ @Path("/{id}") @PUT public Response updateResource(@PathParam("id") String rawId, InputStream messageBody) { if (readonly) { return Response.status(405).build(); } // check if we accept this content type if (!isMediaTypeCompatible(requestContentType)) { return Response.status(UNSUPPORTED_MEDIA_TYPE).build(); } T entity = unmarshallEntity(messageBody); T2 id = unmarshallId(rawId); // check representation id - we don't allow renaming Object storedId = Entity.forBean(entity).getIdentifier(entity); if (!id.equals(storedId)) { return Response.status(BAD_REQUEST).build(); } updateEntity(entity, id); return Response.noContent().build(); } /** * Merge the state of the database entity with the entity passed as a * parameter. Override to customize the update strategy - for instance to * update specific fields only instead of a full merge. * * @param entity */ public void updateEntity(T entity, T2 id) { entityHome.merge(entity); } /** * Called by RESTEasy when HTTP DELETE request is received. Deletes a * database entity. * * @param rawId String form of entity identifier * @return response * @see #deleteEntity */ @Path("/{id}") @DELETE public Response deleteResource(@PathParam("id") String rawId) { if (readonly) { return Response.status(405).build(); } T2 id = unmarshallId(rawId); deleteEntity(id); return Response.noContent().build(); } /** * Delete database entity. * * @param id entity identifier */ public void deleteEntity(T2 id) { getEntity(id); entityHome.remove(); } /** * Convert HTTP request body into entity class instance. * * @param is HTTP request body * @return entity */ private T unmarshallEntity(InputStream is) { Class<T> entityClass = getEntityClass(); MessageBodyReader<T> reader = SeamResteasyProviderFactory.getInstance().getMessageBodyReader(entityClass, entityClass, entityClass.getAnnotations(), requestContentType); if (reader == null) { throw new RuntimeException("Unable to find MessageBodyReader for content type " + requestContentType); } T entity; try { entity = reader.readFrom(entityClass, entityClass, entityClass.getAnnotations(), requestContentType, headers, is); } catch (Exception e) { throw new RuntimeException("Unable to unmarshall request body"); } return entity; } /** * Converts String form of entity identifier to it's natural type. * * @param id String form of entity identifier * @return entity identifier */ private T2 unmarshallId(String id) { StringParameterInjector injector = new StringParameterInjector(getEntityIdClass(), getEntityIdClass(), "id", PathParam.class, null, null, new Annotation[] {pathParamAnnotation}, SeamResteasyProviderFactory.getInstance()); return (T2) injector.extractValue(id); } /** * EntityHome component getter. Override this method to set the EntityHome * this resource will operate on. You can use either EntityHome or * HibernateEntityHome instance. * * @return entity home */ public Home<?, T> getEntityHome() { return (entityHome == null) ? null : entityHome.unwrap(); } /** * EntityHome component setter * * @param entityHome */ public void setEntityHome(Home<?, T> entityHome) { this.entityHome = new EntityHomeWrapper<T>(entityHome); } @Override public Class<T> getEntityClass() { return entityHome.getEntityClass(); } public boolean isReadonly() { return readonly; } /** * If set to read-only mode, this resource will only response to GET * requests. HTTP 415 status code (method not allowed) will returned in all * other cases. * * @param readonly */ public void setReadonly(boolean readonly) { this.readonly = readonly; } /** * Retrieve entity identifier's class. If not set, type parameters of a * superclass are examined. * * @return class of entity identifier */ public Class getEntityIdClass() { if (entityIdClass == null) { Type superclass = this.getClass().getGenericSuperclass(); if (superclass instanceof ParameterizedType) { ParameterizedType parameterizedSuperclass = (ParameterizedType) superclass; if (parameterizedSuperclass.getActualTypeArguments().length == 2) { return (Class) parameterizedSuperclass.getActualTypeArguments()[1]; } } throw new RuntimeException("Unable to determine entity id class."); } else { return entityIdClass; } } public void setEntityIdClass(Class entityIdClass) { this.entityIdClass = entityIdClass; } /** * Annotation implementation (@PathParam("id")) for providing RESTEasy with metadata. */ static class PathParamAnnotation implements PathParam { public String value() { return "id"; } public Class<? extends Annotation> annotationType() { return PathParam.class; } } }