Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 58 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
58
Dung lượng
602,09 KB
Nội dung
508 CHAPTER 16 ■ LEVERAGING SPRING } def notSubscribed = { attrs, body -> if(!checkSubscribed(request.user, attrs.artist)) { out << body() } } Of course, testing is crucial too. You could test the tag library using the GroovyPagesTestCase class you used to test the AlbumArtTagLib. However, since the SubscriptionTagLib is mainly about branching logic and not markup rendering, it is probably easier to take advantage of the grails.test.TagLibUnitTestCase class that lets you unit test tag libraries but not the markup they generate. Simply create a new a unit test in the test/unit/com/g2one/gtunes directory called SubscriptionTagLibTests that extends from the TagLibUnitTestCase class, as shown in Listing 16-29. Listing 16-29. Using the TagLibUnitTestCase Class package com.g2one.gtunes class SubscriptionTagLibTests extends grails.test.TagLibUnitTestCase { } You can then write a couple of simple tests that check the behavior of the <gtunes: isSubscribed> and <gtunes:notSubscribed> tags. Listing 16-30 shows two tests called testIsSubscribed and testNotSubscribed. Listing 16-30. Testing the SubscriptionTagLib Class void testIsSubscribed() { mockDomain(ArtistSubscription) def artist = new Artist(name:"Kings of Leon") def user = new User(login:"testuser") new ArtistSubscription(artist:artist, user:user).save() tagLib.request.user = user tagLib.isSubscribed(artist:artist) { "subscribed" } tagLib.notSubscribed(artist:artist) { "notsubscribed" } assertEquals "subscribed", tagLib.out.toString() } void testNotSubscribed() { CHAPTER 16 ■ LEVERAGING SPRING 509 mockDomain(ArtistSubscription) def artist = new Artist(name:"Kings of Leon") def user = new User(login:"testuser") tagLib.request.user = user tagLib.isSubscribed(artist:artist) { "subscribed" } tagLib.notSubscribed(artist:artist) { "notsubscribed" } assertEquals "notsubscribed", tagLib.out.toString() } A closure can be passed as the body of the tag, as long as it returns a String representing the body contents. In Listing 16-30, either “subscribed” or “notsubscribed” will be written to the mock out variable. OK, with the tests out of the way, the next thing to do is to modify the grails-app/views/artist/_artist.gsp template to include the new _subscribe.gsp template. Listing 16-31 shows the necessary code changes highlighted in bold. Listing 16-31. Updates to the _artist.gsp Template <div id="artist${artist.id}" class="artistProfile" style="display:none;"> <div class="artistDetails"> <g:render template="subscribe" model="[artist:artist]"></g:render> </div> </div> Now when you visit one of the artist pages, you’ll see a new “Subscribe” link, as shown in Figure 16-6. Figure 16-6. The “Subscribe” link 510 CHAPTER 16 ■ LEVERAGING SPRING Unfortunately, when you click the link, you’ll receive a “Page not found” 404 error. To resolve this issue, you need to implement the server logic for the subscribe and unsubscribe actions that the <g:remoteLink> tags in Listing 16-25 refer to. Open the ArtistController class, and add a new action called subscribe that persists a new ArtistSubscription if one doesn’t already exist. Listing 16-32 shows an example implementation. Listing 16-32. Implementing the subscribe Action def subscribe = { def artist = Artist.get(params.id) def user = request.user if(artist && user) { def subscription = ArtistSubscription.findByUserAndArtist(user, artist) if(!subscription) { new ArtistSubscription(artist:artist, user:user).save(flush:true) } render(template:"/artist/subscribe", model:[artist:artist]) } } As you can see from the code in Listing 16-32, the subscribe action reuses the _subscribe.gsp template to render an Ajax response to the client. The logic in the SubscriptionTagLib deals with the rest. To add the unsubscribe logic, you simply need to delete the ArtistSubscription instance if it exists, as shown in Listing 16-33. Listing 16-33. Implementing the unsubscribe Action def unsubscribe = { def artist = Artist.get(params.id) def user = request.user if(artist && user) { def subscription = ArtistSubscription.findByUserAndArtist(user, artist) if(subscription) { subscription.delete(flush:true) } render(template:"/artist/subscribe", model:[artist:artist]) } } Finally, you need to add a couple of URL mappings in order to expose the subscribe and unsubscribe actions, as shown in Listing 16-34. Listing 16-34. The Subscriptions URL Mappings "/artist/subscribe/$id"(controller:"artist", action:"subscribe") "/artist/unsubscribe/$id"(controller:"artist", action:"unsubscribe") CHAPTER 16 ■ LEVERAGING SPRING 511 Implementing Asynchronous E-mail Notifications Now with users able to subscribe to their favorite artists, it is time to consider the onNewAlbum method of the StoreService class again. Whenever a JMS message is received, you’re going to need to find all the subscribers for the Artist associated with the passed Album and send an e-mail to each one. To do this, you first need a reference to the mailService bean, provided by the Mail plugin installed in Chapter 12, which can be obtained by defining a property of the same name: def mailService Next, you need to obtain a list of all the User instances subscribed to the Artist associated with the Album. To do this, you can get a reference to the Artist via the artist property: def artist = album.artist Then use a criteria query to obtain a list of users: def users = ArtistSubscription.withCriteria { projections { property "user" } eq('artist', artist) } Notice the use of the projections block to specify that you want the result to contain the user property of each ArtistSubscription found. Once you have a list of users, you can now use the mailService to send an e-mail to each one: for(user in users) { mailService.sendMail { from "notifications@gtunes.com" to user.email title "${artist.name} has released a new album: ${album.title}!" body view:"/emails/artistSubscription", model:[album:album, artist:artist, user:user] } } As you can see, the body method is used to specify that the e-mail is to be rendered by a view called /emails/artistSubscription. We’ll return to this view in a moment. For complete- ness, Listing 16-35 contains the full code listing for the onNewAlbum(Album) method. Listing 16-35. The onNewAlbum(Album) Method void onNewAlbum(Album album) { try { def artist = album.artist 512 CHAPTER 16 ■ LEVERAGING SPRING def users = ArtistSubscription.withCriteria { projections { property "user" } eq('artist', artist) } for(user in users) { mailService.sendMail { from "notifications@gtunes.com" to user.email title "${artist.name} has released a new album: ${album.title}!" body view:"/emails/artistSubscription", model:[album:album, artist:artist, user:user] } } } catch(Exception e) { log.error "Error sending album $album notification message: $e.message", e throw e } } One addition that we didn’t cover previously is the surrounding try/catch block in Listing 16-35. An exception could occur if there was an error sending a mail or communicating with the database. Notice how the exception is logged and rethrown within the catch block. So, why rethrow the exception? Essentially, the StockService is a transactional service class. It is using Grails’ transactionManager underneath the surface. If you recall, the jmsContainer bean was given a reference to the Grails transactionManager in Listing 16-21. As a reminder, here is the relevant snippet from grails-app/conf/spring/resources.groovy: jmsContainer(org.springframework.jms.listener.DefaultMessageListenerContainer) { transactionManager = ref("transactionManager") autoStartup = false } If an exception is thrown, Grails will automatically roll back the transaction. Since the jmsContainer has a reference to the transactionManager, it will be made aware that the trans- action was rolled back. The result is that the JMS transaction will be rolled back, effectively marking the message as undelivered. ActiveMQ will then try to deliver the message again later. Thanks to Spring’s transaction abstraction layer, you get a reliable messaging system, with guarantees of message redelivery. CHAPTER 16 ■ LEVERAGING SPRING 513 The last thing to do is to finish up the subscription implementation by providing the view that renders the e-mail. Listing 16-36 shows the grails-app/views/emails/artistSubscription. gsp view. Listing 16-36. The artistSubscription View <%@ page contentType="text/plain"%> Dear ${user.firstName} ${user.lastName}, One of your favorite artists ${artist.name} has released a new album called ${album.title}! It is available now on gTunes at <g:createLink controller="album" action="display" id="${album.id}" absolute="true" /> Kind Regards, The gTunes Team Mixing Groovy and Java with Spring Although Grails already takes advantage of Groovy’s joint compiler, allowing you to integrate Java code seamlessly into a Grails application, it is often nice to provide this integration via Spring. As an example, currently the gTunes application is using some Groovy code to stream music to the user. You can find the relevant code in the stream action of the SongController, which is shown in Listing 16-37. Listing 16-37. The Stream action of the SongController Class def file = new File(song.file) try { def type = file.name[-3 1] response.contentType = "audio/x-${type}" def out = response.outputStream def bytes = new byte[BUFFER_SIZE] file.withInputStream { inp -> while( inp.read(bytes) != -1) { out.write(bytes) out.flush() } } } 514 CHAPTER 16 ■ LEVERAGING SPRING catch(Exception e) { log.error "Error streaming song $file: $e.message", e response.sendError 500 } Performance-wise, Java undoubtedly has the edge on Groovy when writing low-level IO code like that in Listing 16-37. You may want to optimize the stream action of the SongController to use a Java class instead. To do so, create a new Java class called StreamingService in the src/ java/com/g2one/gtunes directory, as shown in Figure 16-7. Figure 16-7. The StreamService.java file Rather than reading each byte, you could take advantage of the java.nio.channels pack- age that allows optimized file transfer. Of course, you could use the java.nio.channels package from Groovy, but we’re currently shooting for maximum performance by writing the class in Java. Listing 16-38 shows the implementation of the StreamingService class, which provides a method called streamSong that can be used to transfer the bytes of a Song instance to the given OutputStream. Listing 16-38. The StreamingService Class package com.g2one.gtunes; import java.io.*; import java.nio.channels.*; import org.apache.commons.logging.*; CHAPTER 16 ■ LEVERAGING SPRING 515 public class StreamingService { private static final int BUFFER_SIZE = 2048; private static final Log LOG = LogFactory.getLog(StreamingService.class); /** * Streams the given song to the given OutputStream */ public void streamSong(Song song, OutputStream out) { if(song != null) { File file = new File(song.getFile()); FileInputStream input = null; try { input = new FileInputStream(file); FileChannel in = input.getChannel(); in.transferTo(0,in.size(), Channels.newChannel(out)); out.flush(); } catch(Exception e) { throw new RuntimeException(e.getMessage(), e); } finally { try { input.close(); } catch(IOException e) { // ignore } } } } } One important thing to note is that this Java class references the domain class com.g2one. gtunes.Song, which is written in Groovy. Groovy’s joint compiler allows Java classes to resolve Groovy classes, something that, as of this writing, is not possible in any other dynamic lan- guage on the JVM. The remainder of the code simply obtains a FileChannel instance and then calls the transferTo method to transfer the file to the response OutputStream. Now you could just use the new operator to create a new instance of the StreamingService class within the SongController. But a nicer way to do this is to use Spring. Simply register a new bean in the grails-app/conf/spring/resources.groovy file for the StreamingService class, as shown in Listing 16-39. Listing 16-39. Creating a streamingService Bean streamingService(com.g2one.gtunes.StreamingService) 516 CHAPTER 16 ■ LEVERAGING SPRING Now to obtain a reference to this bean in SongController, just create the equivalent property: def streamingService The stream action can then be modified to take advantage of the streamingService instance, as shown in Listing 16-40. Listing 16-40. Using the streamingService Bean def stream = { if(song) { def albumPermission = new AlbumPermission(album:song.album) jsec.hasPermission(permission:albumPermission) { response.contentType = "audio/x-${song.file[-3 1]}" streamingService.streamSong(song, response.outputStream) } } } As you can see from Listing 16-40, the streamSong method is called, passing in the Song instance and the response object’s outputStream property. You now have a much better- performing implementation that uses the java.nio.channels package instead. Since it is a Spring bean, if you one day decided to change the StreamingService implementation—for example, to stream the music from Amazon S3 instead—then all you would need to do is alter the grails-app/conf/spring/resources.groovy file and register a different implementation. The SongController would need no changes at all since it is using duck typing to invoke the streamSong method. If you prefer static typing, then you could introduce an interface that the StreamingService class can implement, exactly as you would do in Java. Summary This chapter gave you some revealing insight into the inner workings of Grails and its Spring underpinnings. Moreover, you have learned that just because Grails embraces Convention over Configuration, it does not mean that configuration is not possible. Quite the contrary— every aspect of Grails is customizable thanks to Spring. Grails provides such a clean abstraction over Spring that often users of Grails simply don’t know Spring is there. In this chapter, you saw how you can reach out to great Spring APIs, such as the JMS support, to help you solve commons problems. Having said that, Spring is an enor- mous framework that provides far more features and benefits than we could possibly cover in this chapter. There are Spring abstractions for pretty much every major Java standard and many of the popular open source projects too. CHAPTER 16 ■ LEVERAGING SPRING 517 If you really want to get to grips with Spring, we recommend you invest some time reading the excellent reference documentation at http://static.springframework.org/spring/docs/ 2.5.x/reference/ or take a look at Apress’ excellent array of Spring books. 2 Doing so will help improve your knowledge of Grails too, because fundamentally Grails is just Spring and Hiber- nate in disguise! In the next chapter, we’ll look at one of the other major frameworks that Grails builds on for its persistence concerns: Hibernate. 2. Some recent Spring books published by Apress include Pro Spring 2.5 by Jan Machacek et al. (Apress, 2008), Spring Recipes: A Problem-Solution Approach by Gary Mak (Apress, 2008), and Pro Java EE Spring Patterns: Best Practices and Design Strategies Implementing Java EE Patterns with the Spring Framework by Dhrubojyoti Kayal (Apress, 2008). [...]... specify the name of the column to store the identifier of the many side Crucially, the mapping in Listing 1 7-5 works only for a unidirectional one -to- many because with a bidirectional one -to- many mapping a join table is not used Instead, a foreign key association is created Figure 1 7-2 shows how GORM maps a bidirectional one -to- many association Figure 1 7-2 A bidirectional one -to- many association As... 1 7-3 shows an example of how a many -to- many association works if you created a hypothetical Composer domain class Each Composer has many albums, while each Album has many composers, making this a many -to- many relationship Figure 1 7-3 How Grails maps a many -to- many association You can change the way a many -to- many association maps onto the underlying database using the same joinTable argument used to. .. street; } } With that rather long listing out of the way, there are a few things left to do to complete the migration to an EJB 3.0 entity First, you need to update each DataSource in the grails- app/conf directory to tell Grails that you want to use an annotation configuration strategy Listing 1 7-2 8 shows the necessary changes to the development DataSource Listing 1 7-2 8 Specifying the Annotation Configuration... joinTable:[name:'Artist _To_ Records', key:'Artist_Id', column:'Record_Id'] } } In the example in Listing 1 7-5 , the joinTable argument is used to map the unidirectional albums association onto a join table called Artist _To_ Records The key argument is used to specify the column to store the identifier of the one side, which in this case is the id property of the Artist Conversely, the column argument is used to specify the. .. artist property to a column called R_CREATOR_ID A one -to- many association requires a little more thought First you need to consider whether the one -to- many association is unidirectional or bidirectional With a unidirectional one-tomany association, GORM will use a join table to associate the two tables, since there isn’t a foreign key available on the many side of the association Figure 1 7-1 illustrates... that uses the column argument with the artist property of the Album class Listing 1 7-6 Changing the Foreign Key for a Bidirectional One -to- Many Association class Album { static mapping = { artist column: "R_Artist_Id" } } One final relationship type to consider is a many -to- many association A many -to- many association is mapped using a join table in a similar way to a unidirectional one -to- many association... The previous mapping simply tells Hibernate which classes are persistent, and the configuration of the mapping is then delegated to the annotations contained within the classes themselves You now need to delete the Groovy version of the Address domain class in grails/ domain, since its Java equivalent has superseded it You can now start Grails using grails run-app,... At the moment, there is just an empty configuration file To map individual classes, it is good practice to create individual mapping files for each class and then refer to them in the main hibernate.cfg.xml file Listing 1 7-2 0 shows how you can use the tag within the hibernate.cfg.xml file to achieve... http://www.hibernate.org/ hib_docs/reference/en/html/mapping.html#mapping-declaration-id-generator Nevertheless, as an example of configuring a custom generator in Grails, Listing 1 7-1 5 shows how to configure a hilo generator Listing 1 7-1 5 Configuring a hilo Generator class Album { static mapping = { id generator:'hilo', params:[table:'hi_value', column:'next_value', max_lo:100] } } The example in Listing 1 7-1 5 uses a... generator For example, say you wanted to use the title property of the Album class as the identifier instead You could do so with the code in Listing 1 7-1 7 Listing 1 7-1 7 Configuring an assigned Generator class Album { String title static mapping = { id name: "title", generator: "assigned" } } The name argument is used to signify the name of the property used for the identifier, while the generator argument . Conversely, the column argument is used to specify the name of the column to store the identifier of the many side. Crucially, the mapping in Listing 1 7-5 works only for a unidirectional one -to- many. relationship type to consider is a many -to- many association. A many -to- many association is mapped using a join table in a similar way to a unidirectional one -to- many associ- ation. Figure 1 7-3 shows. relationship. Figure 1 7-3 . How Grails maps a many -to- many association You can change the way a many -to- many association maps onto the underlying database using the same joinTable argument used to configure