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  package org.opencastproject.oaipmh.persistence.impl;
22  
23  import org.opencastproject.db.DBSession;
24  import org.opencastproject.mediapackage.MediaPackage;
25  import org.opencastproject.mediapackage.MediaPackageElement;
26  import org.opencastproject.mediapackage.MediaPackageParser;
27  import org.opencastproject.oaipmh.persistence.OaiPmhDatabase;
28  import org.opencastproject.oaipmh.persistence.OaiPmhDatabaseException;
29  import org.opencastproject.oaipmh.persistence.OaiPmhElementEntity;
30  import org.opencastproject.oaipmh.persistence.OaiPmhEntity;
31  import org.opencastproject.oaipmh.persistence.OaiPmhSetDefinition;
32  import org.opencastproject.oaipmh.persistence.OaiPmhSetDefinitionFilter;
33  import org.opencastproject.oaipmh.persistence.Query;
34  import org.opencastproject.oaipmh.persistence.SearchResult;
35  import org.opencastproject.oaipmh.persistence.SearchResultElementItem;
36  import org.opencastproject.oaipmh.persistence.SearchResultItem;
37  import org.opencastproject.security.api.SecurityService;
38  import org.opencastproject.util.MimeTypes;
39  import org.opencastproject.util.NotFoundException;
40  import org.opencastproject.util.XmlUtil;
41  import org.opencastproject.workspace.api.Workspace;
42  
43  import org.apache.commons.io.IOUtils;
44  import org.apache.commons.lang3.StringUtils;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  import java.io.InputStream;
49  import java.util.ArrayList;
50  import java.util.Arrays;
51  import java.util.Date;
52  import java.util.List;
53  import java.util.concurrent.locks.ReadWriteLock;
54  import java.util.concurrent.locks.ReentrantReadWriteLock;
55  import java.util.regex.Pattern;
56  
57  import javax.persistence.EntityManager;
58  import javax.persistence.NoResultException;
59  import javax.persistence.TypedQuery;
60  import javax.persistence.criteria.CriteriaBuilder;
61  import javax.persistence.criteria.CriteriaQuery;
62  import javax.persistence.criteria.Predicate;
63  import javax.persistence.criteria.Root;
64  
65  public abstract class AbstractOaiPmhDatabase implements OaiPmhDatabase {
66    /** Logging utilities */
67    private static final Logger logger = LoggerFactory.getLogger(AbstractOaiPmhDatabase.class);
68  
69    private ReadWriteLock dbAccessLock = new ReentrantReadWriteLock();
70  
71    public abstract DBSession getDBSession();
72  
73    public abstract SecurityService getSecurityService();
74  
75    public abstract Workspace getWorkspace();
76  
77    @Override
78    public void store(MediaPackage mediaPackage, String repository) throws OaiPmhDatabaseException {
79      try {
80        dbAccessLock.writeLock().lock();
81        storeInternal(mediaPackage, repository);
82      } finally {
83        dbAccessLock.writeLock().unlock();
84      }
85    }
86  
87    private void storeInternal(MediaPackage mediaPackage, String repository) throws OaiPmhDatabaseException {
88      try {
89        getDBSession().execTx(em -> {
90          OaiPmhEntity entity = getOaiPmhEntity(mediaPackage.getIdentifier().toString(), repository, em);
91          if (entity == null) {
92            // no entry found, create new entity
93            entity = new OaiPmhEntity();
94            updateEntity(entity, mediaPackage, repository);
95            em.persist(entity);
96          } else {
97            // entry found, update existing
98            updateEntity(entity, mediaPackage, repository);
99            em.merge(entity);
100         }
101       });
102     } catch (Exception e) {
103       logger.error("Could not store mediapackage '{}' to OAI-PMH repository '{}'", mediaPackage.getIdentifier(),
104             repository, e);
105       throw new OaiPmhDatabaseException(e);
106     }
107   }
108 
109   public void updateEntity(OaiPmhEntity entity, MediaPackage mediaPackage, String repository) {
110     entity.setOrganization(getSecurityService().getOrganization().getId());
111     entity.setDeleted(false);
112     entity.setRepositoryId(repository);
113 
114     entity.setMediaPackageId(mediaPackage.getIdentifier().toString());
115     entity.setMediaPackageXML(MediaPackageParser.getAsXml(mediaPackage));
116     entity.setSeries(mediaPackage.getSeries());
117     entity.removeAllMediaPackageElements();
118 
119 
120     for (MediaPackageElement mpe : mediaPackage.getElements()) {
121       if (mpe.getFlavor() == null) {
122         logger.debug("A flavor must be set on media package elements for publishing");
123         continue;
124       }
125 
126       if (mpe.getElementType() != MediaPackageElement.Type.Catalog
127               && mpe.getElementType() != MediaPackageElement.Type.Attachment) {
128         logger.debug("Only catalog and attachment types are currently supported");
129         continue;
130       }
131 
132       if (mpe.getMimeType() == null || !mpe.getMimeType().eq(MimeTypes.XML)) {
133         logger.debug("Only media package elements with mime type XML are supported");
134         continue;
135       }
136       String catalogXml = null;
137       try (InputStream in = getWorkspace().read(mpe.getURI())) {
138         catalogXml = IOUtils.toString(in, "UTF-8");
139       } catch (Throwable e) {
140         logger.warn("Unable to load catalog {} from media package {}",
141                 mpe.getIdentifier(), mediaPackage.getIdentifier().toString(), e);
142         continue;
143       }
144       if (catalogXml == null || StringUtils.isBlank(catalogXml) || !XmlUtil.parseNs(catalogXml).isRight()) {
145         logger.warn("The catalog {} from media package {} isn't a well formatted XML document",
146                 mpe.getIdentifier(), mediaPackage.getIdentifier().toString());
147         continue;
148       }
149 
150       entity.addMediaPackageElement(new OaiPmhElementEntity(
151               mpe.getElementType().name(), mpe.getFlavor().toString(), catalogXml));
152     }
153   }
154 
155   @Override
156   public void delete(String mediaPackageId, String repository) throws OaiPmhDatabaseException, NotFoundException {
157     try {
158       dbAccessLock.writeLock().lock();
159       deleteInternal(mediaPackageId, repository);
160     } finally {
161       dbAccessLock.writeLock().unlock();
162     }
163   }
164 
165   private void deleteInternal(String mediaPackageId, String repository)
166           throws OaiPmhDatabaseException, NotFoundException {
167     try {
168       getDBSession().execTxChecked(em -> {
169         OaiPmhEntity oaiPmhEntity = getOaiPmhEntity(mediaPackageId, repository, em);
170         if (oaiPmhEntity == null) {
171           throw new NotFoundException("No media package with id " + mediaPackageId + " exists");
172         }
173 
174         oaiPmhEntity.setDeleted(true);
175         em.merge(oaiPmhEntity);
176       });
177     } catch (NotFoundException e) {
178       throw e;
179     } catch (Exception e) {
180       logger.error("Could not delete mediapackage '{}' from OAI-PMH repository '{}'", mediaPackageId, repository, e);
181       throw new OaiPmhDatabaseException(e);
182     }
183   }
184 
185   @Override
186   public SearchResult search(Query query) {
187     try {
188       final int chunkSize = query.getLimit().orElse(-1);
189       dbAccessLock.readLock().lock();
190       return searchInternal(query, chunkSize);
191     } finally {
192       dbAccessLock.readLock().unlock();
193     }
194   }
195 
196   private SearchResult searchInternal(Query query, int chunkSize) {
197     final String requestSetSpec = query.getSetSpec().orElse(null);
198     Date lastDate = new Date();
199     long resultSize;
200     long resultOffset;
201     long resultLimit;
202     SearchResult result = getDBSession().exec(em -> {
203       CriteriaBuilder cb = em.getCriteriaBuilder();
204       CriteriaQuery<OaiPmhEntity> q = cb.createQuery(OaiPmhEntity.class);
205       Root<OaiPmhEntity> c = q.from(OaiPmhEntity.class);
206       q.select(c);
207 
208       // create predicates joined in an "and" expression
209       final List<Predicate> predicates = new ArrayList<>();
210       predicates.add(cb.equal(c.get("organization"), getSecurityService().getOrganization().getId()));
211 
212       if (query.getMediaPackageId().isPresent()) {
213         predicates.add(cb.equal(c.get("mediaPackageId"), query.getMediaPackageId().get()));
214       }
215       if (query.getRepositoryId().isPresent()) {
216         predicates.add(cb.equal(c.get("repositoryId"), query.getRepositoryId().get()));
217       }
218       if (query.getSeriesId().isPresent()) {
219         predicates.add(cb.equal(c.get("series"), query.getSeriesId().get()));
220       }
221       if (query.isDeleted().isPresent()) {
222         predicates.add(cb.equal(c.get("deleted"), query.isDeleted().get()));
223       }
224       if (query.isSubsequentRequest()) {
225         if (query.getModifiedAfter().isPresent()) {
226           predicates.add(cb.greaterThan(c.get("modificationDate").as(Date.class), query.getModifiedAfter().get()));
227         }
228       } else {
229         if (query.getModifiedAfter().isPresent()) {
230           predicates.add(cb.greaterThanOrEqualTo(c.get("modificationDate").as(Date.class),
231               query.getModifiedAfter().get()));
232         }
233       }
234       if (query.getModifiedBefore().isPresent()) {
235         predicates.add(cb.lessThanOrEqualTo(c.get("modificationDate").as(Date.class), query.getModifiedBefore().get()));
236       }
237 
238       q.where(cb.and(predicates.toArray(new Predicate[0])));
239       q.orderBy(cb.asc(c.get("modificationDate")));
240 
241       TypedQuery<OaiPmhEntity> typedQuery = em.createQuery(q);
242       if (chunkSize > 0) {
243         typedQuery.setMaxResults(chunkSize);
244       }
245       if (query.getOffset().isPresent()) {
246         logger.warn("I'm pretty sure things break if this is used");
247         typedQuery.setFirstResult(query.getOffset().get());
248       }
249 
250       return createSearchResult(typedQuery);
251     });
252 
253     if (requestSetSpec == null) {
254       return new SearchResultImpl(result.getOffset(), result.getLimit(), result.getItems());
255     }
256 
257     // If we are here, setSpec request is ongoing
258     final List<SearchResultItem> filteredItems = new ArrayList<>();
259     for (SearchResultItem item : result.getItems()) {
260       for (OaiPmhSetDefinition setDef : query.getSetDefinitions()) {
261         if (matchSetDef(setDef, item.getElements())) {
262           item.addSetSpec(setDef.getSetSpec());
263         }
264       }
265       if (item.getSetSpecs().contains(requestSetSpec)) {
266         filteredItems.add(item);
267       } else {
268         // If the setSpec does not match, we should mark this item as deleted for this specific setSpec
269         filteredItems.add(new SearchResultItemImpl(item.getId(), item.getMediaPackageXml(), item.getOrganization(),
270             item.getRepository(), item.getModificationDate(), true, item.getMediaPackage(),
271             item.getElements(), Arrays.asList(requestSetSpec)));
272       }
273 
274     }
275     return new SearchResultImpl(result.getOffset(), result.getLimit(), filteredItems);
276   }
277 
278   /**
279    * Returns true if all set definition filters matches.
280    *
281    * @param setDef set definition to test
282    * @param elements media package elements to test
283    * @return returns true if all set definition filters matches, otherwise false
284    */
285   protected boolean matchSetDef(OaiPmhSetDefinition setDef, List<SearchResultElementItem> elements) {
286     // all filters should match
287     for (OaiPmhSetDefinitionFilter filter : setDef.getFilters()) {
288       if (!matchSetDefFilter(filter, elements)) {
289         return false;
290       }
291     }
292     return true;
293   }
294 
295   /**
296    * Returns true if any filter criterion matches
297    *
298    * @param filter filter to test
299    * @param elements media package elements to test filter criteria on
300    * @return true if any filter criteria matches, otherwise false
301    */
302   private boolean matchSetDefFilter(OaiPmhSetDefinitionFilter filter, List<SearchResultElementItem> elements) {
303     // At least one filter criterion should match
304     for (String criterion : filter.getCriteria().keySet()) {
305       if (StringUtils.equals(OaiPmhSetDefinitionFilter.CRITERION_CONTAINS, criterion)) {
306         for (SearchResultElementItem element : elements) {
307           if (!StringUtils.equals(filter.getFlavor(), element.getFlavor())) {
308             continue;
309           }
310           for (String criterionValue : filter.getCriteria().get(criterion)) {
311             if (StringUtils.contains(element.getXml(), criterionValue)) {
312               return true;
313             }
314           }
315         }
316       } else if (StringUtils.equals(OaiPmhSetDefinitionFilter.CRITERION_CONTAINSNOT, criterion)) {
317         for (SearchResultElementItem element : elements) {
318           if (!StringUtils.equals(filter.getFlavor(), element.getFlavor())) {
319             continue;
320           }
321           for (String criterionValue : filter.getCriteria().get(criterion)) {
322             if (!StringUtils.contains(element.getXml(), criterionValue)) {
323               return true;
324             }
325           }
326         }
327       } else if (StringUtils.equals(OaiPmhSetDefinitionFilter.CRITERION_MATCH, criterion)) {
328         for (String criterionValue : filter.getCriteria().get(criterion)) {
329           Pattern matchPattern = null; // wait with initialization until we found an element to test
330           for (SearchResultElementItem element : elements) {
331             if (!StringUtils.equals(filter.getFlavor(), element.getFlavor())) {
332               continue;
333             }
334             // initialize regex pattern once and only if we need it (for performance reasons)
335             if (matchPattern == null) {
336               matchPattern = Pattern.compile(criterionValue);
337             }
338             if (matchPattern.matcher(element.getXml()).find()) {
339               return true;
340             }
341           }
342         }
343       } else {
344         logger.warn("Unknown OAI-PMH set filter criterion '{}'. Ignore it.", criterion);
345       }
346     }
347     return false;
348   }
349 
350   /**
351    * Gets a OAI-PMH entity by it's id, using the current organizational context.
352    *
353    * @param id
354    *          the media package identifier
355    * @param repository
356    *          the OAI-PMH repository
357    * @param em
358    *          an open entity manager
359    * @return the OAI-PMH entity, or null if not found
360    */
361   private OaiPmhEntity getOaiPmhEntity(String id, String repository, EntityManager em) {
362     final String orgId = getSecurityService().getOrganization().getId();
363     javax.persistence.Query q = em.createNamedQuery("OaiPmh.findById").setParameter("mediaPackageId", id)
364             .setParameter("repository", repository).setParameter("organization", orgId);
365     try {
366       return (OaiPmhEntity) q.getSingleResult();
367     } catch (NoResultException e) {
368       return null;
369     }
370   }
371 
372   /**
373    * Creates a search result from a given JPA query
374    *
375    * @param query
376    *          the query
377    * @return The search result.
378    */
379   private SearchResult createSearchResult(TypedQuery<OaiPmhEntity> query) {
380     // Create and configure the query result
381     final long offset = query.getFirstResult();
382     final long limit = query.getMaxResults() != Integer.MAX_VALUE ? query.getMaxResults() : 0;
383     final List<SearchResultItem> items = new ArrayList<>();
384     for (OaiPmhEntity oaipmhEntity : query.getResultList()) {
385       try {
386         items.add(new SearchResultItemImpl(oaipmhEntity));
387       } catch (Exception ex) {
388         logger.warn("Unable to parse an OAI-PMH database entry", ex);
389       }
390     }
391     return new SearchResultImpl(offset, limit, items);
392   }
393 }