/** * <a href="http://www.openolat.org"> * OpenOLAT - Online Learning and Training</a><br> * <p> * Licensed under the Apache License, Version 2.0 (the "License"); <br> * you may not use this file except in compliance with the License.<br> * You may obtain a copy of the License at the * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a> * <p> * Unless required by applicable law or agreed to in writing,<br> * software distributed under the License is distributed on an "AS IS" BASIS, <br> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br> * See the License for the specific language governing permissions and <br> * limitations under the License. * <p> * Initial code contributed and copyrighted by<br> * frentix GmbH, http://www.frentix.com * <p> */ package org.olat.modules.webFeed.managers; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.apache.commons.io.IOUtils; import org.olat.admin.quota.QuotaConstants; import org.olat.core.CoreSpringFactory; import org.olat.core.commons.modules.bc.meta.tagged.MetaTagged; import org.olat.core.commons.modules.bc.vfs.OlatRootFolderImpl; import org.olat.core.commons.persistence.PersistenceHelper; import org.olat.core.commons.services.commentAndRating.CommentAndRatingService; import org.olat.core.commons.services.image.ImageService; import org.olat.core.commons.services.image.Size; import org.olat.core.gui.components.form.flexible.elements.FileElement; import org.olat.core.gui.media.MediaResource; import org.olat.core.id.Identity; import org.olat.core.id.OLATResourceable; import org.olat.core.logging.AssertException; import org.olat.core.logging.OLog; import org.olat.core.util.CodeHelper; import org.olat.core.util.Encoder; import org.olat.core.util.FileUtils; import org.olat.core.util.Formatter; import org.olat.core.util.StringHelper; import org.olat.core.util.ZipUtil; import org.olat.core.util.cache.CacheWrapper; import org.olat.core.util.coordinate.Coordinator; import org.olat.core.util.coordinate.CoordinatorManager; import org.olat.core.util.coordinate.LockResult; import org.olat.core.util.coordinate.SyncerCallback; import org.olat.core.util.coordinate.SyncerExecutor; import org.olat.core.util.resource.OresHelper; import org.olat.core.util.vfs.LocalFileImpl; import org.olat.core.util.vfs.Quota; import org.olat.core.util.vfs.QuotaManager; import org.olat.core.util.vfs.VFSContainer; import org.olat.core.util.vfs.VFSItem; import org.olat.core.util.vfs.VFSLeaf; import org.olat.core.util.vfs.VFSMediaResource; import org.olat.core.util.xml.XStreamHelper; import org.olat.fileresource.FileResourceManager; import org.olat.fileresource.types.BlogFileResource; import org.olat.fileresource.types.FeedFileResource; import org.olat.fileresource.types.PodcastFileResource; import org.olat.modules.webFeed.RSSFeed; import org.olat.modules.webFeed.SyndFeedMediaResource; import org.olat.modules.webFeed.dispatching.FeedMediaDispatcher; import org.olat.modules.webFeed.models.Enclosure; import org.olat.modules.webFeed.models.Feed; import org.olat.modules.webFeed.models.Item; import org.olat.repository.RepositoryEntry; import org.olat.repository.RepositoryManager; import org.olat.resource.OLATResource; import org.olat.resource.OLATResourceManager; import com.rometools.rome.feed.synd.SyndContent; import com.rometools.rome.feed.synd.SyndEnclosure; import com.rometools.rome.feed.synd.SyndEntry; import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.feed.synd.SyndImage; import com.rometools.rome.io.FeedException; import com.rometools.rome.io.ParsingFeedException; import com.rometools.rome.io.SyndFeedInput; import com.rometools.rome.io.XmlReader; import com.thoughtworks.xstream.XStream; /** * This is the actual feed manager implementation. It handles all operations on * the various feeds and items. * * <P> * Initial Date: Feb 17, 2009 <br> * * @author Gregor Wassmann */ public 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 ImageService imageHelper; private final XStream xstream; // Better performance when protected (apparently) protected CacheWrapper<String,Feed> 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(); } /** * [used by Spring] * @param imageHelper */ public void setImageHelper(ImageService imageHelper) { this.imageHelper = imageHelper; } /** * * @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<String,Feed> 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().getCache(FeedManager.class.getSimpleName(), "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 = CoreSpringFactory.getImpl(CommentAndRatingService.class); commentAndRatingService.deleteAllIgnoringSubPath(feed); 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 = 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; } /** * @see org.olat.modules.webFeed.managers.FeedManager#getItem(org.olat.modules.webFeed.models.Feed, java.lang.String) */ public Item getItem(Feed feed, String GUID) { for (Item item : feed.getItems()) { if (item.getGuid().equals(GUID)) { return item; } } return null; } /** * Puts the feed to the feedCache in a synchronized manner. * * @param ores * @param feed */ private 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<Item> getItems(Feed feed) { List<Item> items = new ArrayList<Item>(); 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. * <p> * Properties are: * <ul> * <li>Title * <li>Author * <li>Descripion (wiki style in repository) * <li>Image * </ul> * * @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()); feed.setDescription(entry.getDescription()); feed.setAuthor(entry.getInitialAuthor()); // Update the image VFSLeaf repoEntryImage = repositoryManager.getImage(entry); if (repoEntryImage != null) { getFeedMediaContainer(feed).copyFrom(repoEntryImage); VFSItem newImage = getFeedMediaContainer(feed).resolve(repoEntryImage.getName()); if (newImage != null) { feed.setImageName(newImage.getName()); } } else { // There's no repo entry image -> delete the feed image as well. //fxdiff: FXOLAT-271 (we don't want to delete image) //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. * <p> * 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; } public Feed readFeedFile(Path feedPath) { Feed feed = null; try (InputStream in = Files.newInputStream(feedPath)) { feed = (Feed)XStreamHelper.readObject(xstream, in); } catch(IOException e) { log.error("", e); } return feed; } /** * 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<Item> loadItems(final Feed feed) { List<Item> items = new ArrayList<Item>(); 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<Item> getItemsFromFeed(Feed extFeed) { List<Item> items = new ArrayList<Item>(); SyndFeed feed = getSyndFeed(extFeed); if (feed != null) { List<SyndEntry> 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(); XmlReader reader = null; try { URL url = new URL(feedURL); reader = new XmlReader(url); feed = input.build(reader); // also add the external image url just in case we'll need it later addExternalImageURL(feed, extFeed); } catch(IllegalArgumentException e) { log.warn("The external feed is invalid: " + feedURL, e); IOUtils.closeQuietly(reader); } 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 <code>SyndEntry</code> into an <code>Item</code> * * @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 StringBuilder sb = new StringBuilder(); for (SyndContent content : (List<SyndContent>) entry.getContents()) { // we don't check for type, assume it is html or txt if (sb.length() > 0) { sb.append("<p />"); } 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 Feed 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 return coordinator.getSyncer().doInSync(feed, new SyncerCallback<Feed>() { public Feed execute() { // reload feed to prevent stale feed overwriting @SuppressWarnings("synthetic-access") Feed reloadedFeed = getFeed(feed, false); reloadedFeed.remove(item); // If the last item has been removed, set the feed to undefined. // The user can then newly decide whether to add items manually or from // an external source. if (!reloadedFeed.hasItems()) { // set undefined reloadedFeed.setExternal(null); } // Delete the item's container on the virtual file system. VFSContainer itemContainer = getItemContainer(item, reloadedFeed); if (itemContainer != null) { itemContainer.delete(); } // Update feed reloadedFeed.setLastModified(new Date()); update(reloadedFeed, false); // Delete comments and ratings CommentAndRatingService commentAndRatingService = CoreSpringFactory.getImpl(CommentAndRatingService.class); commentAndRatingService.deleteAll(feed, item.getGuid()); return reloadedFeed; } }); } /** * @see org.olat.modules.webFeed.managers.FeedManager#addItem(org.olat.modules.webFeed.models.Item, * org.olat.core.gui.components.form.flexible.elements.FileElement, * org.olat.modules.webFeed.models.Feed) */ @Override public Feed addItem(final Item item, final FileElement file, final Feed feed) { if (feed.isInternal()) { // synchronize all feed item CUD operations on this feed to prevent // overwriting of changes // o_clusterOK by:fg return coordinator.getSyncer().doInSync(feed, new SyncerCallback<Feed>() { @SuppressWarnings("synthetic-access") public Feed execute() { // reload feed to prevent stale feed overwriting Feed reloadedFeed = getFeed(feed, false); // Set the current date as published date. if (item.getPublishDate() == null) item.setPublishDate(new Date()); // Set the file properties to the item and store the file setEnclosure(file, item, reloadedFeed); // Write the item.xml file VFSContainer itemContainer = createItemContainer(feed, item); VFSLeaf itemFile = itemContainer.createChildLeaf(ITEM_FILE_NAME); XStreamHelper.writeObject(xstream, itemFile, item); // finally add the item to the feed reloadedFeed.add(item); reloadedFeed.setLastModified(item.getLastModified()); // Save the feed (needed because of itemIds list) update(reloadedFeed, false); return reloadedFeed; } }); } return null; } /** * Method to write the feed object to disk using xstream. * <p> * This method MUST be called from a cluster-synced block with the most recent * feed object from the cluster feed cache! * * @param feed */ void update(Feed feed, boolean inSync) { feed.setLastModified(new Date()); // If the feed url has changed, the items must be reloaded. if (feed.isExternal()) { String oldFeed = getFeed(feed, inSync).getExternalFeedUrl(); String newFeed = feed.getExternalFeedUrl(); if (newFeed != null && !newFeed.equals("")) { if (!newFeed.equals(oldFeed)) { loadItems(feed); } } } // Write the feed file. (Items are saved when adding them.) feed.sortItems(); VFSContainer container = getFeedContainer(feed); VFSLeaf leaf = (VFSLeaf) container.resolve(FEED_FILE_NAME); XStreamHelper.writeObject(xstream, leaf, feed); initFeedCache().update(feed.getResourceableId().toString(), feed); enrichRepositoryEntryByFeedInformation(feed); } /** * A unique key for the item of the feed. Can be used e.g. for locking and * caching. * * @param itemId * @param feedId * @return A unique key for the item of the feed */ private String itemKey(String itemId, String feedId) { final StringBuffer key = new StringBuffer(); key.append("feed").append(feedId); key.append("_item_").append(itemId); return key.toString(); } /** * A unique key for the item of the feed. Can be used e.g. for locking and * caching. (Protected for performance reasons) * * @param item * @param feed * @return A unique key for the item of the feed */ protected String itemKey(Item item, OLATResourceable feed) { String key = itemKey(item.getGuid(), feed.getResourceableId().toString()); return key; } /** * The unique keys for the items of the feed. (Protected for performance * reasons) * * @param feed * @return The unique keys for the items of the feed */ protected String[] itemKeys(Feed feed) { List<Item> items = feed.getItems(); String[] keys = null; if (items != null) { int size = items.size(); keys = new String[size]; for (int i = 0; i < size; i++) { keys[i] = itemKey(items.get(i), feed); } } return keys; } /** * Update the repository entry with the latest set properties in the feed * resource. * <p> * Properties are: * <ul> * <li>Title * <li>Author * <li>Descripion (wiki style in repository) * <li>Image * </ul> * * @param feed */ void enrichRepositoryEntryByFeedInformation(Feed feed) { RepositoryEntry entry = getRepositoryEntry(feed); if (entry != null && feed != null) { Date whenTheFeedWasLastModified = feed.getLastModified(); if (whenTheFeedWasLastModified != null && entry.getLastModified().before(whenTheFeedWasLastModified)) { // feed is newer than repository entry, update repository entry String saveTitle = PersistenceHelper.truncateStringDbSave(feed.getTitle(), 100, true); entry.setDisplayname(saveTitle); String saveDesc = PersistenceHelper.truncateStringDbSave(feed.getDescription(), 16777210, true); entry.setDescription(saveDesc); // Update the image VFSLeaf oldEntryImage = repositoryManager.getImage(entry); if (oldEntryImage != null) { // Delete the old File oldEntryImage.delete(); } // Copy the feed image to the repository home folder unless it was // deleted. String feedImage = feed.getImageName(); if (feedImage != null) { VFSItem newImage = getFeedMediaContainer(feed).resolve(feedImage); if (newImage == null) { // huh? image defined but not found on disk - remove image from feed deleteImage(feed); } else { repositoryManager.setImage((VFSLeaf)newImage, entry); } } } } } /** * @see org.olat.modules.webFeed.managers.FeedManager#getFeedContainer(org.olat.core.id.OLATResourceable) */ public VFSContainer getFeedContainer(OLATResourceable ores) { VFSContainer feedDir = null; if (ores != null) { VFSContainer resourceDir = getResourceContainer(ores); String feedDirName = getFeedKind(ores); feedDir = (VFSContainer) resourceDir.resolve(feedDirName); if (feedDir == null) { // If the folder does not exist create it. feedDir = resourceDir.createChildContainer(feedDirName); } } return feedDir; } /** * @param ores * @return The resource (root) container of the feed */ public OlatRootFolderImpl getResourceContainer(OLATResourceable ores) { return fileResourceManager.getFileResourceRootImpl(ores); } /** * @param file * @param item * @param feed */ public void setEnclosure(FileElement file, Item item, Feed feed) { if (file != null) { VFSContainer itemMediaContainer = (VFSContainer) getItemContainer(item, feed).resolve(MEDIA_DIR); // Empty the container and write the new media file (called 'enclosure' in // rss) for (VFSItem fileItem : itemMediaContainer.getItems()) { if (!fileItem.getName().startsWith(".")) { fileItem.delete(); } } // Move uploaded file to our container VFSItem movedItem = file.moveUploadFileTo(itemMediaContainer); // Rename to something save for the file system String saveFileName = Formatter.makeStringFilesystemSave(file.getUploadFileName()); movedItem.rename(saveFileName); // Update the enclosure meta data Enclosure enclosure = new Enclosure(); enclosure.setFileName(saveFileName); enclosure.setLength(file.getUploadSize()); enclosure.setType(file.getUploadMimeType()); item.setEnclosure(enclosure); } } /** * @see org.olat.modules.webFeed.managers.FeedManager#getItemContainer(org.olat.modules.webFeed.models.Item, * org.olat.modules.webFeed.models.Feed) */ public VFSContainer getItemContainer(Item item, Feed feed) { return (VFSContainer) getItemsContainer(feed).resolve(item.getGuid()); } /** * @see org.olat.modules.webFeed.managers.FeedManager#getItemMediaContainer(org.olat.modules.webFeed.models.Item, * org.olat.modules.webFeed.models.Feed) */ public VFSContainer getItemMediaContainer(Item item, Feed feed) { VFSContainer itemMediaContainer = null; VFSContainer itemContainer = getItemContainer(item, feed); if (itemContainer != null) { itemMediaContainer = (VFSContainer) itemContainer.resolve(MEDIA_DIR); } return itemMediaContainer; } /** * @see org.olat.modules.webFeed.managers.FeedManager#getItemMediaContainer(org.olat.modules.webFeed.models.Item, * org.olat.modules.webFeed.models.Feed) */ public File getItemEnclosureFile(Item item, Feed feed) { VFSContainer mediaDir = getItemMediaContainer(item, feed); Enclosure enclosure = item.getEnclosure(); File file = null; if (mediaDir != null && enclosure != null) { VFSLeaf mediaFile = (VFSLeaf) mediaDir.resolve(enclosure.getFileName()); if (mediaFile != null && mediaFile instanceof LocalFileImpl) { file = ((LocalFileImpl) mediaFile).getBasefile(); } } return file; } /** * Returns the items container of feed * * @param feed * @return The container of all items */ private VFSContainer getItemsContainer(OLATResourceable feed) { VFSContainer items = null; VFSContainer feedContainer = getFeedContainer(feed); // If feed container is null we're in trouble items = (VFSContainer) feedContainer.resolve(ITEMS_DIR); if (items == null) { items = feedContainer.createChildContainer(ITEMS_DIR); } return items; } /** * Returns the container of media files * * @param feed * @return The feed media container */ public VFSContainer getFeedMediaContainer(OLATResourceable feed) { VFSContainer media = null; VFSContainer feedContainer = getFeedContainer(feed); // If feed container is null we're in trouble media = (VFSContainer) feedContainer.resolve(MEDIA_DIR); if (media == null) { media = feedContainer.createChildContainer(MEDIA_DIR); } return media; } /** * @see org.olat.modules.webFeed.managers.FeedManager#updateItem(org.olat.modules.webFeed.models.Item, * org.olat.core.gui.components.form.flexible.elements.FileElement, * org.olat.modules.webFeed.models.Feed) */ @Override public Feed updateItem(final Item item, final FileElement file, final Feed feed) { if (feed.isInternal()) { // synchronize all feed item CUD operations on this feed to prevent // overwriting of changes // o_clusterOK by:fg return coordinator.getSyncer().doInSync(feed, new SyncerCallback<Feed>() { @SuppressWarnings("synthetic-access") public Feed execute() { // reload feed to prevent stale feed overwriting Feed reloadedFeed = getFeed(feed, false); if (reloadedFeed.getItemIds().contains(item.getGuid())) { if (file != null) { setEnclosure(file, item, reloadedFeed); } updateItemFileWithoutDoInSync(item, reloadedFeed); update(feed, false); } else { // do nothing, item was deleted by someone in the meantime } return reloadedFeed; } }); } return null; } /** * Internal helper method to update an item without adding the necessary * do-in-sync wrapper. This should only be called from within a code block * that is cluster-synced! * * @param item * @param feed */ private void updateItemFileWithoutDoInSync (final Item item, final Feed feed) { // Write the item.xml file VFSLeaf itemFile = (VFSLeaf) getItemContainer(item, feed).resolve(ITEM_FILE_NAME); XStreamHelper.writeObject(xstream, itemFile, item); } /** * @see org.olat.modules.webFeed.managers.FeedManager#updateFeedMode(java.lang.Boolean, org.olat.modules.webFeed.models.Feed) */ public Feed updateFeedMode(final Boolean external, final Feed feed) { return coordinator.getSyncer().doInSync(feed, new SyncerCallback<Feed>() { @SuppressWarnings("synthetic-access") public Feed execute() { // reload feed to prevent stale feed overwriting Feed reloadedFeed = getFeed(feed, false); reloadedFeed.setExternal(external); update(reloadedFeed, false); return reloadedFeed; } }); } /** * @see org.olat.modules.webFeed.managers.FeedManager#updateFeedMetadata(org.olat.modules.webFeed.models.Feed) */ public Feed updateFeedMetadata(final Feed feed) { // reload feed to prevent stale feed overwriting final Feed reloadedFeed = getFeed(feed); reloadedFeed.setAuthor(feed.getAuthor()); reloadedFeed.setDescription(feed.getDescription()); reloadedFeed.setExternalFeedUrl(feed.getExternalFeedUrl()); reloadedFeed.setExternalImageURL(feed.getExternalImageURL()); reloadedFeed.setImageName(feed.getImageName()); reloadedFeed.setTitle(feed.getTitle()); return coordinator.getSyncer().doInSync(feed, new SyncerCallback<Feed>() { public Feed execute() { update(reloadedFeed, false); return reloadedFeed; } }); } /** * @see org.olat.modules.webFeed.managers.FeedManager#createMediaResource(java.lang.Long, * java.lang.String, java.lang.String, java.lang.String) */ @Override public MediaResource createItemMediaFile(OLATResourceable feed, String itemId, String fileName) { VFSMediaResource mediaResource = null; // Brute force method for fast delivery try { VFSItem item = getItemsContainer(feed); item = item.resolve(itemId); item = item.resolve(MEDIA_DIR); item = item.resolve(fileName); if(item instanceof VFSLeaf) { mediaResource = new VFSMediaResource((VFSLeaf)item); } } catch (NullPointerException e) { log.debug("Media resource could not be created from file: ", fileName); } return mediaResource; } /** * @see org.olat.modules.webFeed.managers.FeedManager#createMediaResource(java.lang.Long, * java.lang.String, java.lang.String) */ @Override public VFSLeaf createFeedMediaFile(OLATResourceable feed, String fileName, Size thumbnailSize) { VFSLeaf mediaResource = null; // Brute force method for fast delivery try { VFSItem item = getFeedMediaContainer(feed); item = item.resolve(fileName); if(thumbnailSize != null && thumbnailSize.getHeight() > 0 && thumbnailSize.getWidth() > 0 && item instanceof MetaTagged) { item = ((MetaTagged)item).getMetaInfo().getThumbnail(thumbnailSize.getWidth(), thumbnailSize.getHeight(), false); } if(item instanceof VFSLeaf) { mediaResource = (VFSLeaf)item; } } catch (NullPointerException e) { log.debug("Media resource could not be created from file: ", fileName); } return mediaResource; } /** * @see org.olat.modules.webFeed.managers.FeedManager#getFeedUri(org.olat.modules.webFeed.models.Feed, * java.lang.Long) */ @Override public String getFeedBaseUri(Feed feed, Identity identity, Long courseId, String nodeId) { return FeedMediaDispatcher.getFeedBaseUri(feed, identity, courseId, nodeId); } /** * @see org.olat.modules.webFeed.managers.FeedManager#createFeedMediaResource(java.lang.Long, * java.lang.String, java.lang.Long) */ @Override public MediaResource createFeedFile(OLATResourceable ores, Identity identity, Long courseId, String nodeId) { MediaResource media = null; Feed feed = getFeed(ores); if (feed != null) { SyndFeed rssFeed = new RSSFeed(feed, identity, courseId, nodeId); media = new SyndFeedMediaResource(rssFeed); } return media; } /** * @see org.olat.modules.webFeed.managers.FeedManager#isValidFeedUrl(java.lang.String) */ @Override public ValidatedURL validateFeedUrl(String url, String type) { SyndFeedInput input = new SyndFeedInput(); boolean modifiedProtocol = false; try { if (url != null) { url = url.trim(); } if (url.startsWith("feed") || url.startsWith("itpc")) { // accept feed(s) urls like generated in safari browser url = "http" + url.substring(4); modifiedProtocol = true; } URL realUrl = new URL(url); SyndFeed feed = input.build(new XmlReader(realUrl)); if(!feed.getEntries().isEmpty()) { //check for enclosures SyndEntry entry = (SyndEntry)feed.getEntries().get(0); if(type != null && type.indexOf("BLOG") >= 0) { return new ValidatedURL(url, ValidatedURL.State.VALID); } if(entry.getEnclosures().isEmpty()) { return new ValidatedURL(url, ValidatedURL.State.NO_ENCLOSURE); } } //The feed was read successfully return new ValidatedURL(url, ValidatedURL.State.VALID); } catch (ParsingFeedException e) { if(modifiedProtocol) { //fallback for SWITCHcast itpc -> http -> https url = "https" + url.substring(4); return validateFeedUrl(url, type); } return new ValidatedURL(url, ValidatedURL.State.NOT_FOUND); } catch (FileNotFoundException e) { return new ValidatedURL(url, ValidatedURL.State.NOT_FOUND); } catch (MalformedURLException e) { // The url is invalid } catch (FeedException e) { // The feed couldn't be read } catch (IOException e) { // Maybe network or file problems } catch (IllegalArgumentException e) { // something very wrong with the feed } return new ValidatedURL(url, ValidatedURL.State.MALFORMED); } @Override public boolean copy(OLATResource sourceResource, OLATResource targetResource) { File sourceFileroot = FileResourceManager.getInstance().getFileResourceRootImpl(sourceResource).getBasefile(); File sourceBlogRoot = new File(sourceFileroot, FeedManager.getInstance().getFeedKind(sourceResource)); File targetFileroot = FileResourceManager.getInstance().getFileResourceRootImpl(targetResource).getBasefile(); FileUtils.copyFileToDir(sourceBlogRoot, targetFileroot, "add file resource"); VFSContainer copyContainer = FeedManager.getInstance().getFeedContainer(targetResource); VFSLeaf leaf = (VFSLeaf) copyContainer.resolve(FeedManager.FEED_FILE_NAME); if (leaf != null) { Feed copyFeed = (Feed) XStreamHelper.readObject(xstream, leaf.getInputStream()); copyFeed.setId(targetResource.getResourceableId()); XStreamHelper.writeObject(xstream, leaf, copyFeed); } return true; } /** * @see org.olat.modules.webFeed.managers.FeedManager#getFeedArchiveMediaResource(org.olat.core.id.OLATResourceable) */ @Override public VFSMediaResource getFeedArchiveMediaResource(OLATResourceable resource) { VFSLeaf zip = getFeedArchive(resource); return new VFSMediaResource(zip); } /** * @see org.olat.modules.webFeed.managers.FeedManager#getFeedArchive(org.olat.core.id.OLATResourceable) */ public VFSLeaf getFeedArchive(final OLATResourceable resource) { final VFSContainer rootContainer, feedContainer; rootContainer = getResourceContainer(resource); feedContainer = getFeedContainer(resource); //prepare fallback for author if needed final Feed feed = getFeed(resource, true); if(feed.isInternal()) { coordinator.getSyncer().doInSync(feed, new SyncerCallback<Boolean>() { @SuppressWarnings("synthetic-access") public Boolean execute() { for(Item item : getItems(feed)) { if(!item.isAuthorFallbackSet()) { //get used authorKey first String author = item.getAuthor(); if(StringHelper.containsNonWhitespace(author)) { //set author fallback item.setAuthor(author); //update item.xml VFSContainer itemContainer = getItemContainer(item, feed); if(itemContainer != null) { VFSLeaf itemFile = (VFSLeaf)itemContainer.resolve(ITEM_FILE_NAME); XStreamHelper.writeObject(xstream, itemFile, item); } } } } return Boolean.TRUE; } }); } // synchronize all zip processes for this feed // o_clusterOK by:fg VFSLeaf zip = coordinator.getSyncer().doInSync(resource, new SyncerCallback<VFSLeaf>() { public VFSLeaf execute() { // Delete the old archive and recreate it from scratch String zipFileName = getZipFileName(resource); VFSItem oldArchive = rootContainer.resolve(zipFileName); if (oldArchive != null) { oldArchive.delete(); } ZipUtil.zip(feedContainer.getItems(), rootContainer.createChildLeaf(zipFileName), false); return (VFSLeaf) rootContainer.resolve(zipFileName); } }); return zip; } /** * Returns the file name of the archive that is to be exported. Depends on the * kind of the resource. * * @param resource * @return The zip archive file name */ String getZipFileName(OLATResourceable resource) { return getFeedKind(resource) + ".zip"; } /** * @see org.olat.modules.webFeed.managers.FeedManager#releaseLock(org.olat.core.util.coordinate.LockResult) */ public void releaseLock(LockResult lock) { if (lock != null) { coordinator.getLocker().releaseLock(lock); } } /** * @see org.olat.modules.webFeed.managers.FeedManager#acquireLock(org.olat.core.id.OLATResourceable, * org.olat.core.id.Identity) */ public LockResult acquireLock(OLATResourceable feed, Identity identity) { // OLATResourceable itemLock = // OresHelper.createOLATResourceableInstance("podcastlock_" + // feed.getResourceableId() + "_meta", item.getId()) LockResult lockResult = coordinator.getLocker().acquireLock(feed, identity, null); return lockResult; } /** * @see org.olat.modules.webFeed.managers.FeedManager#acquireLock(org.olat.core.id.OLATResourceable, * org.olat.modules.webFeed.models.Item, org.olat.core.id.Identity) */ public LockResult acquireLock(OLATResourceable feed, Item item, Identity identity) { String key = itemKey(item, feed); if (key.length() >= OresHelper.ORES_TYPE_LENGTH) { key = Encoder.md5hash(key); } OLATResourceable itemResource = OresHelper.createOLATResourceableType(key); LockResult lockResult = coordinator.getLocker().acquireLock(itemResource, identity, key); return lockResult; } @Override public Quota getQuota(OLATResourceable feed) { OlatRootFolderImpl container = getResourceContainer(feed); Quota quota = QuotaManager.getInstance().getCustomQuota(container.getRelPath()); if (quota == null) { Quota defQuota = QuotaManager.getInstance().getDefaultQuota(QuotaConstants.IDENTIFIER_DEFAULT_FEEDS); quota = QuotaManager.getInstance().createQuota(container.getRelPath(), defQuota.getQuotaKB(), defQuota.getUlLimitKB()); } return quota; } /** * @see org.olat.modules.webFeed.managers.FeedManager#setImage(org.olat.core.gui.components.form.flexible.elements.FileElement, * org.olat.modules.webFeed.models.Feed) */ @Override public void setImage(FileElement image, Feed feed) { if (image != null) { // Delete the old image deleteImage(feed); // Save the new image VFSLeaf imageLeaf = image.moveUploadFileTo(getFeedMediaContainer(feed)); // Resize to same dimension box as with repo meta image VFSLeaf tmp = getFeedMediaContainer(feed).createChildLeaf("" + CodeHelper.getRAMUniqueID()); imageHelper.scaleImage(imageLeaf, tmp, PICTUREWIDTH,PICTUREWIDTH, false); imageLeaf.delete(); imageLeaf = tmp; // Make file system save String saveFileName = Formatter.makeStringFilesystemSave(image.getUploadFileName()); imageLeaf.rename(saveFileName); // Update metadata feed.setImageName(saveFileName); } } /** * @see org.olat.modules.webFeed.managers.FeedManager#deleteImage(org.olat.modules.webFeed.models.Feed) */ @Override public void deleteImage(Feed feed) { VFSContainer mediaContainer = getFeedMediaContainer(feed); String imageName = feed.getImageName(); if (imageName != null) { VFSLeaf image = (VFSLeaf) mediaContainer.resolve(imageName); if (image != null) { image.delete(); } feed.setImageName(null); } } /** * @see org.olat.modules.webFeed.managers.FeedManager#createItemContainer(org.olat.modules.webFeed.models.Feed, org.olat.modules.webFeed.models.Item) */ @Override public VFSContainer createItemContainer(Feed feed, Item item) { VFSContainer itemContainer = getItemContainer(item, feed); if (itemContainer == null) { if (!StringHelper.containsNonWhitespace(item.getGuid())) { throw new AssertException("Programming error, item has no GUID set, can not create an item container for this item"); } itemContainer = getItemsContainer(feed).createChildContainer(item.getGuid()); } // prepare media container VFSContainer mediaContainer = getItemMediaContainer(item, feed); if (mediaContainer == null) { itemContainer.createChildContainer(MEDIA_DIR); } return itemContainer; } }