/**
* OLAT - Online Learning and Training
* http://www.olat.org
*
* Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*
* Initial Date: Feb 17, 2009
*
* @author Gregor Wassmann
*/
public abstract class FeedManagerImpl extends FeedManager {
private static final int PICTUREWIDTH = 570; // same as in repository metadata image upload
private RepositoryManager repositoryManager;
private Coordinator coordinator;
private OLATResourceManager resourceManager;
private FileResourceManager fileResourceManager;
private final XStream xstream;
// Better performance when protected (apparently)
protected CacheWrapper feedCache;
private OLog log;
/**
* spring only
*/
protected FeedManagerImpl(OLATResourceManager resourceManager,
FileResourceManager fileResourceManager, CoordinatorManager coordinatorManager) {
this.resourceManager = resourceManager;
this.fileResourceManager = fileResourceManager;
INSTANCE = this;
this.log = getLogger();
xstream = new XStream();
xstream.alias("feed", Feed.class);
xstream.alias("item", Item.class);
this.coordinator = coordinatorManager.getCoordinator();
}
/**
*
* @param repositoryManager
*/
public void setRepositoryManager(RepositoryManager repositoryManager) {
this.repositoryManager = repositoryManager;
}
/**
* Creates a blank feed object and writes it to the (virtual) file system
*
* @see org.olat.modules.webFeed.managers.FeedManager#createPodcastResource()
*/
public OLATResourceable createPodcastResource() {
FeedFileResource podcastResource = new PodcastFileResource();
return createFeedResource(podcastResource);
}
/**
* Creates a blank feed object and writes it to the file system
*
* @see org.olat.modules.webFeed.managers.FeedManager#createPodcastResource()
*/
public OLATResourceable createBlogResource() {
FeedFileResource blogResource = new BlogFileResource();
return createFeedResource(blogResource);
}
/**
* @param feedResource
* @return The feed resourcable after creation on file system
*/
private OLATResourceable createFeedResource(FeedFileResource feedResource) {
OLATResource ores = resourceManager.createOLATResourceInstance(feedResource);
resourceManager.saveOLATResource(ores);
Feed feed = new Feed(feedResource);
VFSContainer podcastContainer = getFeedContainer(feedResource);
VFSLeaf leaf = podcastContainer.createChildLeaf(FEED_FILE_NAME);
podcastContainer.createChildContainer(MEDIA_DIR);
podcastContainer.createChildContainer(ITEMS_DIR);
XStreamHelper.writeObject(xstream, leaf, feed);
return feedResource;
}
/**
* Instanciates or just returns the feed cache. (Protected for better
* performance)
*
* @return The feed cache
*/
protected CacheWrapper initFeedCache() {
if (feedCache == null) {
OLATResourceable ores = OresHelper.createOLATResourceableType(Feed.class);
coordinator.getSyncer().doInSync(ores, new SyncerExecutor() {
@SuppressWarnings("synthetic-access")
public void execute() {
if (feedCache == null) {
feedCache = coordinator.getCacher().getOrCreateCache(this.getClass(), "feed");
}
}
});
}
return feedCache;
}
/**
* @see org.olat.modules.webFeed.managers.FeedManager#delete(org.olat.core.id.OLATResourceable)
*/
@Override
public void delete(OLATResourceable feed) {
fileResourceManager.deleteFileResource(feed);
// Delete comments and ratings
CommentAndRatingService commentAndRatingService = getCommentAndRatingService();
if (commentAndRatingService != null) {
commentAndRatingService.init(null, feed, null, true, false);
commentAndRatingService.deleteAllIgnoringSubPath();
}
//
feed = null;
}
/**
* @see org.olat.modules.webFeed.managers.FeedManager#getFeed(org.olat.core.id.OLATResourceable)
*/
public Feed getFeed(OLATResourceable ores) {
return getFeed(ores, true);
}
/**
* Load the feed and all the items and put it into the cache. If it's already
* in the cache, use the version from the cache. The feed object is shared
* between users.
*
* @param ores
* @param inSync
* @return
*/
private Feed getFeed(OLATResourceable ores, boolean inSync) {
// Attempt to fetch the feed from the cache
Feed myFeed = (Feed) initFeedCache().get(ores.getResourceableId().toString());
if (myFeed == null) {
// Load the feed from file and put it to the cache
VFSContainer feedContainer = getFeedContainer(ores);
myFeed = readFeedFile(feedContainer);
if (myFeed != null) {
// Reset the feed id. (This is necessary for imported feeds.)
myFeed.setId(ores.getResourceableId());
// Load all items
getItems(myFeed);
// See if there are some version issues that need to be fixed now
fixFeedVersionIssues(myFeed);
// Get repository entry information
enrichFeedByRepositoryEntryInfromation(myFeed);
// must be final for sync
final Feed feed = myFeed;
syncedFeedCacheUpdate(feed, inSync);
}
}
return myFeed;
}
/**
* Puts the feed to the feedCache in a synchronized manner.
*
* @param ores
* @param feed
*/
void syncedFeedCacheUpdate(final Feed feed, boolean inSync) {
initFeedCache();
if(inSync) {
coordinator.getSyncer().doInSync(feed, new SyncerExecutor() {
public void execute() {
// update and put behaves the same way
feedCache.update(feed.getResourceableId().toString(), feed);
}
});
} else {
feedCache.update(feed.getResourceableId().toString(), feed);
}
}
/**
* Gets the items of the feed from the feed or load them from the files system.
*
* @param feed
* @return The items of the feed
*/
private List getItems(Feed feed) {
List items = new ArrayList();
if (feed.isExternal() && (feed.getItemIds() == null || feed.getItemIds().size() == 0)) {
items = getItemsFromFeed(feed);
} else if (feed.getItemIds() != null) {
if (feed.getItems() != null && feed.getItems().size() == feed.getItemIds().size()) {
// items already loaded, use the loaded items
items = feed.getItems();
} else {
// reload all items
items = loadItems(feed);
}
}
feed.setItems(items);
return items;
}
/**
* Update the feed resource with the latest set properties in the repository
* entry.
*
* Properties are:
*
*
Title
*
Author
*
Descripion (wiki style in repository)
*
Image
*
*
* @param feed
*/
private void enrichFeedByRepositoryEntryInfromation(Feed feed) {
RepositoryEntry entry = getRepositoryEntry(feed);
if (entry != null && feed != null) {
Date whenTheFeedWasLastModified = feed.getLastModified();
if (whenTheFeedWasLastModified == null || entry.getLastModified().after(whenTheFeedWasLastModified)) {
// entry is newer then feed, update feed
feed.setTitle(entry.getDisplayname());
// Formatter.formatWikiMarkup(entry.getDescription())
feed.setDescription(entry.getDescription());
feed.setAuthor(entry.getInitialAuthor());
// Update the image
VFSContainer repoHome = new LocalFolderImpl(new File(FolderConfig.getCanonicalRoot() + FolderConfig.getRepositoryHome()));
String imageFilename = RepositoryEntryImageController.getImageFilename(entry);
VFSItem repoEntryImage = repoHome.resolve(imageFilename);
if (repoEntryImage != null) {
getFeedMediaContainer(feed).copyFrom(repoEntryImage);
VFSLeaf newImage = (VFSLeaf) getFeedMediaContainer(feed).resolve(imageFilename);
if (newImage != null) {
feed.setImageName(imageFilename);
}
} else {
// There's no repo entry image -> delete the feed image as well.
deleteImage(feed);
}
}
}
}
/**
* @param ores
* @return The repository entry of ores or null
*/
private RepositoryEntry getRepositoryEntry(OLATResourceable ores) {
return repositoryManager.lookupRepositoryEntry(ores, false);
}
/**
* Returns the feed in the given container. It is public to be accessible by
* PodcastFileResource.
*
* Note: this does ONLY read the file from disk, it does NOT put the feed into
* the feed cache nor does it load the associated items. Do not use this
* method generally, use getFeedLight() instead!
*
* @param feedContainer
* @return The Feed upon success
*/
public Feed readFeedFile(VFSContainer feedContainer) {
Feed myFeed = null;
if (feedContainer != null) {
VFSLeaf leaf = (VFSLeaf) feedContainer.resolve(FEED_FILE_NAME);
if (leaf != null) {
myFeed = (Feed) XStreamHelper.readObject(xstream, leaf.getInputStream());
}
} else {
log.error("Feed xml-file could not be found on file system. Feed container: " + feedContainer);
}
return myFeed;
}
/**
* Method that checks the current feed data model version and applies necessary
* fixes to the model. Since feeds can be exported and imported this fixes
* must apply on the fly and can't be implemented with the system upgrade mechanism.
*
* @param feed
*/
private void fixFeedVersionIssues(Feed feed) {
if (feed == null) return;
if (feed.getModelVersion() < 2) {
// The model version of models before the introduction of the model version
// will have a model version=0 (set by xstream)
if (PodcastFileResource.TYPE_NAME.equals(feed.getResourceableTypeName())) {
if (feed.isInternal()) {
// In model 1 the podcast episode items were set as drafts which resulted
// in invisible episodes. They have to be set to published. (OLAT-5767)
for (Item episode : feed.getItems()) {
// Mark episode as published and persist the item file on disk
episode.setDraft(false);
updateItemFileWithoutDoInSync(episode, feed);
}
}
}
// Set feed model to newest version and persist feed file on disk
feed.setModelVersion(Feed.CURRENT_MODEL_VERSION);
VFSContainer container = getFeedContainer(feed);
VFSLeaf leaf = (VFSLeaf) container.resolve(FEED_FILE_NAME);
XStreamHelper.writeObject(xstream, leaf, feed);
//
log.info("Updated feed::" + feed.getResourceableTypeName() + "::" + feed.getResourceableId() + " to version::" + Feed.CURRENT_MODEL_VERSION);
}
}
/**
* Load all items of the feed (from file system or the external feed)
*
* @param feed
*/
public List loadItems(final Feed feed) {
List items = new ArrayList();
if (feed.isExternal()) {
items = getItemsFromFeed(feed);
} else if (feed.isInternal()) {
// Load from virtual file system
VFSContainer itemsContainer = getItemsContainer(feed);
for (String itemId : feed.getItemIds()) {
VFSItem itemContainer = itemsContainer.resolve(itemId);
Item item = loadItem(itemContainer);
if (item != null) {
items.add(item);
}
}
}
// else, this feed is undefined and should have no items. It probably has
// just been created.
feed.setItems(items);
return items;
}
/**
* Read the items of an external feed url
*
* @param feedURL
* @return The list of all items
*/
// ROME library uses untyped lists
@SuppressWarnings("unchecked")
private List getItemsFromFeed(Feed extFeed) {
List items = new ArrayList();
SyndFeed feed = getSyndFeed(extFeed);
if (feed != null) {
List entries = feed.getEntries();
for (SyndEntry entry : entries) {
Item item = convertToItem(entry);
items.add(item);
}
}
return items;
}
/**
* @param extFeed
* @param items
*/
private SyndFeed getSyndFeed(Feed extFeed) {
SyndFeed feed = null;
SyndFeedInput input = new SyndFeedInput();
String feedURL = extFeed.getExternalFeedUrl();
try {
URL url = new URL(feedURL);
feed = input.build(new XmlReader(url));
// also add the external image url just in case we'll need it later
addExternalImageURL(feed, extFeed);
} catch (MalformedURLException e) {
log.info("The externalFeedUrl is invalid: " + feedURL);
} catch (FeedException e) {
log.info("The read feed is invalid: " + feedURL);
} catch (IOException e) {
log.info("Cannot read from feed: " + feedURL);
} finally {
// No streams to be closed
}
return feed;
}
/**
* @param extFeed
* @param feed
*/
private void addExternalImageURL(SyndFeed feed, Feed extFeed) {
SyndImage img = feed.getImage();
if (img != null) {
extFeed.setExternalImageURL(img.getUrl());
} else {
extFeed.setExternalImageURL(null);
}
}
/**
* Converts a SyndEntry into an Item
*
* @param entry The SyndEntry
* @return The Item
*/
private Item convertToItem(SyndEntry entry) {
// A SyncEntry can potentially have many attributes like title, description,
// guid, link, enclosure or content. In OLAT, however, items are limited
// to the attributes, title, description and one media file (called
// enclosure in RSS) for simplicity.
Item e = new Item();
e.setTitle(entry.getTitle());
e.setDescription(entry.getDescription() != null ? entry.getDescription().getValue() : null);
// Extract content objects from syndication item
StringBuffer sb = new StringBuffer();
for (SyndContent content : (List) entry.getContents()) {
// we don't check for type, assume it is html or txt
if (sb.length() > 0) {
sb.append("");
}
sb.append(content.getValue());
}
// Set aggregated content from syndication item as our content
if (sb.length() > 0) {
e.setContent(sb.toString());
}
e.setGuid(entry.getUri());
e.setExternalLink(entry.getLink());
e.setLastModified(entry.getUpdatedDate());
e.setPublishDate(entry.getPublishedDate());
for (Object enclosure : entry.getEnclosures()) {
if (enclosure instanceof SyndEnclosure) {
SyndEnclosure syndEnclosure = (SyndEnclosure) enclosure;
Enclosure media = new Enclosure();
media.setExternalUrl(syndEnclosure.getUrl());
media.setType(syndEnclosure.getType());
media.setLength(syndEnclosure.getLength());
e.setEnclosure(media);
}
// Break after one cycle because only one media file is supported
break;
}
return e;
}
/**
* Loads an item from file. Used for validation in PodcastFileResource, that's
* why its public and static.
*
* @param container
* @return The item
*/
public Item loadItem(VFSItem container) {
VFSLeaf itemLeaf = null;
Item item = null;
if (container != null) {
itemLeaf = (VFSLeaf) container.resolve(ITEM_FILE_NAME);
if (itemLeaf != null) {
item = (Item) XStreamHelper.readObject(xstream, itemLeaf.getInputStream());
}
}
return item;
}
/**
* @see org.olat.modules.webFeed.managers.FeedManager#remove(org.olat.modules.webFeed.models.Item,
* org.olat.modules.webFeed.models.Feed)
*/
@Override
public void remove(final Item item, final Feed feed) {
// synchronize all feed item CUD operations on this feed to prevend
// overwriting of changes
// o_clusterOK by:fg
coordinator.getSyncer().doInSync(feed, new SyncerCallback