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.search.api;
23
24 import org.opencastproject.mediapackage.EName;
25 import org.opencastproject.mediapackage.MediaPackage;
26 import org.opencastproject.mediapackage.MediaPackageException;
27 import org.opencastproject.mediapackage.MediaPackageParser;
28 import org.opencastproject.metadata.dublincore.DublinCore;
29 import org.opencastproject.metadata.dublincore.DublinCoreCatalog;
30 import org.opencastproject.metadata.dublincore.DublinCoreValue;
31 import org.opencastproject.metadata.dublincore.DublinCores;
32 import org.opencastproject.metadata.dublincore.OpencastDctermsDublinCore;
33 import org.opencastproject.security.api.AccessControlEntry;
34 import org.opencastproject.security.api.AccessControlList;
35
36 import com.google.gson.Gson;
37
38 import org.elasticsearch.index.mapper.DateFieldMapper;
39
40 import java.time.Instant;
41 import java.time.ZoneOffset;
42 import java.time.format.DateTimeFormatter;
43 import java.util.Date;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.LinkedList;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.stream.Collectors;
51
52 public class SearchResult {
53
54 public static final String TYPE = "type";
55 public static final String MEDIAPACKAGE = "mediapackage";
56 public static final String MEDIAPACKAGE_XML = "mediapackage_xml";
57 public static final String DUBLINCORE = "dc";
58 public static final String ORG = "org";
59 public static final String MODIFIED_DATE = "modified";
60 public static final String DELETED_DATE = "deleted";
61 public static final String INDEX_ACL = "searchable_acl";
62 public static final String REST_ACL = "acl";
63 public static final String LIVE = "live";
64
65 private static final Gson gson = new Gson();
66
67 private SearchService.IndexEntryType type;
68
69 private MediaPackage mp;
70
71 private DublinCoreCatalog dublinCore;
72
73 private AccessControlList acl;
74
75 private String orgId;
76
77 private String id = null;
78
79 private Boolean live = null;
80
81 private Instant modified = null;
82
83 private Instant deleted = null;
84
85 public SearchResult(SearchService.IndexEntryType type, DublinCoreCatalog dc, AccessControlList acl,
86 String orgId, MediaPackage mp, Instant modified, Instant deleted) {
87 this.type = type;
88 this.dublinCore = dc;
89 this.acl = acl;
90 this.orgId = orgId;
91 this.mp = mp;
92 this.deleted = deleted;
93 this.modified = modified;
94
95 if (SearchService.IndexEntryType.Episode.equals(type)) {
96 this.id = this.getMediaPackage().getIdentifier().toString();
97 this.live = this.getMediaPackage().isLive();
98 } else if (SearchService.IndexEntryType.Series.equals(type)) {
99 this.id = this.dublinCore.getFirst(DublinCore.PROPERTY_IDENTIFIER);
100 }
101 }
102
103 public Date getModifiedDate() {
104 return new Date(this.modified.toEpochMilli());
105 }
106
107 public String getId() {
108 return this.id;
109 }
110
111 public Boolean getLive() {
112 return this.live;
113 }
114
115 public Date getDeletionDate() {
116 return null == this.deleted ? null : new Date(this.deleted.toEpochMilli());
117 }
118
119 @SuppressWarnings("unchecked")
120 public static SearchResult rehydrate(Map<String, Object> data) throws SearchException {
121
122
123 try {
124
125
126 SearchService.IndexEntryType type = SearchService.IndexEntryType.valueOf((String) data.get(TYPE));
127 DublinCoreCatalog dc = rehydrateDC(type, (Map<String, Object>) data.get(DUBLINCORE));
128 AccessControlList acl = rehydrateACL((Map<String, Object>) data.get(INDEX_ACL));
129 String org = (String) data.get(ORG);
130
131 Instant deleted = null;
132 if (data.containsKey(DELETED_DATE) && null != data.get(DELETED_DATE)) {
133 deleted = Instant.parse((String) data.get(DELETED_DATE));
134 }
135
136 Instant modified = null;
137 if (data.containsKey(MODIFIED_DATE) && !data.get(MODIFIED_DATE).equals("null")) {
138 modified = Instant.parse((String) data.get(MODIFIED_DATE));
139 }
140
141
142 MediaPackage mp = null;
143
144 if (SearchService.IndexEntryType.Episode.equals(type)) {
145 mp = MediaPackageParser.getFromXml((String) data.get(MEDIAPACKAGE_XML));
146 }
147 return new SearchResult(type, dc, acl, org, mp, modified, deleted);
148 } catch (MediaPackageException e) {
149 throw new SearchException(e);
150 }
151 }
152
153 public static Map<String, List<String>> dehydrateDC(DublinCoreCatalog dublinCoreCatalog) {
154 var metadata = new HashMap<String, List<String>>();
155 for (var entry : dublinCoreCatalog.getValues().entrySet()) {
156 var key = entry.getKey().getLocalName();
157 var values = entry.getValue().stream()
158 .map(DublinCoreValue::getValue)
159 .map(val -> {
160
161 if (entry.getKey().equals(DublinCore.PROPERTY_CREATED)) {
162 var date = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.parse(val);
163 return DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.withZone(ZoneOffset.UTC).format(date);
164 } else {
165 return val;
166 }
167 })
168 .collect(Collectors.toList());
169 metadata.put(key, values);
170 }
171
172 return metadata;
173 }
174
175
176
177
178
179
180 public static Map<String, Set<String>> dehydrateAclForIndex(AccessControlList acl) {
181 var result = new HashMap<String, Set<String>>();
182 for (var entry : acl.getEntries()) {
183 var action = entry.getAction();
184 if (!result.containsKey(action)) {
185 result.put(action, new HashSet<>());
186 }
187 result.get(action).add(entry.getRole());
188 }
189 return result;
190 }
191
192 public static List<Map<String, ?>> dehydrateAclForREST(AccessControlList acl) {
193 return acl.getEntries().stream()
194 .map(ace -> Map.of("action", ace.getAction(), "role", ace.getRole(), "allow", Boolean.TRUE))
195 .collect(Collectors.toList());
196 }
197
198 @SuppressWarnings("unchecked")
199 public static AccessControlList rehydrateACL(Map<String, Object> map) {
200 List<AccessControlEntry> aces = new LinkedList<>();
201 for (var entry : map.entrySet()) {
202 String action = entry.getKey();
203 for (String rolename : (List<String>) entry.getValue()) {
204 AccessControlEntry ace = new AccessControlEntry(rolename, action, true);
205 aces.add(ace);
206 }
207 }
208 return new AccessControlList(aces);
209 }
210
211 @SuppressWarnings("unchecked")
212 public static DublinCoreCatalog rehydrateDC(SearchService.IndexEntryType type, Map<String, Object> map)
213 throws SearchException {
214 OpencastDctermsDublinCore dc;
215 if (SearchService.IndexEntryType.Episode.equals(type)) {
216 dc = DublinCores.mkOpencastEpisode();
217 } else if (SearchService.IndexEntryType.Series.equals(type)) {
218 dc = DublinCores.mkOpencastSeries();
219 } else {
220 throw new SearchException("Unknown DC type!");
221 }
222 for (var entry: map.entrySet()) {
223 String key = entry.getKey();
224
225 List<String> value = (List<String>) entry.getValue();
226 dc.set(EName.mk(DublinCore.TERMS_NS_URI, key), value);
227 }
228 return dc.getCatalog();
229 }
230
231 public Map<String, Object> dehydrateForIndex() {
232 return dehydrate().entrySet().stream()
233 .filter(entry -> !entry.getKey().equals(REST_ACL))
234 .collect(HashMap::new, (m,v)->m.put(v.getKey(), v.getValue()), HashMap::putAll);
235 }
236
237 public Map<String, Object> dehydrateForREST() {
238 return dehydrate().entrySet().stream()
239 .filter(entry -> !entry.getKey().equals(INDEX_ACL))
240 .filter(entry -> !entry.getKey().equals(MEDIAPACKAGE_XML))
241 .collect(HashMap::new, (m,v)->m.put(v.getKey(), v.getValue()), HashMap::putAll);
242 }
243
244 public Map<String, Object> dehydrate() {
245 if (SearchService.IndexEntryType.Episode.equals(getType())) {
246 return dehydrateEpisode();
247 } else if (SearchService.IndexEntryType.Series.equals(getType())) {
248 return dehydrateSeries();
249 }
250 return null;
251 }
252
253 public Map<String, Object> dehydrateEpisode() {
254
255 var ret = new HashMap<>(Map.of(
256 MEDIAPACKAGE, gson.fromJson(MediaPackageParser.getAsJSON(this.mp), Map.class).get(MEDIAPACKAGE),
257 MEDIAPACKAGE_XML, MediaPackageParser.getAsXml(this.mp),
258 INDEX_ACL, SearchResult.dehydrateAclForIndex(acl),
259 REST_ACL, SearchResult.dehydrateAclForREST(acl),
260 DUBLINCORE, SearchResult.dehydrateDC(this.dublinCore),
261 ORG, this.orgId,
262 TYPE, this.type.name(),
263 MODIFIED_DATE, DateTimeFormatter.ISO_INSTANT.format(this.modified),
264 LIVE, this.live));
265
266 ret.put(DELETED_DATE, null == this.deleted ? null : DateTimeFormatter.ISO_INSTANT.format(this.deleted));
267
268 return ret;
269 }
270
271 public Map<String, Object> dehydrateSeries() {
272
273 var ret = new HashMap<>(Map.of(
274 INDEX_ACL, SearchResult.dehydrateAclForIndex(acl),
275 REST_ACL, SearchResult.dehydrateAclForREST(acl),
276 DUBLINCORE, SearchResult.dehydrateDC(this.dublinCore),
277 ORG, this.orgId,
278 TYPE, this.type.name(),
279 MODIFIED_DATE, DateTimeFormatter.ISO_INSTANT.format(this.modified)));
280
281 ret.put(DELETED_DATE, null == this.deleted ? null : DateTimeFormatter.ISO_INSTANT.format(this.deleted));
282
283 return ret;
284 }
285
286 public DublinCoreCatalog getDublinCore() {
287 return this.dublinCore;
288 }
289
290 public AccessControlList getAcl() {
291 return acl;
292 }
293
294 public MediaPackage getMediaPackage() {
295 return mp;
296 }
297
298 public SearchService.IndexEntryType getType() {
299 return type;
300 }
301
302 public Instant getCreatedDate() {
303 var acc = DateFieldMapper.DEFAULT_DATE_TIME_FORMATTER.parse(this.dublinCore.getFirst(DublinCore.PROPERTY_CREATED));
304 return Instant.from(acc);
305 }
306
307 public String getOrgId() {
308 return orgId;
309 }
310 }