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) throws OaiPmhDatabaseException, NotFoundException {
166     try {
167       getDBSession().execTxChecked(em -> {
168         OaiPmhEntity oaiPmhEntity = getOaiPmhEntity(mediaPackageId, repository, em);
169         if (oaiPmhEntity == null)
170           throw new NotFoundException("No media package with id " + mediaPackageId + " exists");
171 
172         oaiPmhEntity.setDeleted(true);
173         em.merge(oaiPmhEntity);
174       });
175     } catch (NotFoundException e) {
176       throw e;
177     } catch (Exception e) {
178       logger.error("Could not delete mediapackage '{}' from OAI-PMH repository '{}'", mediaPackageId, repository, e);
179       throw new OaiPmhDatabaseException(e);
180     }
181   }
182 
183   @Override
184   public SearchResult search(Query query) {
185     try {
186       final int chunkSize = query.getLimit().orElse(-1);
187       dbAccessLock.readLock().lock();
188       return searchInternal(query, chunkSize);
189     } finally {
190       dbAccessLock.readLock().unlock();
191     }
192   }
193 
194   private SearchResult searchInternal(Query query, int chunkSize) {
195     final String requestSetSpec = query.getSetSpec().orElse(null);
196     Date lastDate = new Date();
197     long resultSize;
198     long resultOffset;
199     long resultLimit;
200     SearchResult result = getDBSession().exec(em -> {
201       CriteriaBuilder cb = em.getCriteriaBuilder();
202       CriteriaQuery<OaiPmhEntity> q = cb.createQuery(OaiPmhEntity.class);
203       Root<OaiPmhEntity> c = q.from(OaiPmhEntity.class);
204       q.select(c);
205 
206       // create predicates joined in an "and" expression
207       final List<Predicate> predicates = new ArrayList<>();
208       predicates.add(cb.equal(c.get("organization"), getSecurityService().getOrganization().getId()));
209 
210       if (query.getMediaPackageId().isPresent())
211         predicates.add(cb.equal(c.get("mediaPackageId"), query.getMediaPackageId().get()));
212       if (query.getRepositoryId().isPresent())
213         predicates.add(cb.equal(c.get("repositoryId"), query.getRepositoryId().get()));
214       if (query.getSeriesId().isPresent())
215         predicates.add(cb.equal(c.get("series"), query.getSeriesId().get()));
216       if (query.isDeleted().isPresent())
217         predicates.add(cb.equal(c.get("deleted"), query.isDeleted().get()));
218       if (query.isSubsequentRequest()) {
219         if (query.getModifiedAfter().isPresent())
220           predicates.add(cb.greaterThan(c.get("modificationDate").as(Date.class), query.getModifiedAfter().get()));
221       } else {
222         if (query.getModifiedAfter().isPresent())
223           predicates.add(cb.greaterThanOrEqualTo(c.get("modificationDate").as(Date.class), query.getModifiedAfter().get()));
224       }
225       if (query.getModifiedBefore().isPresent())
226         predicates.add(cb.lessThanOrEqualTo(c.get("modificationDate").as(Date.class), query.getModifiedBefore().get()));
227 
228       q.where(cb.and(predicates.toArray(new Predicate[0])));
229       q.orderBy(cb.asc(c.get("modificationDate")));
230 
231       TypedQuery<OaiPmhEntity> typedQuery = em.createQuery(q);
232       if (chunkSize > 0) {
233         typedQuery.setMaxResults(chunkSize);
234       }
235       if (query.getOffset().isPresent()) {
236         logger.warn("I'm pretty sure things break if this is used");
237         typedQuery.setFirstResult(query.getOffset().get());
238       }
239 
240       return createSearchResult(typedQuery);
241     });
242 
243     if (requestSetSpec == null) {
244       return new SearchResultImpl(result.getOffset(), result.getLimit(), result.getItems());
245     }
246 
247     // If we are here, setSpec request is ongoing
248     final List<SearchResultItem> filteredItems = new ArrayList<>();
249     for (SearchResultItem item : result.getItems()) {
250       for (OaiPmhSetDefinition setDef : query.getSetDefinitions()) {
251         if (matchSetDef(setDef, item.getElements())) {
252           item.addSetSpec(setDef.getSetSpec());
253         }
254       }
255       if (item.getSetSpecs().contains(requestSetSpec)) {
256         filteredItems.add(item);
257       } else {
258         // If the setSpec does not match, we should mark this item as deleted for this specific setSpec
259         filteredItems.add(new SearchResultItemImpl(item.getId(), item.getMediaPackageXml(), item.getOrganization(),
260             item.getRepository(), item.getModificationDate(), true, item.getMediaPackage(),
261             item.getElements(), Arrays.asList(requestSetSpec)));
262       }
263 
264     }
265     return new SearchResultImpl(result.getOffset(), result.getLimit(), filteredItems);
266   }
267 
268   /**
269    * Returns true if all set definition filters matches.
270    *
271    * @param setDef set definition to test
272    * @param elements media package elements to test
273    * @return returns true if all set definition filters matches, otherwise false
274    */
275   protected boolean matchSetDef(OaiPmhSetDefinition setDef, List<SearchResultElementItem> elements) {
276     // all filters should match
277     for (OaiPmhSetDefinitionFilter filter : setDef.getFilters()) {
278       if (!matchSetDefFilter(filter, elements)) {
279         return false;
280       }
281     }
282     return true;
283   }
284 
285   /**
286    * Returns true if any filter criterion matches
287    *
288    * @param filter filter to test
289    * @param elements media package elements to test filter criteria on
290    * @return true if any filter criteria matches, otherwise false
291    */
292   private boolean matchSetDefFilter(OaiPmhSetDefinitionFilter filter, List<SearchResultElementItem> elements) {
293     // At least one filter criterion should match
294     for (String criterion : filter.getCriteria().keySet()) {
295       if (StringUtils.equals(OaiPmhSetDefinitionFilter.CRITERION_CONTAINS, criterion)) {
296         for (SearchResultElementItem element : elements) {
297           if (!StringUtils.equals(filter.getFlavor(), element.getFlavor())) {
298             continue;
299           }
300           for (String criterionValue : filter.getCriteria().get(criterion)) {
301             if (StringUtils.contains(element.getXml(), criterionValue)) {
302               return true;
303             }
304           }
305         }
306       } else if (StringUtils.equals(OaiPmhSetDefinitionFilter.CRITERION_CONTAINSNOT, criterion)) {
307         for (SearchResultElementItem element : elements) {
308           if (!StringUtils.equals(filter.getFlavor(), element.getFlavor())) {
309             continue;
310           }
311           for (String criterionValue : filter.getCriteria().get(criterion)) {
312             if (!StringUtils.contains(element.getXml(), criterionValue)) {
313               return true;
314             }
315           }
316         }
317       } else if (StringUtils.equals(OaiPmhSetDefinitionFilter.CRITERION_MATCH, criterion)) {
318         for (String criterionValue : filter.getCriteria().get(criterion)) {
319           Pattern matchPattern = null; // wait with initialization until we found an element to test
320           for (SearchResultElementItem element : elements) {
321             if (!StringUtils.equals(filter.getFlavor(), element.getFlavor())) {
322               continue;
323             }
324             // initialize regex pattern once and only if we need it (for performance reasons)
325             if (matchPattern == null) {
326               matchPattern = Pattern.compile(criterionValue);
327             }
328             if (matchPattern.matcher(element.getXml()).find()) {
329               return true;
330             }
331           }
332         }
333       } else {
334         logger.warn("Unknown OAI-PMH set filter criterion '{}'. Ignore it.", criterion);
335       }
336     }
337     return false;
338   }
339 
340   /**
341    * Gets a OAI-PMH entity by it's id, using the current organizational context.
342    *
343    * @param id
344    *          the media package identifier
345    * @param repository
346    *          the OAI-PMH repository
347    * @param em
348    *          an open entity manager
349    * @return the OAI-PMH entity, or null if not found
350    */
351   private OaiPmhEntity getOaiPmhEntity(String id, String repository, EntityManager em) {
352     final String orgId = getSecurityService().getOrganization().getId();
353     javax.persistence.Query q = em.createNamedQuery("OaiPmh.findById").setParameter("mediaPackageId", id)
354             .setParameter("repository", repository).setParameter("organization", orgId);
355     try {
356       return (OaiPmhEntity) q.getSingleResult();
357     } catch (NoResultException e) {
358       return null;
359     }
360   }
361 
362   /**
363    * Creates a search result from a given JPA query
364    *
365    * @param query
366    *          the query
367    * @return The search result.
368    */
369   private SearchResult createSearchResult(TypedQuery<OaiPmhEntity> query) {
370     // Create and configure the query result
371     final long offset = query.getFirstResult();
372     final long limit = query.getMaxResults() != Integer.MAX_VALUE ? query.getMaxResults() : 0;
373     final List<SearchResultItem> items = new ArrayList<>();
374     for (OaiPmhEntity oaipmhEntity : query.getResultList()) {
375       try {
376         items.add(new SearchResultItemImpl(oaipmhEntity));
377       } catch (Exception ex) {
378         logger.warn("Unable to parse an OAI-PMH database entry", ex);
379       }
380     }
381     return new SearchResultImpl(offset, limit, items);
382   }
383 }