/*   **********************************************************************  **
 **   Copyright notice                                                       **
 **                                                                          **
 **   (c) 2003-2006 RSSOwl Development Team                                  **
 **   http://www.rssowl.org/                                                 **
 **                                                                          **
 **   All rights reserved                                                    **
 **                                                                          **
 **   This program and the accompanying materials are made available under   **
 **   the terms of the Eclipse Public License 1.0 which accompanies this     **
 **   distribution, and is available at:                                     **
 **   http://www.rssowl.org/legal/epl-v10.html                               **
 **                                                                          **
 **   A copy is found in the file epl-v10.html and important notices to the  **
 **   license from the team is found in the textfile LICENSE.txt distributed **
 **   in this package.                                                       **
 **                                                                          **
 **   This copyright notice MUST APPEAR in all copies of the file!           **
 **                                                                          **
 **   Contributors:                                                          **
 **     RSSOwl - initial API and implementation (bpasero@rssowl.org)         **
 **                                                                          **
 **  **********************************************************************  */

package net.sourceforge.rssowl.model;

import net.sourceforge.rssowl.controller.GUI;
import net.sourceforge.rssowl.dao.NewsfeedFactoryException;
import net.sourceforge.rssowl.util.DateParser;
import net.sourceforge.rssowl.util.search.ParsedSearch;
import net.sourceforge.rssowl.util.search.SearchDefinition;
import net.sourceforge.rssowl.util.shop.StringShop;

import org.jdom.Document;
import org.jdom.Element;

