/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.feed.impl; import org.opencastproject.feed.api.Feed.Type; import org.opencastproject.feed.api.FeedGenerator; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.metadata.dublincore.DublinCore; import org.opencastproject.metadata.dublincore.DublinCoreCatalog; import org.opencastproject.metadata.dublincore.EncodingSchemeUtils; import org.opencastproject.search.api.MediaSegment; import org.opencastproject.search.api.MediaSegmentImpl; import org.opencastproject.search.api.SearchQuery; import org.opencastproject.search.api.SearchResult; import org.opencastproject.search.api.SearchResultImpl; import org.opencastproject.search.api.SearchResultItem; import org.opencastproject.search.api.SearchResultItemImpl; import org.opencastproject.util.data.Function; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.Properties; import java.util.concurrent.TimeUnit; /** * This feed generator implements a feed for series. The series argument is taken from the first url parameter after the * feed type and version, and {@link #accept(String[])} returns <code>true</code> if the search service returns a result * for that series identifier. */ public class SeriesFeedService extends AbstractFeedService implements FeedGenerator { /** The logging facility */ private static final Logger logger = LoggerFactory.getLogger(SeriesFeedService.class); /** The series identifier */ protected ThreadLocal<String> series = new ThreadLocal<String>(); /** The series data */ protected ThreadLocal<SearchResult> seriesData = new ThreadLocal<SearchResult>(); /** Number of milliseconds to cache the recordings (1h) */ private static final long SERIES_CACHE_TIME = 60L * 60L * 1000L; /** A token to store in the miss cache */ private Object nullToken = new Object(); private final CacheLoader<String, Object> seriesLoader = new CacheLoader<String, Object>() { @Override public Object load(String id) { SearchResult result = loadSeries.apply(id); return result == null ? nullToken : result; } }; /** The series metadata, cached for up to the indicated amount of time */ private final LoadingCache<String, Object> seriesCache = CacheBuilder.newBuilder() .expireAfterWrite(SERIES_CACHE_TIME, TimeUnit.MILLISECONDS).maximumSize(500).build(seriesLoader); /** * @see org.opencastproject.feed.api.FeedGenerator#accept(java.lang.String[]) */ @Override public boolean accept(String[] query) { boolean generalChecksPassed = super.accept(query); if (!generalChecksPassed) return false; // Build the series id, first parameter is the selector. Note that if the series identifier // contained slashes (e. g. in the case of a handle or doi), we need to reassemble the // identifier StringBuffer sId = new StringBuffer(); int idparts = query.length - 1; if (idparts < 1) return false; for (int i = 1; i <= idparts; i++) { if (sId.length() > 0) sId.append("/"); sId.append(query[i]); } // Remember the series id final String seriesId = sId.toString(); series.set(seriesId); try { // To check if we can accept the query it is enough to query for just one result // Check the series service to see if the series exists // but has not yet had anything published from it Object result = seriesCache.getUnchecked(seriesId); if (result == nullToken) return false; SearchResult searchResult = (SearchResult) result; seriesData.set(searchResult); return searchResult.size() > 0; } catch (Exception e) { return false; } } /** * {@inheritDoc} * * @see org.opencastproject.feed.impl.AbstractFeedGenerator#getIdentifier() */ @Override public String getIdentifier() { return series.get() != null ? series.get() : super.getIdentifier(); } /** * {@inheritDoc} * * @see org.opencastproject.feed.impl.AbstractFeedGenerator#getName() */ @Override public String getName() { SearchResult rs = seriesData.get(); return (rs != null) ? rs.getItems()[0].getDcTitle() : super.getName(); } /** * {@inheritDoc} * * @see org.opencastproject.feed.impl.AbstractFeedGenerator#getDescription() */ @Override public String getDescription() { String dcAbstract = null; SearchResult rs = seriesData.get(); if (rs != null) { dcAbstract = rs.getItems()[0].getDcAbstract(); } return dcAbstract == null ? super.getDescription() : dcAbstract; } /** * {@inheritDoc} * * @see org.opencastproject.feed.impl.AbstractFeedGenerator#loadFeedData(org.opencastproject.feed.api.Feed.Type, * java.lang.String[], int, int) */ @Override protected SearchResult loadFeedData(Type type, String[] query, int limit, int offset) { SearchQuery q = createBaseQuery(type, limit, offset); q.includeEpisodes(true); q.includeSeries(false); q.withSeriesId(series.get()); return searchService.getByQuery(q); } /** * {@inheritDoc} * * @see org.opencastproject.feed.impl.AbstractFeedService#initialize(java.util.Properties) */ @Override public void initialize(Properties properties) { super.initialize(properties); // Clear the selector, since super.accept() relies on the fact that it's not set setSelector(null); } /** * Find if a series exists in the seriesService and return it's dublinCore as a search result. This call should be * used when searchService returns null as a series may exist but not have had any episodes yet published. * * @param id * the series to lookup * @return search result, null if series not found */ private final Function<String, SearchResult> loadSeries = new Function<String, SearchResult>() { @Override public SearchResult apply(String id) { // Try to look up the series from the search service SearchQuery q = new SearchQuery(); q.includeEpisodes(false); q.includeSeries(true); q.withId(id); SearchResult result = searchService.getByQuery(q); if (result.getItems().length > 0) { logger.trace("Metadata for series {} loaded from search service", id); return result; } // There is nothing in the search index, let's ask the series service try { logger.debug("Loading metadata for series {} from series service", id); final DublinCoreCatalog seriesDublinCore = seriesService.getSeries(id); SearchResultImpl artificialResult = new SearchResultImpl(); // Response either finds the one series or nothing at all artificialResult.setLimit(1); artificialResult.setOffset(0); artificialResult.setTotal(1); SearchResultItemImpl item = SearchResultItemImpl.fill(new SearchResultItem() { @Override public String getId() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_IDENTIFIER); } @Override public String getOrganization() { return null; } @Override public MediaPackage getMediaPackage() { return null; } @Override public long getDcExtent() { return -1; } @Override public String getDcTitle() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_TITLE); } @Override public String getDcSubject() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_SUBJECT); } @Override public String getDcDescription() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_DESCRIPTION); } @Override public String getDcCreator() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_CREATOR); } @Override public String getDcPublisher() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_PUBLISHER); } @Override public String getDcContributor() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_CONTRIBUTOR); } @Override public String getDcAbstract() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_ABSTRACT); } @Override public Date getDcCreated() { String date = seriesDublinCore.getFirst(DublinCore.PROPERTY_CREATED); if (date != null) { return EncodingSchemeUtils.decodeDate(date); } return null; } @Override public Date getDcAvailableFrom() { return null; } @Override public Date getDcAvailableTo() { return null; } @Override public String getDcLanguage() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_LANGUAGE); } @Override public String getDcRightsHolder() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_RIGHTS_HOLDER); } @Override public String getDcSpatial() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_SPATIAL); } @Override public String getDcTemporal() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_TEMPORAL); } @Override public String getDcIsPartOf() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_IS_PART_OF); } @Override public String getDcReplaces() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_REPLACES); } @Override public String getDcType() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_TYPE); } @Override public String getDcAccessRights() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_ACCESS_RIGHTS); } @Override public String getDcLicense() { return seriesDublinCore.getFirst(DublinCore.PROPERTY_LICENSE); } @Override public String getOcMediapackage() { return null; } @Override public SearchResultItem.SearchResultItemType getType() { return SearchResultItemType.Series; } @Override public String[] getKeywords() { return new String[0]; } @Override public String getCover() { return null; } @Override public Date getModified() { return null; } @Override public double getScore() { return 0.0; } @Override public MediaSegment[] getSegments() { return new MediaSegmentImpl[0]; } }); artificialResult.addItem(item); return artificialResult; } catch (Exception e) { return null; } } }; }