View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  package example.mediaserver;
16  
17  import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder;
18  import org.fourthline.cling.controlpoint.ActionCallback;
19  import org.fourthline.cling.model.DefaultServiceManager;
20  import org.fourthline.cling.model.action.ActionInvocation;
21  import org.fourthline.cling.model.message.UpnpResponse;
22  import org.fourthline.cling.model.meta.LocalService;
23  import org.fourthline.cling.model.meta.Service;
24  import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService;
25  import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode;
26  import org.fourthline.cling.support.contentdirectory.ContentDirectoryException;
27  import org.fourthline.cling.support.contentdirectory.DIDLParser;
28  import org.fourthline.cling.support.contentdirectory.callback.Browse;
29  import org.fourthline.cling.support.model.BrowseFlag;
30  import org.fourthline.cling.support.model.BrowseResult;
31  import org.fourthline.cling.support.model.DIDLContent;
32  import org.fourthline.cling.support.model.DIDLObject;
33  import org.fourthline.cling.support.model.PersonWithRole;
34  import org.fourthline.cling.support.model.Res;
35  import org.fourthline.cling.support.model.SortCriterion;
36  import org.fourthline.cling.support.model.item.Item;
37  import org.fourthline.cling.support.model.item.MusicTrack;
38  import org.seamless.util.io.IO;
39  import org.seamless.util.MimeType;
40  import org.testng.annotations.Test;
41  
42  import java.io.InputStream;
43  
44  import static org.testng.Assert.assertEquals;
45  
46  /**
47   * Browsing a ContentDirectory
48   * <p/>
49   * <p>
50   * A <em>ContentDirectory:1</em> service provides media resource metadata. The content format for
51   * this metadata is XML and the schema is a mixture of DIDL, Dublic Core, and UPnP specific elements
52   * and attributes. Usually you'd have to call the <code>Browse</code> action of the content directory
53   * service to get this XML metadata and then parse it manually.
54   * </p>
55   * <p>
56   * The <code>Browse</code> action callback in Cling Support handles all of this for you:
57   * </p>
58   * <a class="citation" href="javadoc://this#browseTracks()" style="read-title: false;"/>
59   */
60  public class ContentDirectoryBrowseTest {
61  
62      protected DIDLParser parser = new DIDLParser();
63  
64      @Test
65      public void browseRootMetadata() {
66  
67          final boolean[] assertions = new boolean[3];
68          new Browse(createService(), "0", BrowseFlag.METADATA) {
69              @Override
70              public void received(ActionInvocation actionInvocation, DIDLContent didl) {
71                  assertEquals(didl.getContainers().size(), 1);
72                  assertEquals(didl.getContainers().get(0).getTitle(), "My multimedia stuff");
73                  assertions[0] = true;
74              }
75  
76              @Override
77              public void updateStatus(Status status) {
78                  if (!assertions[1] && status.equals(Status.LOADING)) {
79                      assertions[1] = true;
80                  } else if (assertions[1] && !assertions[2] && status.equals(Status.OK)) {
81                      assertions[2] = true;
82                  }
83              }
84  
85              @Override
86              public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
87  
88              }
89          }.run();
90  
91          for (boolean assertion : assertions) {
92              assertEquals(assertion, true);
93          }
94      }
95  
96      @Test
97      public void browseRootChildren() {
98  
99          final boolean[] assertions = new boolean[3];
100         new Browse(
101                 createService(), "0", BrowseFlag.DIRECT_CHILDREN, "foo", 1, 10l,
102                 new SortCriterion(true, "dc:title"), new SortCriterion(false, "dc:creator")
103         ) {
104             public void received(ActionInvocation actionInvocation, DIDLContent didl) {
105                 assertEquals(didl.getContainers().size(), 3);
106                 assertEquals(didl.getContainers().get(0).getTitle(), "My Music");
107                 assertEquals(didl.getContainers().get(1).getTitle(), "My Photos");
108                 assertEquals(didl.getContainers().get(2).getTitle(), "Album Art");
109                 assertions[0] = true;
110             }
111 
112             @Override
113             public void updateStatus(Status status) {
114                 if (!assertions[1] && status.equals(Status.LOADING)) {
115                     assertions[1] = true;
116                 } else if (assertions[1] && !assertions[2] && status.equals(Status.OK)) {
117                     assertions[2] = true;
118                 }
119             }
120 
121             @Override
122             public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
123             }
124         }.run();
125 
126         for (boolean assertion : assertions) {
127             assertEquals(assertion, true);
128         }
129     }
130 
131     /**
132      * <a class="citation" href="javacode://this" style="include: INC1; exclude: EXC1, EXC2;"/>
133      * <p>
134      * The first callback retrieves all the children of container <code>3</code> (container identifier).
135      * </p>
136      * <div class="note" id="browse_root_container">
137      *     <div class="title">The root container identifier</div>
138      *     You can not copy/paste the shown example code! It will most likely not return any items!
139      *     You need to use a different container ID! The shown container ID '3' is just an example.
140      *     Your server does not have a container with identifier '3'! If you want to browse the
141      *     "root" container of the ContentDirectory, use the identifier '0':
142      *     <code>Browse(service, "0", BrowseFlag.DIRECT_CHILDREN)</code>. Although not standardized
143      *     many media servers consider the ID '0' to be the root container's identifier. If it's not,
144      *     ask your media server vendor. By listing all the children of the root container you can
145      *     get the identifiers of sub-containers and so on, recursively.
146      * </div>
147      * <p>
148      * The <code>received()</code> method is called after the DIDL XML content has been validated and
149      * parsed, so you can use a type-safe API to work with the metadata.
150      * DIDL content is a composite structure of <code>Container</code> and <code>Item</code> elements,
151      * here we are interested in the items of the container, ignoring any sub-containers it might or
152      * might not have.
153      * </p>
154      * <p>
155      * You can implement or ignore the <code>updateStatus()</code> method, it's convenient to be
156      * notified before the metadata is loaded, and after it has been parsed. You can use this
157      * event to update a status message/icon of your user interface, for example.
158      * </p>
159      * <p>
160      * This more complex callback instantiation shows some of the available options:
161      * </p>
162      * <a class="citation" href="javacode://this" id="browse_tracks2" style="include: INC2; exclude: EXC3;"/>
163      * <p>
164      * The arguments declare filtering with a wildcard, limiting the result to 50 items starting at
165      * item 100 (pagination), and some sort criteria. It's up to the content directory
166      * provider to handle these options.
167      * </p>
168      */
169     @Test
170     public void browseTracks() {
171 
172         final boolean[] assertions = new boolean[3];
173 
174         Service service = createService();
175 
176         ActionCallback simpleBrowseAction =
177                 new Browse(service, "3", BrowseFlag.DIRECT_CHILDREN) {                      // DOC: INC1
178 
179                     @Override
180                     public void received(ActionInvocation actionInvocation, DIDLContent didl) {
181 
182                         // Read the DIDL content either using generic Container and Item types...
183                         assertEquals(didl.getItems().size(), 2);
184                         Item item1 = didl.getItems().get(0);
185                         assertEquals(
186                                 item1.getTitle(),
187                                 "All Secrets Known"
188                         );
189                         assertEquals(
190                                 item1.getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM.class),
191                                 "Black Gives Way To Blue"
192                         );
193                         assertEquals(
194                                 item1.getFirstResource().getProtocolInfo().getContentFormatMimeType().toString(),
195                                 "audio/mpeg"
196                         );
197                         assertEquals(
198                                 item1.getFirstResource().getValue(),
199                                 "http://10.0.0.1/files/101.mp3"
200                         );
201 
202                         // ... or cast it if you are sure about its type ...
203                         assert MusicTrack.CLASS.equals(item1);
204                         MusicTrack track1 = (MusicTrack) item1;
205                         assertEquals(track1.getTitle(), "All Secrets Known");
206                         assertEquals(track1.getAlbum(), "Black Gives Way To Blue");
207                         assertEquals(track1.getFirstArtist().getName(), "Alice In Chains");
208                         assertEquals(track1.getFirstArtist().getRole(), "Performer");
209 
210                         MusicTrack track2 = (MusicTrack) didl.getItems().get(1);
211                         assertEquals(track2.getTitle(), "Check My Brain");
212 
213                         // ... which is much nicer for manual parsing, of course!
214 
215                         // DOC: EXC1
216                         assertions[0] = true;
217                         // DOC: EXC1
218                     }
219 
220                     @Override
221                     public void updateStatus(Status status) {
222                         // Called before and after loading the DIDL content
223                         // DOC: EXC2
224                         if (!assertions[1] && status.equals(Status.LOADING)) {
225                             assertions[1] = true;
226                         } else if (assertions[1] && !assertions[2] && status.equals(Status.OK)) {
227                             assertions[2] = true;
228                         }
229                         // DOC: EXC2
230                     }
231 
232                     @Override
233                     public void failure(ActionInvocation invocation,
234                                         UpnpResponse operation,
235                                         String defaultMsg) {
236                         // Something wasn't right...
237                     }
238                 };                                                                          // DOC: INC1
239 
240 
241         ActionCallback complexBrowseAction =                                                // DOC: INC2
242                 new Browse(service, "3", BrowseFlag.DIRECT_CHILDREN,
243                            "*",
244                            100l, 50l,
245                            new SortCriterion(true, "dc:title"),        // Ascending
246                            new SortCriterion(false, "dc:creator")) {   // Descending
247 
248                     // Implementation...
249 
250                     // DOC: EXC3
251                     @Override
252                     public void received(ActionInvocation actionInvocation, DIDLContent didl) {
253 
254                     }
255 
256                     @Override
257                     public void updateStatus(Status status) {
258 
259                     }
260 
261                     @Override
262                     public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
263                     }
264                     // DOC: EXC3
265                 };                                                                          // DOC: INC2
266 
267         simpleBrowseAction.run();
268 
269         for (boolean assertion : assertions) {
270             assertEquals(assertion, true);
271         }
272     }
273 
274     public Service createService() {
275         LocalService<AbstractContentDirectoryService> service =
276                 new AnnotationLocalServiceBinder().read(AbstractContentDirectoryService.class);
277         service.setManager(
278                 new DefaultServiceManager<AbstractContentDirectoryService>(service, null) {
279                     @Override
280                     protected AbstractContentDirectoryService createServiceInstance() throws Exception {
281                         return new MP3ContentDirectory();
282                     }
283                 }
284         );
285         return service;
286     }
287 
288     /**
289      * The ContentDirectory service
290      * <p>
291      * Let's switch perspective and consider the server-side of a <em>ContentDirectory</em>. Bundled in
292      * Cling Support is a simple <em>ContentDirectory</em> abstract service class,
293      * the only thing you have to do is implement the <code>browse()</code> method:
294      * </p>
295      * <a class="citation" href="javacode://this" style="exclude: EXC1, EXTRA"/>
296      * <p>
297      * You need a <code>DIDLContent</code> instance and a <code>DIDLParser</code> that will transform
298      * the content into an XML string when the <code>BrowseResult</code> is returned. It's up to
299      * you how you construct the DIDL content, typically you'd have a backend database you'd query
300      * and then build the <code>Container</code> and <code>Item</code> graph dynamically. Cling provides
301      * many convenience content model classes fore representing multimedia metadata, as defined
302      * in the <em>ContentDirectory:1</em> specification (<code>MusicTrack</code>, <code>Movie</code>, etc.),
303      * they can all be found in the package <code>org.fourthline.cling.support.model</code>.
304      * </p>
305      * <p>
306      * The <code>DIDLParser</code> is <em>not</em> thread-safe, so don't share a single instance
307      * between all threads of your server application!
308      * </p>
309      * <p>
310      * The <code>AbstractContentDirectoryService</code> only implements the mandatory actions and
311      * state variables as defined in <em>ContentDirectory:1</em> for browsing and searching
312      * content. If you want to enable editing of metadata, you have to add additional action methods.
313      * </p>
314      */
315     public class MP3ContentDirectory extends AbstractContentDirectoryService {
316 
317         @Override
318         public BrowseResult browse(String objectID, BrowseFlag browseFlag,
319                                    String filter,
320                                    long firstResult, long maxResults,
321                                    SortCriterion[] orderby) throws ContentDirectoryException {
322             try {
323                 // DOC: EXC1
324                 if (objectID.equals("0") && browseFlag.equals(BrowseFlag.METADATA)) {
325 
326                     assertEquals(firstResult, 0);
327                     assertEquals(maxResults, 999);
328 
329                     assertEquals(orderby.length, 0);
330 
331                     String result = readResource("org/fourthline/cling/test/support/contentdirectory/samples/browseRoot.xml");
332                     return new BrowseResult(result, 1, 1);
333 
334                 } else if (objectID.equals("0") && browseFlag.equals(BrowseFlag.DIRECT_CHILDREN)) {
335 
336                     assertEquals(filter, "foo");
337                     assertEquals(firstResult, 1);
338                     assertEquals(maxResults, 10l);
339 
340                     assertEquals(orderby.length, 2); // We don't sort, just test
341                     assertEquals(orderby[0].isAscending(), true);
342                     assertEquals(orderby[0].getPropertyName(), "dc:title");
343                     assertEquals(orderby[1].isAscending(), false);
344                     assertEquals(orderby[1].getPropertyName(), "dc:creator");
345 
346                     String result = readResource("org/fourthline/cling/test/support/contentdirectory/samples/browseRootChildren.xml");
347                     return new BrowseResult(result, 3, 3);
348                 }
349                 // DOC: EXC1
350 
351                 // This is just an example... you have to create the DIDL content dynamically!
352 
353                 DIDLContent didl = new DIDLContent();
354 
355                 String album = ("Black Gives Way To Blue");
356                 String creator = "Alice In Chains"; // Required
357                 PersonWithRole artist = new PersonWithRole(creator, "Performer");
358                 MimeType mimeType = new MimeType("audio", "mpeg");
359 
360                 didl.addItem(new MusicTrack(
361                         "101", "3", // 101 is the Item ID, 3 is the parent Container ID
362                         "All Secrets Known",
363                         creator, album, artist,
364                         new Res(mimeType, 123456l, "00:03:25", 8192l, "http://10.0.0.1/files/101.mp3")
365                 ));
366 
367                 didl.addItem(new MusicTrack(
368                         "102", "3",
369                         "Check My Brain",
370                         creator, album, artist,
371                         new Res(mimeType, 2222222l, "00:04:11", 8192l, "http://10.0.0.1/files/102.mp3")
372                 ));
373 
374                 // Create more tracks...
375 
376                 // Count and total matches is 2
377                 return new BrowseResult(new DIDLParser().generate(didl), 2, 2);
378 
379             } catch (Exception ex) {
380                 throw new ContentDirectoryException(
381                         ContentDirectoryErrorCode.CANNOT_PROCESS,
382                         ex.toString()
383                 );
384             }
385         }
386 
387         @Override
388         public BrowseResult search(String containerId,
389                                    String searchCriteria, String filter,
390                                    long firstResult, long maxResults,
391                                    SortCriterion[] orderBy) throws ContentDirectoryException {
392             // You can override this method to implement searching!
393             return super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy);
394         }
395     }
396 
397     protected String readResource(String resource) {
398         InputStream is = null;
399         try {
400             is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
401             return IO.readLines(is);
402         } catch (Exception ex) {
403             throw new RuntimeException(ex);
404         } finally {
405             try {
406                 if (is != null) is.close();
407             } catch (Exception ex) {
408                 //
409             }
410         }
411     }
412 
413 }