View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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     //Our sole parameter here is a map containing a mix of string:string pairs, and String:Map<String, Object> pairs
122 
123     try {
124       // We're *really* hoping that no one feeds us things that aren't what we expect
125       // but ES results come back in json, and get turned into multi-layered Maps
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       //There had better be a mediapackage with an episode...
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                 // Normalize `created` field: we want it to be in ISO 8601 format in UTC.
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    * Simplify ACL structure, so we can easily search by action.
177    * @param acl The access control List to restructure
178    * @return Restructured ACL
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       //This is *always* a list, per dehydrateACL
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 }