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