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  
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     //Our sole parameter here is a map containing a mix of string:string pairs, and String:Map<String, Object> pairs
114 
115     try {
116       // We're *really* hoping that no one feeds us things that aren't what we expect
117       // but ES results come back in json, and get turned into multi-layered Maps
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       //There had better be a mediapackage with an episode...
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                 // Normalize `created` field: we want it to be in ISO 8601 format in UTC.
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    * Simplify ACL structure, so we can easily search by action.
169    * @param acl The access control List to restructure
170    * @return Restructured ACL
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       //This is *always* a list, per dehydrateACL
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 }