1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.elasticsearch.index.objects.event;
23
24 import static org.opencastproject.security.util.SecurityUtil.getEpisodeRoleId;
25
26 import org.opencastproject.elasticsearch.api.SearchIndexException;
27 import org.opencastproject.elasticsearch.api.SearchMetadata;
28 import org.opencastproject.elasticsearch.api.SearchResult;
29 import org.opencastproject.elasticsearch.impl.SearchMetadataCollection;
30 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
31 import org.opencastproject.elasticsearch.index.objects.series.Series;
32 import org.opencastproject.elasticsearch.index.objects.series.SeriesIndexSchema;
33 import org.opencastproject.elasticsearch.index.objects.series.SeriesSearchQuery;
34 import org.opencastproject.list.api.ListProviderException;
35 import org.opencastproject.list.api.ListProvidersService;
36 import org.opencastproject.list.api.ResourceListQuery;
37 import org.opencastproject.list.impl.ResourceListQueryImpl;
38 import org.opencastproject.mediapackage.Attachment;
39 import org.opencastproject.mediapackage.Catalog;
40 import org.opencastproject.mediapackage.EName;
41 import org.opencastproject.mediapackage.MediaPackage;
42 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
43 import org.opencastproject.mediapackage.Publication;
44 import org.opencastproject.mediapackage.Track;
45 import org.opencastproject.metadata.dublincore.DCMIPeriod;
46 import org.opencastproject.metadata.dublincore.DublinCore;
47 import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
48 import org.opencastproject.metadata.dublincore.EncodingSchemeUtils;
49 import org.opencastproject.security.api.AccessControlEntry;
50 import org.opencastproject.security.api.AccessControlList;
51 import org.opencastproject.security.api.AccessControlParser;
52 import org.opencastproject.security.api.Permissions;
53 import org.opencastproject.security.api.Permissions.Action;
54 import org.opencastproject.security.api.User;
55 import org.opencastproject.util.DateTimeSupport;
56
57 import org.apache.commons.io.IOUtils;
58 import org.apache.commons.lang3.StringUtils;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import java.io.IOException;
63 import java.nio.charset.Charset;
64 import java.util.ArrayList;
65 import java.util.Arrays;
66 import java.util.Date;
67 import java.util.HashMap;
68 import java.util.HashSet;
69 import java.util.List;
70 import java.util.Map;
71 import java.util.Set;
72 import java.util.TreeSet;
73
74 import javax.xml.bind.Unmarshaller;
75
76
77
78
79 public final class EventIndexUtils {
80
81 private static final Logger logger = LoggerFactory.getLogger(EventIndexUtils.class);
82
83
84 public static final int DEFAULT_ATTEMPTS = 10;
85
86 public static final long DEFAULT_SLEEP = 100L;
87
88
89
90
91 private EventIndexUtils() {
92 }
93
94
95
96
97
98
99
100
101
102
103
104 public static Event toRecordingEvent(SearchMetadataCollection metadata, Unmarshaller unmarshaller)
105 throws IOException {
106 Map<String, SearchMetadata<?>> metadataMap = metadata.toMap();
107 String eventJson = (String) metadataMap.get(EventIndexSchema.OBJECT).getValue();
108 return Event.valueOf(IOUtils.toInputStream(eventJson, Charset.defaultCharset()), unmarshaller);
109 }
110
111
112
113
114
115
116
117
118 public static SearchMetadataCollection toSearchMetadata(Event event, ListProvidersService listProviderService,
119 boolean episodeIdRole) {
120 SearchMetadataCollection metadata = new SearchMetadataCollection(
121 event.getIdentifier().concat(event.getOrganization()), Event.DOCUMENT_TYPE);
122 metadata.addField(EventIndexSchema.UID, event.getIdentifier(), true);
123 metadata.addField(EventIndexSchema.ORGANIZATION, event.getOrganization(), false);
124 metadata.addField(EventIndexSchema.OBJECT, event.toXML(), false);
125 if (StringUtils.isNotBlank(event.getTitle())) {
126 metadata.addField(EventIndexSchema.TITLE, event.getTitle(), true);
127 }
128 if (StringUtils.isNotBlank(event.getDescription())) {
129 metadata.addField(EventIndexSchema.DESCRIPTION, event.getDescription(), true);
130 }
131 if (StringUtils.isNotBlank(event.getLocation())) {
132 metadata.addField(EventIndexSchema.LOCATION, event.getLocation(), true);
133 }
134 if (StringUtils.isNotBlank(event.getSeriesId())) {
135 metadata.addField(EventIndexSchema.SERIES_ID, event.getSeriesId(), true);
136 }
137 if (StringUtils.isNotBlank(event.getSeriesName())) {
138 metadata.addField(EventIndexSchema.SERIES_NAME, event.getSeriesName(), true);
139 }
140 if (StringUtils.isNotBlank(event.getLanguage())) {
141 metadata.addField(EventIndexSchema.LANGUAGE, event.getLanguage(), true);
142 }
143 if (StringUtils.isNotBlank(event.getSubject())) {
144 metadata.addField(EventIndexSchema.SUBJECT, event.getSubject(), true);
145 }
146 if (StringUtils.isNotBlank(event.getSource())) {
147 metadata.addField(EventIndexSchema.SOURCE, event.getSource(), true);
148 }
149 if (StringUtils.isNotBlank(event.getCreated())) {
150 metadata.addField(EventIndexSchema.CREATED, event.getCreated(), true);
151 }
152 if (StringUtils.isNotBlank(event.getCreator())) {
153 metadata.addField(EventIndexSchema.CREATOR, event.getCreator(), true);
154 }
155 if (StringUtils.isNotBlank(event.getPublisher())) {
156 metadata.addField(EventIndexSchema.PUBLISHER, event.getPublisher(), true);
157 }
158 if (StringUtils.isNotBlank(event.getLicense())) {
159 metadata.addField(EventIndexSchema.LICENSE, event.getLicense(), true);
160 }
161 if (StringUtils.isNotBlank(event.getRights())) {
162 metadata.addField(EventIndexSchema.RIGHTS, event.getRights(), true);
163 }
164 if (StringUtils.isNotBlank(event.getManagedAcl())) {
165 metadata.addField(EventIndexSchema.MANAGED_ACL, event.getManagedAcl(), true);
166 }
167 if (StringUtils.isNotBlank(event.getWorkflowState())) {
168 metadata.addField(EventIndexSchema.WORKFLOW_STATE, event.getWorkflowState(), true);
169 }
170 if (event.getWorkflowId() != null) {
171 metadata.addField(EventIndexSchema.WORKFLOW_ID, event.getWorkflowId(), true);
172 }
173 if (StringUtils.isNotBlank(event.getWorkflowDefinitionId())) {
174 metadata.addField(EventIndexSchema.WORKFLOW_DEFINITION_ID, event.getWorkflowDefinitionId(), true);
175 }
176 if (StringUtils.isNotBlank(event.getRecordingStartDate())) {
177 metadata.addField(EventIndexSchema.START_DATE, event.getRecordingStartDate(), true);
178 }
179 if (StringUtils.isNotBlank(event.getRecordingEndDate())) {
180 metadata.addField(EventIndexSchema.END_DATE, event.getRecordingEndDate(), true);
181 }
182 if (event.getDuration() != null) {
183 metadata.addField(EventIndexSchema.DURATION, event.getDuration(), true);
184 }
185 if (event.getArchiveVersion() != null) {
186 metadata.addField(EventIndexSchema.ARCHIVE_VERSION, event.getArchiveVersion(), true);
187 }
188 if (event.getRecordingStatus() != null) {
189 metadata.addField(EventIndexSchema.RECORDING_STATUS, event.getRecordingStatus(), true);
190 }
191
192 metadata.addField(EventIndexSchema.EVENT_STATUS, event.getEventStatus(), true);
193
194 metadata.addField(EventIndexSchema.HAS_COMMENTS, event.hasComments(), true);
195 metadata.addField(EventIndexSchema.HAS_OPEN_COMMENTS, event.hasOpenComments(), true);
196
197 if (event.comments() != null) {
198 List<Comment> comments = event.comments();
199 HashMap<String, Object>[] commentsArray = new HashMap[comments.size()];
200 for (int i = 0; i < comments.size(); i++) {
201 Comment comment = comments.get(i);
202 HashMap<String, Object> myMap = new HashMap<String, Object>() {{
203 put(CommentIndexSchema.ID, comment.getId());
204 put(CommentIndexSchema.REASON, comment.getReason());
205 put(CommentIndexSchema.TEXT, comment.getText());
206 put(CommentIndexSchema.RESOLVED_STATUS, comment.isResolvedStatus());
207 }};
208 commentsArray[i] = myMap;
209
210 }
211
212 metadata.addField(EventIndexSchema.COMMENTS, commentsArray, true);
213 }
214
215 metadata.addField(EventIndexSchema.NEEDS_CUTTING, event.needsCutting(), true);
216
217 if (event.getPublications() != null) {
218 List<Publication> publications = event.getPublications();
219 HashMap<String, Object>[] publicationsArray = new HashMap[publications.size()];
220 for (int i = 0; i < publications.size(); i++) {
221 publicationsArray[i] = generatePublicationDoc(publications.get(i));
222 }
223
224 if (publications.size() == 1 && !publications.get(0).getChannel().equals("internal") || publications.size() > 1) {
225 metadata.addField(EventIndexSchema.IS_PUBLISHED, true, true);
226 } else {
227 metadata.addField(EventIndexSchema.IS_PUBLISHED, false, true);
228 }
229
230 metadata.addField(EventIndexSchema.PUBLICATION, publicationsArray, true);
231
232 }
233
234 if (event.getPresenters() != null) {
235 List<String> presenters = event.getPresenters();
236 metadata.addField(EventIndexSchema.PRESENTER, presenters.toArray(new String[presenters.size()]), true);
237 }
238
239 if (event.getContributors() != null) {
240 List<String> contributors = event.getContributors();
241 metadata.addField(EventIndexSchema.CONTRIBUTOR, contributors.toArray(new String[contributors.size()]), true);
242 }
243
244 if (!event.getExtendedMetadata().isEmpty()) {
245 addExtendedMetadata(metadata, event.getExtendedMetadata());
246 }
247
248 if (StringUtils.isNotBlank(event.getAccessPolicy())) {
249 metadata.addField(EventIndexSchema.ACCESS_POLICY, event.getAccessPolicy(), true);
250 addAuthorization(metadata, event.getAccessPolicy(), event.getIdentifier(), listProviderService, episodeIdRole);
251 }
252
253 if (StringUtils.isNotBlank(event.getAgentId())) {
254 metadata.addField(EventIndexSchema.AGENT_ID, event.getAgentId(), true);
255 }
256
257 if (StringUtils.isNotBlank(event.getTechnicalStartTime())) {
258 metadata.addField(EventIndexSchema.TECHNICAL_START, event.getTechnicalStartTime(), true);
259 }
260
261 if (StringUtils.isNotBlank(event.getTechnicalEndTime())) {
262 metadata.addField(EventIndexSchema.TECHNICAL_END, event.getTechnicalEndTime(), true);
263 }
264
265 if (event.getTechnicalPresenters() != null) {
266 metadata.addField(EventIndexSchema.TECHNICAL_PRESENTERS,
267 event.getTechnicalPresenters().toArray(new String[event.getTechnicalPresenters().size()]), true);
268 }
269
270 return metadata;
271 }
272
273 private static void addObjectStringtToMap(HashMap<String, Object> map, String key, Object value) {
274 if (value == null) {
275 map.put(key, "");
276 } else {
277 map.put(key, value.toString());
278 }
279 }
280
281
282
283
284
285
286
287
288 private static HashMap<String, Object> generatePublicationDoc(Publication publication) {
289 HashMap<String, Object> pMap = new HashMap<String, Object>();
290
291
292 pMap.put(PublicationIndexSchema.CHANNEL, publication.getChannel());
293 addObjectStringtToMap(pMap, PublicationIndexSchema.MIMETYPE, publication.getMimeType());
294
295
296 Attachment[] attachments = publication.getAttachments();
297 HashMap<String, Object>[] attachmentsArray = new HashMap[attachments.length];
298 for (int i = 0; i < attachmentsArray.length; i++) {
299 Attachment attachment = attachments[i];
300 HashMap<String, Object> element = new HashMap<String, Object>();
301 element.put(PublicationIndexSchema.ELEMENT_ID, attachment.getIdentifier());
302 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_MIMETYPE, attachment.getMimeType());
303 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_TYPE, attachment.getElementType());
304 element.put(PublicationIndexSchema.ELEMENT_TAG, attachment.getTags());
305 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_URL, attachment.getURI());
306 element.put(PublicationIndexSchema.ELEMENT_SIZE, attachment.getSize());
307 attachmentsArray[i] = element;
308 }
309 pMap.put(PublicationIndexSchema.ATTACHMENT, attachmentsArray);
310
311
312 Catalog[] catalogs = publication.getCatalogs();
313 HashMap<String, Object>[] catalogsArray = new HashMap[catalogs.length];
314 for (int i = 0; i < catalogsArray.length; i++) {
315 Catalog catalog = catalogs[i];
316 HashMap<String, Object> element = new HashMap<String, Object>();
317 element.put(PublicationIndexSchema.ELEMENT_ID, catalog.getIdentifier());
318 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_MIMETYPE, catalog.getMimeType());
319 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_TYPE, catalog.getElementType());
320 element.put(PublicationIndexSchema.ELEMENT_TAG, catalog.getTags());
321 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_URL, catalog.getURI());
322 element.put(PublicationIndexSchema.ELEMENT_SIZE, catalog.getSize());
323 catalogsArray[i] = element;
324 }
325 pMap.put(PublicationIndexSchema.CATALOG, catalogsArray);
326
327
328 Track[] tracks = publication.getTracks();
329 HashMap<String, Object>[] tracksArray = new HashMap[tracks.length];
330 for (int i = 0; i < tracksArray.length; i++) {
331 Track track = tracks[i];
332 HashMap<String, Object> element = new HashMap<String, Object>();
333 element.put(PublicationIndexSchema.ELEMENT_ID, track.getIdentifier());
334 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_MIMETYPE, track.getMimeType());
335 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_TYPE, track.getElementType());
336 element.put(PublicationIndexSchema.ELEMENT_TAG, track.getTags());
337 addObjectStringtToMap(element, PublicationIndexSchema.ELEMENT_URL, track.getURI());
338 element.put(PublicationIndexSchema.ELEMENT_SIZE, track.getSize());
339 element.put(PublicationIndexSchema.TRACK_DURATION, track.getDuration());
340 tracksArray[i] = element;
341 }
342 pMap.put(PublicationIndexSchema.TRACK, tracksArray);
343
344 return pMap;
345 }
346
347
348
349
350
351
352
353
354
355 private static void addExtendedMetadata(SearchMetadataCollection doc, Map<String, Map<String,
356 List<String>>> extendedMetadata) {
357 for (String type: extendedMetadata.keySet()) {
358 Map<String, List<String>> extendedMetadataByType = extendedMetadata.get(type);
359 for (String name: extendedMetadataByType.keySet()) {
360 List<String> values = extendedMetadataByType.get(name);
361 String fieldName = SeriesIndexSchema.EXTENDED_METADATA_PREFIX.concat(type + "_" + name);
362 doc.addField(fieldName, values, true);
363 }
364 }
365 }
366
367
368
369
370
371
372
373
374
375 private static void addAuthorization(SearchMetadataCollection doc, String aclString,
376 String eventId, ListProvidersService listProvidersService, boolean episodeIdRole) {
377 Map<String, List<String>> permissions = new HashMap<>();
378
379
380 for (Action action : Permissions.Action.values()) {
381 permissions.put(action.toString(), new ArrayList<>());
382 }
383
384
385 AccessControlList acl = AccessControlParser.parseAclSilent(aclString);
386 List<AccessControlEntry> entries = acl.getEntries();
387
388
389 if (episodeIdRole) {
390 Set<AccessControlEntry> customEntries = new HashSet<>();
391 customEntries.add(new AccessControlEntry(getEpisodeRoleId(eventId, "READ"), "read", true));
392 customEntries.add(new AccessControlEntry(getEpisodeRoleId(eventId, "WRITE"), "write", true));
393
394 ResourceListQuery query = new ResourceListQueryImpl();
395 if (listProvidersService.hasProvider("ACL.ACTIONS")) {
396 Map<String, String> actions = new HashMap<>();
397 try {
398 actions = listProvidersService.getList("ACL.ACTIONS", query, true);
399 } catch (ListProviderException e) {
400 logger.error("Listproviders not loaded. " + e);
401 }
402 for (String action : actions.keySet()) {
403 customEntries.add(new AccessControlEntry(getEpisodeRoleId(eventId, action), action, true));
404 }
405 }
406
407 entries.addAll(customEntries);
408 }
409
410
411 for (AccessControlEntry entry : entries) {
412 if (!entry.isAllow()) {
413 logger.info("Event index does not support denial via ACL, ignoring {}", entry);
414 continue;
415 }
416 List<String> actionPermissions = permissions.get(entry.getAction());
417 if (actionPermissions == null) {
418 actionPermissions = new ArrayList<>();
419 permissions.put(entry.getAction(), actionPermissions);
420 }
421 actionPermissions.add(entry.getRole());
422 }
423
424
425 for (Map.Entry<String, List<String>> entry : permissions.entrySet()) {
426 String fieldName = EventIndexSchema.ACL_PERMISSION_PREFIX.concat(entry.getKey());
427 doc.addField(fieldName, entry.getValue(), false);
428 }
429 }
430
431
432
433
434
435
436
437
438
439
440
441
442
443 public static Event updateEventExtendedMetadata(Event event, DublinCoreCatalog dc, MediaPackageElementFlavor flavor) {
444 Map<String, List<String>> map = new HashMap();
445 Set<EName> eNames = dc.getProperties();
446 for (EName eName: eNames) {
447 String name = eName.getLocalName();
448 List<String> values = dc.get(eName, DublinCore.LANGUAGE_ANY);
449 map.put(name, values);
450 }
451 event.setExtendedMetadata(flavor.toString(), map);
452 return event;
453 }
454
455
456
457
458
459
460
461
462
463
464 public static Event updateEvent(Event event, DublinCore dc) {
465 event.setTitle(dc.getFirst(DublinCore.PROPERTY_TITLE));
466 event.setDescription(dc.getFirst(DublinCore.PROPERTY_DESCRIPTION));
467 event.setSubject(dc.getFirst(DublinCore.PROPERTY_SUBJECT));
468 event.setLocation(dc.getFirst(DublinCore.PROPERTY_SPATIAL));
469 event.setLanguage(dc.getFirst(DublinCore.PROPERTY_LANGUAGE));
470 event.setSource(dc.getFirst(DublinCore.PROPERTY_SOURCE));
471 event.setSeriesId(dc.getFirst(DublinCore.PROPERTY_IS_PART_OF));
472 event.setLicense(dc.getFirst(DublinCore.PROPERTY_LICENSE));
473 event.setRights(dc.getFirst(DublinCore.PROPERTY_RIGHTS_HOLDER));
474 event.setPublisher(dc.getFirst(DublinCore.PROPERTY_PUBLISHER));
475 Date created;
476 String encodedDate = dc.getFirst(DublinCore.PROPERTY_CREATED);
477 if (StringUtils.isBlank(encodedDate)) {
478 created = new Date();
479 } else {
480 created = EncodingSchemeUtils.decodeDate(encodedDate);
481 }
482 event.setCreated(DateTimeSupport.toUTC(created.getTime()));
483 String strPeriod = dc.getFirst(DublinCore.PROPERTY_TEMPORAL);
484 try {
485 if (StringUtils.isNotBlank(strPeriod)) {
486 DCMIPeriod period = EncodingSchemeUtils.decodeMandatoryPeriod(strPeriod);
487 event.setRecordingStartDate(DateTimeSupport.toUTC(period.getStart().getTime()));
488 event.setRecordingEndDate(DateTimeSupport.toUTC(period.getEnd().getTime()));
489 event.setDuration(period.getEnd().getTime() - period.getStart().getTime());
490 } else {
491 event.setRecordingStartDate(DateTimeSupport.toUTC(created.getTime()));
492 }
493 } catch (Exception e) {
494 logger.warn("Invalid start and end date/time for event {}: {}", event.getIdentifier(), strPeriod);
495 event.setRecordingStartDate(DateTimeSupport.toUTC(created.getTime()));
496 }
497
498 updateTechnicalDate(event);
499
500
501 event.setContributors(dc.get(DublinCore.PROPERTY_CONTRIBUTOR, DublinCore.LANGUAGE_ANY));
502 event.setPresenters(dc.get(DublinCore.PROPERTY_CREATOR, DublinCore.LANGUAGE_ANY));
503 return event;
504 }
505
506 public static Event updateTechnicalDate(Event event) {
507 if (event.isScheduledEvent() && event.hasRecordingStarted()) {
508
509 event.setTechnicalStartTime(event.getRecordingStartDate());
510 event.setTechnicalEndTime(event.getRecordingEndDate());
511 } else {
512
513 if (StringUtils.isBlank(event.getTechnicalStartTime())) {
514 event.setTechnicalStartTime(event.getRecordingStartDate());
515 }
516 if (StringUtils.isBlank(event.getTechnicalEndTime())) {
517 event.setTechnicalEndTime(event.getRecordingEndDate());
518 }
519 }
520 return event;
521 }
522
523
524
525
526
527
528
529
530
531
532 public static Event updateEvent(Event event, MediaPackage mp) {
533 event.setPublications(Arrays.asList(mp.getPublications()));
534 event.setSeriesName(mp.getSeriesTitle());
535 return event;
536 }
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551 public static void updateSeriesName(Event event, String organization, User user, ElasticsearchIndex searchIndex)
552 throws SearchIndexException {
553 updateSeriesName(event, organization, user, searchIndex, DEFAULT_ATTEMPTS, DEFAULT_SLEEP);
554 }
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572 public static void updateSeriesName(Event event, String organization, User user, ElasticsearchIndex searchIndex,
573 int tries, long sleep) throws SearchIndexException {
574 if (event.getSeriesId() != null) {
575 for (int i = 1; i <= tries; i++) {
576 SearchResult<Series> result = searchIndex.getByQuery(
577 new SeriesSearchQuery(organization, user).withoutActions().withIdentifier(event.getSeriesId()));
578 if (result.getHitCount() > 0) {
579 event.setSeriesName(result.getItems()[0].getSource().getTitle());
580 break;
581 } else {
582 Integer triesLeft = tries - i;
583 logger.debug("Not able to find the series {} in the search index for the event {}. Will try {} more times.",
584 event.getSeriesId(), event.getIdentifier(), triesLeft);
585 try {
586 Thread.sleep(sleep);
587 } catch (InterruptedException e) {
588 logger.warn("Interrupted while sleeping before checking for the series being added to the index", e);
589 }
590 }
591
592 }
593 }
594 }
595
596
597
598
599
600
601
602
603 private static String[] getPublicationFlavors(List<Publication> publications) {
604 Set<String> allPublicationFlavors = new TreeSet<String>();
605 for (Publication p : publications) {
606 for (Attachment attachment : p.getAttachments()) {
607 if (attachment.getFlavor() != null) {
608 allPublicationFlavors.add(attachment.getFlavor().toString());
609 }
610 }
611 for (Catalog catalog : p.getCatalogs()) {
612 if (catalog.getFlavor() != null) {
613 allPublicationFlavors.add(catalog.getFlavor().toString());
614 }
615 }
616 for (Track track : p.getTracks()) {
617 if (track.getFlavor() != null) {
618 allPublicationFlavors.add(track.getFlavor().toString());
619 }
620 }
621 }
622 return allPublicationFlavors.toArray(new String[allPublicationFlavors.size()]);
623 }
624
625
626
627
628
629
630
631
632 public static Boolean subflavorMatches(List<Publication> publications, String previewSubtype) {
633 String[] publicationFlavors = getPublicationFlavors(publications);
634 if (publicationFlavors != null && previewSubtype != null) {
635 final String subtype = "/" + previewSubtype;
636 for (String flavor : publicationFlavors) {
637 if (flavor.endsWith(subtype)) {
638 return true;
639 }
640 }
641 }
642 return false;
643 }
644
645 }