import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Model for a Channel
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public class Channel {

  /** If this is an aggregation, the aggregated category */
  private Category aggregatedCategory;

  /**
   * If the Channel was built from an aggregated category, this field will
   * remember which favorites where aggregated.
   */
  private TreeSet aggregatedFavorites;

  private String category;
  private String copyright;
  private String creator;
  private String description;

  /** This tag should contain a URL that references a description of the channel */
  private String docs;

  /** Save the Document (rss xml) */
  private Document document;

  /** Format of this Newsfeed */
  private String format;

  /** A string indicating the program used to generate the channel. */
  private String generator;

  /** URL of this feed's homepage */
  private String homepage;

  /** May Attributes */
  private ChannelImage image;

  /** Number of items (= news) in this RSS */
  private int itemCount;

  /** Newsitems */
  private Hashtable items;

  private String language;

  /** The last time the channel was modified */
  private String lastBuildDate;

  /** The parsed lastBuildDate */
  private Date lastBuildDateParsed;

  private String link;

  /** The email address of the managing editor of the site */
  private String managingEditor;

  /** Save what channel infos are available */
  private Vector newsChannelInfos;

  /** Save what newsitem infos are available */
  private Vector newsItemInfos;

  /** Save order of the news from the XML */
  private Vector newsItemOrder;

  /** Date when channel was published. */
  private String pubDate;

  /** The parsed publish date */
  private Date pubDateParsed;

  private String publisher;

  /** Sorted items that match the search */
  private Vector searchResultsItemOrder;

  /** Items that match the search */
  private Hashtable searchResultsItems;

  private String title;

  /**
   * ttl stands for time to live. It's a number of minutes that indicates how
   * long a channel can be cached before refreshing from the source.
   */
  private String ttl;

  private String updateFrequency;
  private String updatePeriod;

  /**
   * The email address of the webmaster for the site, the person to contact if
   * there are technical problems with the channel
   */
  private String webmaster;

  /** Default constructor */
  public Channel() {
    this(null, null, null, null);
  }

  /**
   * This constructor is used to aggregate a Vector of RSSChannels into one
   * channel.
   * 
   * @param title Title of the channel
   * @param aggregatedCategory The category that is aggregated
   * @param rssChannels Vector holding some rss channels
   * @param aggregatedFavorites The favorites that have been aggregated
   * @param generateUniqueTitles Set to TRUE if the news-titles have to be
   * unique
   */
  public Channel(String title, Category aggregatedCategory, Vector rssChannels, TreeSet aggregatedFavorites, boolean generateUniqueTitles) {

    /** Call default constructor */
    this();

    this.aggregatedFavorites = aggregatedFavorites;
    this.aggregatedCategory = aggregatedCategory;
    setTitle(title);
    aggregateChannels(rssChannels, generateUniqueTitles);

    /** The news table shall display the feed XML url */
    addAvailableNewsItemInfo("TABLE_HEADER_FEED");
  }

  /**
   * Instantiates a news Channel
   * 
   * @param title Title of the channel
   * @param description Description of the channel
   * @param link Link of the channel
   * @param language Language of the channel
   */
  public Channel(String title, String description, String link, String language) {
    this.title = title;
    this.description = description;
    this.link = link;
    this.language = language;
    aggregatedCategory = null;
    items = new Hashtable();
    newsItemOrder = new Vector();
    newsChannelInfos = new Vector();
    newsItemInfos = new Vector();
    searchResultsItems = new Hashtable();
    searchResultsItemOrder = new Vector();

    /** Default values */
    newsItemInfos.add("TABLE_HEADER_STATUS");
    newsItemInfos.add("TABLE_HEADER_NEWSTITLE");
  }

  /**
   * Create a fake Channel that displays an warning message given from the
   * Exception.
   * 
   * @param url The URL where the warning occured
   * @param e The exception
   * @return Channel The warning channel
   */
  public static Channel createErrorChannel(String url, NewsfeedFactoryException e) {

    /** RSS Channel */
    Channel rssErrorChannel = new Channel();
    rssErrorChannel.setTitle(Category.getTitleForLink(url));

    /** RSS Newsitem */
    NewsItem errorItem = new NewsItem();
    errorItem.setLink(e.getUrl());
    errorItem.setDescription((e.getMessage() != null) ? e.getMessage() : "-");

    if (e.getTitle() != null)
      errorItem.setTitle(StringShop.printf(GUI.i18n.getTranslation("ERROR_LOADING_FEED"), new String[] { "%TITLE%" }, new String[] { e.getTitle() }));
    else
      errorItem.setTitle(StringShop.printf(GUI.i18n.getTranslation("ERROR_LOADING_FEED"), new String[] { "%TITLE%" }, new String[] { e.getUrl() }));

    /** Insert item into channel */
    rssErrorChannel.insertItem(errorItem);

    return rssErrorChannel;
  }

  /**
   * Add an available news channel info
   * 
   * @param i18nName The i18n key for the info
   */
  public void addAvailableNewsChannelInfo(String i18nName) {
    if (!newsChannelInfos.contains(i18nName))
      newsChannelInfos.add(i18nName);
  }

  /**
   * Add an available news item info
   * 
   * @param i18nName The i18n key for the info
   */
  public void addAvailableNewsItemInfo(String i18nName) {
    if (!newsItemInfos.contains(i18nName))
      newsItemInfos.add(i18nName);
  }

  /**
   * Get wether this Channel contains unread news
   * 
   * @return boolean TRUE in case it contains unread news
   */
  public boolean containsUnreadNews() {
    return getUnreadNewsCount() > 0;
  }

  /**
   * @return Category If this channel is from an aggregation, will return the
   * aggregated category, or NULL in the other case.
   */
  public Category getAggregatedCategory() {
    return aggregatedCategory;
  }

  /**
   * @return TreeSet Get the list of aggregated favorites if this channel was
   * created from an aggregation
   */
  public TreeSet getAggregatedFavorites() {
    return aggregatedFavorites;
  }

  /**
   * Get available news channel infos
   * 
   * @return Vector available news channel infos
   */
  public Vector getAvailableNewsChannelInfos() {
    return newsChannelInfos;
  }

  /**
   * Get available news item infos
   * 
   * @return Vector available news item infos
   */
  public Vector getAvailableNewsItemInfos() {
    return newsItemInfos;
  }

  /**
   * Get the category
   * 
   * @return String category
   */
  public String getCategory() {
    return category;
  }

  /**
   * Get the copyright
   * 
   * @return String copyright
   */
  public String getCopyright() {
    return copyright;
  }

  /**
   * Get the creator
   * 
   * @return String creator
   */
  public String getCreator() {
    return creator;
  }

  /**
   * Get the description
   * 
   * @return String description
   */
  public String getDescription() {
    return description;
  }

  /**
   * Get the docs
   * 
   * @return String docs
   */
  public String getDocs() {
    return docs;
  }

  /**
   * Get the document
   * 
   * @return Document document
   */
  public Document getDocument() {
    return document;
  }

  /**
   * Get the Format
   * 
   * @return String Feed Format
   */
  public String getFormat() {
    return format;
  }

  /**
   * Get the generator
   * 
   * @return String generator
   */
  public String getGenerator() {
    return generator;
  }

  /**
   * Get the Feeds Homepage
   * 
   * @return String Feeds Homepage
   */
  public String getHomepage() {
    return homepage;
  }

  /**
   * Get the ChannelImage
   * 
   * @return ChannelImage the RSS channel image
   */
  public ChannelImage getImage() {
    return image;
  }

  /**
   * Get count of items
   * 
   * @return int item count
   */
  public int getItemCount() {
    return itemCount;
  }

  /**
   * Get all items
   * 
   * @return Hashtable items
   */
  public Hashtable getItems() {
    return items;
  }

  /**
   * Get the language
   * 
   * @return String language
   */
  public String getLanguage() {
    return language;
  }

  /**
   * Get the last build date
   * 
   * @return String last build date
   */
  public String getLastBuildDate() {
    return lastBuildDate;
  }

  /**
   * Get the parsed last build Date
   * 
   * @return Date The last build Date object
   */
  public Date getLastBuildDateParsed() {
    return lastBuildDateParsed;
  }

  /**
   * Get the link
   * 
   * @return String link
   */
  public String getLink() {
    return link;
  }

  /**
   * Get the managing editor
   * 
   * @return String managing editor
   */
  public String getManagingEditor() {
    return managingEditor;
  }

  /**
   * Get news item order
   * 
   * @return Vector news item order
   */
  public Vector getNewsItemOrder() {
    return newsItemOrder;
  }

  /**
   * Get the publish date
   * 
   * @return String publish date
   */
  public String getPubDate() {
    return pubDate;
  }

  /**
   * Get the parsed publish Date
   * 
   * @return Date The publish Date object
   */
  public Date getPubDateParsed() {
    return pubDateParsed;
  }

  /**
   * Get the publisher
   * 
   * @return String publisher
   */
  public String getPublisher() {
    return publisher;
  }

  /**
   * Get the search results item order
   * 
   * @return Vector search results item order
   */
  public Vector getSearchResultsItemOrder() {
    return searchResultsItemOrder;
  }

  /**
   * Get the search result items
   * 
   * @return Hashtable search result items
   */
  public Hashtable getSearchResultsItems() {
    return searchResultsItems;
  }

  /**
   * Get the title
   * 
   * @return String title
   */
  public String getTitle() {
    return title;
  }

  /**
   * Get the time to live
   * 
   * @return String time to live
   */
  public String getTtl() {
    return ttl;
  }

  /**
   * Get the number of unread news for this channel
   * 
   * @return int The number of unread news
   */
  public int getUnreadNewsCount() {
    int count = 0;
    Enumeration elements = items.elements();

    /** For each newsitem */
    while (elements.hasMoreElements()) {
      NewsItem item = ((NewsItem) elements.nextElement());
      if (!item.isRead())
        count++;
    }

    return count;
  }

  /**
   * Get the number of unread news for the given feed. This method is called
   * when determining unread news from a certain feed inside an aggregation.
   * 
   * @param feedXMLUrl The URL of the newsfeed
   * @return int The number of unread news for the given feed
   */
  public int getUnreadNewsCount(String feedXMLUrl) {
    int count = 0;
    Enumeration elements = items.elements();

    /** For each newsitem */
    while (elements.hasMoreElements()) {
      NewsItem item = ((NewsItem) elements.nextElement());
      String itemsFeedXMLUrl = item.getNewsfeedXmlUrl();

      /** Only count in case the newsitem is from the given feed */
      if (!item.isRead() && itemsFeedXMLUrl != null && itemsFeedXMLUrl.equals(feedXMLUrl))
        count++;
    }

    return count;
  }

  /**
   * Get update frequency
   * 
   * @return String update frequency
   */
  public String getUpdateFrequency() {
    return updateFrequency;
  }

  /**
   * Get the update period
   * 
   * @return String update period
   */
  public String getUpdatePeriod() {
    return updatePeriod;
  }

  /**
   * Get the webmaster
   * 
   * @return String webmaster
   */
  public String getWebmaster() {
    return webmaster;
  }

  /**
   * Insert a newsitem into the Channel and generate a unique title
   * 
   * @param item The NewsItem to insert
   */
  public void insertItem(NewsItem item) {
    insertItem(item, true);
  }

  /**
   * Insert a newsitem into the Channel. Generate a unique title if set so.
   * 
   * @param item The newsitem to add
   * @param generateUniqueTitle TRUE if the title must be unique
   */
  public void insertItem(NewsItem item, boolean generateUniqueTitle) {
    String title = item.getTitle();
    String oldTitle = item.getTitle();

    /** In case title is not given */
    if (StringShop.isWhiteSpaceOrEmpty(title)) {
      title = GUI.i18n.getTranslation("NO_TITLE");
      oldTitle = GUI.i18n.getTranslation("NO_TITLE");
      item.setTitle(title);
    }

    /** Generate a unique title */
    if (generateUniqueTitle) {
      int number = 1;

      /** As long as title is not unique */
      while (items.containsKey(title)) {
        title = oldTitle;
        title = title + " #" + number;
        number++;
      }

      /** Apply Title to Item, dont strip Tags or Trim */
      item.setTitle(title, false, false);
    }

    /** Tell Item about Channels Title and URL */
    item.setNewsfeedTitle(getTitle());
    item.setNewsfeedXmlUrl(getLink());

    items.put(title, item);
    newsItemOrder.add(title);
    itemCount++;
  }

  /**
   * @return boolean TRUE if the Feed is an aggregated category
   */
  public boolean isAggregatedCat() {
    return aggregatedCategory != null;
  }

  /**
   * Run a Search on the Items of this Channel.
   * 
   * @param parsedSearch The parsed Search to use for the Search. A search may
   * either use the pattern directly as regular expression or use lists of must-
   * and must-not words.
   */
  public void peformSearch(ParsedSearch parsedSearch) {

    /** RegEx Search: Use Pattern directly as regular expression for the Search */
    if (parsedSearch.isRegExSearch())
      search(parsedSearch.getSearchDefinition());

    /** Normal Search: Use Lists of must- and must-not words for the Search */
    else
      search(parsedSearch);
  }

  /**
   * Set the category
   * 
   * @param category
   */
  public void setCategory(String category) {
    this.category = category;
    addAvailableNewsChannelInfo("CHANNEL_INFO_CATEGORY");
  }

  /**
   * Set the copyright
   * 
   * @param copyright
   */
  public void setCopyright(String copyright) {
    this.copyright = copyright;
  }

  /**
   * Set the creator
   * 
   * @param creator
   */
  public void setCreator(String creator) {
    this.creator = creator;
    addAvailableNewsChannelInfo("CHANNEL_INFO_CREATOR");
  }

  /**
   * Set the description
   * 
   * @param description
   */
  public void setDescription(String description) {
    this.description = description;
  }

  /**
   * Set the docs
   * 
   * @param docs
   */
  public void setDocs(String docs) {
    this.docs = docs;
    addAvailableNewsChannelInfo("CHANNEL_INFO_DOCS");
  }

  /**
   * Set the document
   * 
   * @param document
   */
  public void setDocument(Document document) {
    this.document = document;
  }

  /**
   * Set the Format
   * 
   * @param format
   */
  public void setFormat(String format) {
    this.format = format;
    addAvailableNewsChannelInfo("CHANNEL_INFO_RSSVERSION");
  }

  /**
   * Set the generator
   * 
   * @param generator
   */
  public void setGenerator(String generator) {
    this.generator = generator;
    addAvailableNewsChannelInfo("CHANNEL_INFO_GENERATOR");
  }

  /**
   * Set the Feeds Homepage
   * 
   * @param homepage
   */
  public void setHomepage(String homepage) {
    this.homepage = homepage;
  }

  /**
   * Set the RSS channel image
   * 
   * @param image
   */
  public void setImage(ChannelImage image) {
    this.image = image;
  }

  /**
   * Set the item count
   * 
   * @param itemCount
   */
  public void setItemCount(int itemCount) {
    this.itemCount = itemCount;
  }

  /**
   * Set the language
   * 
   * @param language
   */
  public void setLanguage(String language) {
    this.language = language;
    addAvailableNewsChannelInfo("CHANNEL_INFO_LANGUAGE");
  }

  /**
   * Set the last build date
   * 
   * @param lastBuildDate
   */
  public void setLastBuildDate(String lastBuildDate) {
    this.lastBuildDate = lastBuildDate;
    lastBuildDateParsed = DateParser.getDate(lastBuildDate);
    addAvailableNewsChannelInfo("CHANNEL_INFO_LASTBUILDDATE");
  }

  /**
   * Set the link
   * 
   * @param link
   */
  public void setLink(String link) {
    this.link = link;
  }

  /**
   * Set the managing editor
   * 
   * @param managingEditor
   */
  public void setManagingEditor(String managingEditor) {
    this.managingEditor = managingEditor;
    addAvailableNewsChannelInfo("CHANNEL_INFO_MANAGINGEDITOR");
  }

  /**
   * Set the publish date
   * 
   * @param pubDate
   */
  public void setPubDate(String pubDate) {
    this.pubDate = pubDate;
    pubDateParsed = DateParser.getDate(pubDate);
    addAvailableNewsChannelInfo("CHANNEL_INFO_PUBDATE");
  }

  /**
   * Set the publisher
   * 
   * @param publisher
   */
  public void setPublisher(String publisher) {
    this.publisher = publisher;
    addAvailableNewsChannelInfo("CHANNEL_INFO_PUBLISHER");
  }

  /**
   * Set the title
   * 
   * @param title
   */
  public void setTitle(String title) {
    this.title = title;
  }

  /**
   * Set the time to live
   * 
   * @param ttl
   */
  public void setTtl(String ttl) {
    this.ttl = ttl;
    addAvailableNewsChannelInfo("CHANNEL_INFO_TTL");
  }

  /**
   * Set the update frequency
   * 
   * @param updateFrequency
   */
  public void setUpdateFrequency(String updateFrequency) {
    this.updateFrequency = updateFrequency;
  }

  /**
   * Set the update period
   * 
   * @param updatePeriod
   */
  public void setUpdatePeriod(String updatePeriod) {
    this.updatePeriod = updatePeriod;
    addAvailableNewsChannelInfo("CHANNEL_INFO_UPDATE_PERIOD");
  }

  /**
   * Set the webmaster
   * 
   * @param webmaster
   */
  public void setWebmaster(String webmaster) {
    this.webmaster = webmaster;
    addAvailableNewsChannelInfo("CHANNEL_INFO_WEBMASTER");
  }

  /**
   * Create a RSS 2.0 XML document from the Channel. This is used when saving
   * newsfeeds.
   * 
   * @return Document A new JDom XML document
   */
  public Document toDocument() {
    Document document = new Document();

    /** Root Element */
    Element root = new Element("rss");
    root.setAttribute("version", "2.0");
    document.setRootElement(root);

    /** Channel */
    Element channel = new Element("channel");
    root.addContent(channel);

    /** Title */
    Element title = new Element("title");
    if (this.title != null)
      title.setText(this.title);
    else
      title.setText(GUI.i18n.getTranslation("NO_TITLE"));
    channel.addContent(title);

    /** Description */
    if (StringShop.isset(this.description)) {
      Element description = new Element("description");
      description.setText(this.description);
      channel.addContent(description);
    }

    /** Link */
    if (StringShop.isset(this.homepage)) {
      Element homepage = new Element("link");
      homepage.setText(this.homepage);
      channel.addContent(homepage);
    }

    /** Language */
    if (StringShop.isset(this.language)) {
      Element language = new Element("language");
      language.setText(this.language);
      channel.addContent(language);
    }

    /** Image */
    if (this.image != null && StringShop.isset(this.image.getImgUrl())) {
      Element image = new Element("image");

      /** URL of the Image */
      Element imageUrl = new Element("url");
      imageUrl.setText(this.image.getImgUrl());
      image.addContent(imageUrl);

      /** Link to Homepage */
      if (StringShop.isset(this.image.getLink())) {
        Element imageLink = new Element("link");
        imageLink.setText(this.image.getLink());
        image.addContent(imageLink);
      }

      channel.addContent(image);
    }

    /** Copyright */
    if (StringShop.isset(this.copyright)) {
      Element copyright = new Element("copyright");
      copyright.setText(this.copyright);
      channel.addContent(copyright);
    }

    /** Pub Date */
    if (StringShop.isset(this.pubDate)) {
      Element pubDate = new Element("pubDate");
      pubDate.setText(this.pubDate);
      channel.addContent(pubDate);
    }

    /** Last Build Date */
    if (StringShop.isset(this.lastBuildDate)) {
      Element lastBuildDate = new Element("lastBuildDate");
      lastBuildDate.setText(this.lastBuildDate);
      channel.addContent(lastBuildDate);
    }

    /** Docs */
    if (StringShop.isset(this.docs)) {
      Element docs = new Element("docs");
      docs.setText(this.docs);
      channel.addContent(docs);
    }

    /** Managing Editor */
    if (StringShop.isset(this.managingEditor)) {
      Element managingEditor = new Element("managingEditor");
      managingEditor.setText(this.managingEditor);
      channel.addContent(managingEditor);
    }

    /** Webmaster */
    if (StringShop.isset(this.webmaster)) {
      Element webmaster = new Element("webMaster");
      webmaster.setText(this.webmaster);
      channel.addContent(webmaster);
    }

    /** Category */
    if (StringShop.isset(this.category)) {
      Element category = new Element("category");
      category.setText(this.category);
      channel.addContent(category);
    }

    /** Generator */
    if (StringShop.isset(this.generator)) {
      Element generator = new Element("generator");
      generator.setText(this.generator);
      channel.addContent(generator);
    }

    /** Ordered list of newsitems */
    Vector newsItems = newsItemOrder;

    /** If the size of this vector is not 0, a search was performed */
    if (searchResultsItemOrder.size() != 0)
      newsItems = searchResultsItemOrder;

    /** News Items */
    for (int a = 0; a < newsItems.size(); a++) {
      NewsItem rssNewsItem = (NewsItem) items.get(newsItems.get(a));
      Element item = new Element("item");

      /** Title */
      Element newsTitle = new Element("title");
      if (StringShop.isset(rssNewsItem.getTitle()))
        newsTitle.setText(rssNewsItem.getTitle());
      else
        newsTitle.setText(GUI.i18n.getTranslation("NO_TITLE"));
      item.addContent(newsTitle);

      /** Link */
      if (StringShop.isset(rssNewsItem.getLink())) {
        Element newsLink = new Element("link");
        newsLink.setText(rssNewsItem.getLink());
        item.addContent(newsLink);
      }

      /** Description */
      if (StringShop.isset(rssNewsItem.getDescription())) {
        Element newsDescription = new Element("description");
        newsDescription.setText(rssNewsItem.getDescription());
        item.addContent(newsDescription);
      }

      /** Publish Date */
      if (StringShop.isset(rssNewsItem.getPubDate())) {
        Element newsPubDate = new Element("pubDate");
        newsPubDate.setText(rssNewsItem.getPubDate());
        item.addContent(newsPubDate);
      }

      /** Author */
      if (StringShop.isset(rssNewsItem.getAuthor())) {
        Element newsAuthor = new Element("author");
        newsAuthor.setText(rssNewsItem.getAuthor());
        item.addContent(newsAuthor);
      }

      /** Category */
      if (StringShop.isset(rssNewsItem.getCategory())) {
        Element newsCategory = new Element("category");
        newsCategory.setText(rssNewsItem.getCategory());
        item.addContent(newsCategory);
      }

      /** GUID */
      if (StringShop.isset(rssNewsItem.getGuid())) {
        Element newsGuid = new Element("guid");
        newsGuid.setText(rssNewsItem.getGuid());
        item.addContent(newsGuid);
      }

      /** Source */
      if (StringShop.isset(rssNewsItem.getSource())) {
        Element newsSource = new Element("source");
        newsSource.setAttribute("url", rssNewsItem.getSource());
        item.addContent(newsSource);
      }

      /** Enclosure */
      if (rssNewsItem.getEnclosures() != null && rssNewsItem.getEnclosures().size() > 0) {
        Vector enclosures = rssNewsItem.getEnclosures();
        for (Iterator iter = enclosures.iterator(); iter.hasNext();) {
          Enclosure enclosure = (Enclosure) iter.next();
          Element enclosureElement = new Element("enclosure");

          /** URL */
          if (StringShop.isset(enclosure.getUrl()))
            enclosureElement.setAttribute("url", enclosure.getUrl());

          /** Length */
          if (StringShop.isset(enclosure.getLength(false)))
            enclosureElement.setAttribute("length", enclosure.getLength(false));

          /** Type */
          if (StringShop.isset(enclosure.getType()))
            enclosureElement.setAttribute("type", enclosure.getType());

          item.addContent(enclosureElement);
        }
      }

      /** Comments */
      if (rssNewsItem.getComments() != null && rssNewsItem.getComments().size() > 0) {
        Vector comments = rssNewsItem.getComments();
        for (Iterator iter = comments.iterator(); iter.hasNext();) {
          String comment = (String) iter.next();
          Element commentElement = new Element("comments");
          commentElement.setText(comment);
          item.addContent(commentElement);
        }
      }

      channel.addContent(item);
    }
    return document;
  }

  /**
   * Check for each NewsItem of the given Channel if the news was read before or
   * is unread. This is done with using the archive that saves read news.
   */
  public void updateReadStatusOnNews() {
    Enumeration elements = items.elements();
    while (elements.hasMoreElements()) {
      NewsItem rssNewsItem = (NewsItem) elements.nextElement();

      /** Set unread / read status */
      rssNewsItem.setRead(GUI.rssOwlGui.getArchiveManager().getArchive().isNewsRead(rssNewsItem));
    }
  }

  /**
   * Aggregate a Vector of RSS channels into this channel
   * 
   * @param rssChannels Vector holding some rss channels
   * @param generateUniqueTitles Set to TRUE if news-titles have to be unique
   */
  private void aggregateChannels(Vector rssChannels, boolean generateUniqueTitles) {

    /** Foreach channels */
    for (int a = 0; a < rssChannels.size(); a++) {
      Channel curChannel = (Channel) rssChannels.get(a);

      /** Detect Title of Newsfeed this Item is in */
      String feedTitle;
      if (curChannel.getLink() != null && Category.getFavPool().containsKey(curChannel.getLink()))
        feedTitle = ((Favorite) Category.getFavPool().get(curChannel.getLink())).getTitle();
      else
        feedTitle = curChannel.getTitle();

      /** Detect URL of Newsfeed this Item is in */
      String feedURL = feedTitle != null ? Category.getLinkForTitle(feedTitle) : null;

      /** Use item order to get the items */
      Vector newsItemOrder = curChannel.getNewsItemOrder();
      Hashtable items = curChannel.getItems();

      /** Foreach newsitem */
      for (int b = 0; b < newsItemOrder.size(); b++) {
        NewsItem rssNewsItem = (NewsItem) items.get(newsItemOrder.get(b));

        /** Create a clone to store in the aggregation */
        NewsItem newsItemClone = new NewsItem(true);
        rssNewsItem.clone(newsItemClone);

        /** Insert clone */
        insertItem(newsItemClone, generateUniqueTitles);

        /** Set these informations explicitly */
        newsItemClone.setNewsfeedTitle(feedTitle);
        newsItemClone.setNewsfeedXmlUrl(feedURL);
        newsItemClone.setNewsfeedHomepage(curChannel.getHomepage());
      }

      /** Get available newsitem infos and add them to aggregated channel */
      Vector newsItemInfos = curChannel.getAvailableNewsItemInfos();
      for (int b = 0; b < newsItemInfos.size(); b++)
        if (!getAvailableNewsItemInfos().contains(newsItemInfos.get(b)))
          addAvailableNewsItemInfo((String) newsItemInfos.get(b));
    }
  }

  /**
   * <p>
   * Perform the search and add the NewsItems that match the pattern to the
   * searchResultsItems Hashtable
   * </p>
   * This search is using two ArrayLists containing the must- and the must-not
   * words.
   * 
   * @param parsedSearch Parsed Search Pattern containing the words that must
   * and the words that must not be part of the NewsItem and Scope to search.
   */
  private void search(ParsedSearch parsedSearch) {
    boolean success;

    /** Reset Hashtables */
    searchResultsItems = new Hashtable();
    searchResultsItemOrder = new Vector();

    ArrayList mustNotContain = parsedSearch.getMustNotContain();
    ArrayList mustContain = parsedSearch.getMustContain();
    SearchDefinition searchDefinition = parsedSearch.getSearchDefinition();

    /** For each news in this channel */
    for (int c = 0; c < newsItemOrder.size(); c++) {
      NewsItem item = (NewsItem) items.get(newsItemOrder.get(c));
      success = true;

      /** Update Items Case Sensitive Flag */
      item.setSearchCaseSensitive(searchDefinition.isCaseSensitive());

      /** Clear Vector */
      item.clearHighlightWords();

      /** Get one line String to search based on SearchDefinition */
      String oneLineComplete = item.toString(parsedSearch.getSearchDefinition());

      /** Check for NOT searchwords */
      for (int a = 0; a < mustNotContain.size(); a++) {
        String mustNotInclude = (String) mustNotContain.get(a);

        /** Create a new pattern with given Case-Sensitivity */
        Pattern pattern = Pattern.compile(mustNotInclude, (searchDefinition.isCaseSensitive()) ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);

        /** News matches a word that the user did not want to be in */
        if (pattern.matcher(oneLineComplete).find()) {
          success = false;
          break;
        }
      }

      /** The item is not containing words that the user did not want to be in */
      if (success) {

        /** Check for AND / OR searchwords */
        for (int b = 0; b < mustContain.size(); b++) {
          String mayInclude = (String) mustContain.get(b);
          int results = 0;

          /** Create a new pattern with given Case-Sensitivity */
          Pattern pattern = Pattern.compile(mayInclude, (searchDefinition.isCaseSensitive()) ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
          Matcher matcher = pattern.matcher(oneLineComplete);

          /** Find the searchwords and add to highlighting vector */
          while (matcher.find()) {
            item.insertHighlightWord(matcher.group());
            results++;
          }

          /** Break if news does not match the regex */
          if (results == 0) {
            success = false;
            break;
          }
        }
      }

      /** Save news item in searchResultsItems */
      if (success) {
        searchResultsItems.put(item.getTitle(), item);
        searchResultsItemOrder.add(item.getTitle());
      }
    }
    setItemCount(searchResultsItems.size());
  }

  /**
   * <p>
   * Perform the search and add the NewsItems that match the pattern to the
   * searchResultsItems Hashtable.
   * </p>
   * This search is using the Pattern of the Definition directly as RegEx.
   * 
   * @param searchDefinition Pattern and Scope of the Search.
   */
  private void search(SearchDefinition searchDefinition) {

    /** Reset Hashtables */
    searchResultsItems = new Hashtable();
    searchResultsItemOrder = new Vector();

    /** Create a new pattern with given Case-Sensitivity */
    Pattern pattern = Pattern.compile(searchDefinition.getPattern(), (searchDefinition.isCaseSensitive()) ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);

    /** For each news in this channel */
    for (int c = 0; c < newsItemOrder.size(); c++) {
      NewsItem item = (NewsItem) items.get(newsItemOrder.get(c));

      /** Get one line String to search based on SearchDefinition */
      String oneLineComplete = item.toString(searchDefinition);

      /** Clear Vector */
      item.clearHighlightWords();

      /** Search for matches and add into Highlight Word Vector */
      Matcher matcher = pattern.matcher(oneLineComplete);
      while (matcher.find())
        item.insertHighlightWord(matcher.group());

      /** Only show newsItems that match the search */
      if (item.getHighlightWords() != null && item.getHighlightWords().size() > 0) {

        /** Save news item in searchResultsItems */
        searchResultsItems.put(item.getTitle(), item);
        searchResultsItemOrder.add(item.getTitle());
      }
    }
    setItemCount(searchResultsItems.size());
  }
}