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 }