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) 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().getOrElse(-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().getOrElseNull();
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
207 final List<Predicate> predicates = new ArrayList<>();
208 predicates.add(cb.equal(c.get("organization"), getSecurityService().getOrganization().getId()));
209
210 for (String p : query.getMediaPackageId())
211 predicates.add(cb.equal(c.get("mediaPackageId"), p));
212 for (String p : query.getRepositoryId())
213 predicates.add(cb.equal(c.get("repositoryId"), p));
214 for (String p : query.getSeriesId())
215 predicates.add(cb.equal(c.get("series"), p));
216 for (Boolean p : query.isDeleted())
217 predicates.add(cb.equal(c.get("deleted"), p));
218 if (query.isSubsequentRequest()) {
219 for (Date p : query.getModifiedAfter())
220 predicates.add(cb.greaterThan(c.get("modificationDate").as(Date.class), p));
221 } else {
222 for (Date p : query.getModifiedAfter())
223 predicates.add(cb.greaterThanOrEqualTo(c.get("modificationDate").as(Date.class), p));
224 }
225 for (Date p : query.getModifiedBefore())
226 predicates.add(cb.lessThanOrEqualTo(c.get("modificationDate").as(Date.class), p));
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 for (int startPosition : query.getOffset()) {
236 logger.warn("I'm pretty sure things break if this is used");
237 typedQuery.setFirstResult(startPosition);
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
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
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
270
271
272
273
274
275 protected boolean matchSetDef(OaiPmhSetDefinition setDef, List<SearchResultElementItem> elements) {
276
277 for (OaiPmhSetDefinitionFilter filter : setDef.getFilters()) {
278 if (!matchSetDefFilter(filter, elements)) {
279 return false;
280 }
281 }
282 return true;
283 }
284
285
286
287
288
289
290
291
292 private boolean matchSetDefFilter(OaiPmhSetDefinitionFilter filter, List<SearchResultElementItem> elements) {
293
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;
320 for (SearchResultElementItem element : elements) {
321 if (!StringUtils.equals(filter.getFlavor(), element.getFlavor())) {
322 continue;
323 }
324
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
342
343
344
345
346
347
348
349
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
364
365
366
367
368
369 private SearchResult createSearchResult(TypedQuery<OaiPmhEntity> query) {
370
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 }