package org.jpwh.web.jsf; import org.jpwh.web.dao.ImageDAO; import org.jpwh.web.dao.ItemDAO; import org.jpwh.web.dao.UserDAO; import org.jpwh.web.model.Image; import org.jpwh.web.model.ImageLookup; import org.jpwh.web.model.Item; import javax.enterprise.context.Conversation; import javax.enterprise.context.ConversationScoped; import javax.enterprise.event.Observes; import javax.inject.Inject; import javax.inject.Named; import javax.persistence.EntityNotFoundException; import javax.servlet.http.Part; import javax.transaction.Transactional; import java.io.Serializable; import java.math.BigDecimal; import static javax.enterprise.event.Reception.IF_EXISTS; @Named /* The service instance is conversation scoped. By default, however, the conversation context is transient, and therefore behaves as a request-scoped service. */ @ConversationScoped /* The class must be Serializable, unlike a request-scope implementation. An instance of EditItemService might be stored in the HTTP session, and that session data might be serialized to disk or sent across the network in a cluster. We took the easy way out in the previous chapter using a stateful EJB, saying: "it's not passivation capable". Anything in the CDI conversation scope however must be passivation-capable and therefore serializable. */ public class EditItemService implements Serializable { /* The injected DAO instances have dependent scope and are serializable. You might think they are not, because they have an EntityManager field, which is not serializable. We'll talk about this mismatch in a second. */ @Inject ItemDAO itemDAO; @Inject ImageDAO imageDAO; @Inject UserDAO userDAO; /* The Conversation API provided by the container, call it to control the conversation context. You'll need it when the user clicks on the Next button for the first time, promoting the transient conversation to long-running. */ @Inject Conversation conversation; /* This is the state of the service: the item you are editing on the pages of the wizard. You start with a fresh Item entity instance in transient state. If this service is initialized with an item identifier value, load the Item in setItemId(). */ Long itemId; Item item = new Item(); /* This is some transient state of the service; we only need it temporarily when the user clicks the Upload button on the "Edit Images" page. The Part class of the Servlet API is not serializable. It's not uncommon to have some transient state in a conversational service, but you must initialize it for every request when it's needed. */ transient Part imageUploadPart; /* The setItemId method will only be called if the request contains an item identifier value. You therefore have two entry points into this conversation: With or without an existing item's identifier value. */ public void setItemId(Long itemId) { this.itemId = itemId; if (item.getId() == null && itemId != null) { /* If you are editing an item, you must load it from the database. You are still relying on a request-scoped persistence context, so as soon as the request is complete, this Item instance is in detached state. You can hold detached entity instances in a conversational service's state and merge it when needed to persist changes. */ item = itemDAO.findById(itemId); if (item == null) throw new EntityNotFoundException(); } } public Conversation getConversation() { return conversation; } public Long getItemId() { return itemId; } public Item getItem() { return item; } public Part getImageUploadPart() { return imageUploadPart; } public void setImageUploadPart(Part imageUploadPart) { this.imageUploadPart = imageUploadPart; } public String editImages() { if (conversation.isTransient()) { conversation.setTimeout(10 * 60 * 1000); // 10 minutes conversation.begin(); } return "editItemImages"; } public void uploadImage() throws Exception { if (imageUploadPart == null) return; /* Create the Image entity instance from the submitted multi-part form. */ Image image = imageDAO.hydrateImage(imageUploadPart.getInputStream()); image.setName(imageUploadPart.getSubmittedFileName()); image.setContentType(imageUploadPart.getContentType()); /* You must add the transient Image to the transient or detached Item. This conversation will consume more and more memory on the server, as uploaded image data is added to conversational state and therefore the user's session. */ image.setItem(item); item.getImages().add(image); } /* The system transaction interceptor wraps the method call. */ @Transactional public String submitItem() { /* You must join the unsynchronized request-scoped persistence context with the system transaction if you want to store data. */ itemDAO.joinTransaction(); item.setSeller(userDAO.findById(1l)); /* This DAO call will make the transient or detached Item persistent, and because you enabled it with a cascading rule on the @OneToMany, also store any new transient or old detached Item#images collection elements. According to the DAO contract, you must take the returned instance as the "current" state. */ item = itemDAO.makePersistent(item); /* You manually end the long-running conversation. This is effectively a demotion: The long-running conversation becomes transient; you destroy the conversation context and this service instance when the request is complete. All conversational state is removed from the user's session. */ if (!conversation.isTransient()) conversation.end(); /* This is a redirect-after-POST in JSF to the auction item details page, with the new identifier value of the now persistent Item. */ return "auction?id=" + item.getId() + "&faces-redirect=true"; } public String cancel() { if (!conversation.isTransient()) conversation.end(); return "catalog?faces-redirect=true"; } // A nice trick with CDI: This bean will answer if the lookup event is fired by // someone, but it won't be created if it doesn't exist already. public void getConversationalImage(@Observes(notifyObserver = IF_EXISTS) ImageLookup imageLookup) { // We might have transient images without identifier value, so we assume // the lookup identifier is actually the index of an image in our list of images Image image = getItem().getImagesSorted().get( new BigDecimal(imageLookup.getId()).intValueExact() ); if (image != null) imageLookup.setImage(image); } }