1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
93 entity = new OaiPmhEntity();
94 updateEntity(entity, mediaPackage, repository);
95 em.persist(entity);
96 } else {
97
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
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
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
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
280
281
282
283
284
285 protected boolean matchSetDef(OaiPmhSetDefinition setDef, List<SearchResultElementItem> elements) {
286
287 for (OaiPmhSetDefinitionFilter filter : setDef.getFilters()) {
288 if (!matchSetDefFilter(filter, elements)) {
289 return false;
290 }
291 }
292 return true;
293 }
294
295
296
297
298
299
300
301
302 private boolean matchSetDefFilter(OaiPmhSetDefinitionFilter filter, List<SearchResultElementItem> elements) {
303
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;
330 for (SearchResultElementItem element : elements) {
331 if (!StringUtils.equals(filter.getFlavor(), element.getFlavor())) {
332 continue;
333 }
334
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
352
353
354
355
356
357
358
359
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
374
375
376
377
378
379 private SearchResult createSearchResult(TypedQuery<OaiPmhEntity> query) {
380
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 }