/*   **********************************************************************  **
 **   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.dao.feedparser;

import net.sourceforge.rssowl.dao.NewsfeedFactoryException;
import net.sourceforge.rssowl.model.Channel;
import net.sourceforge.rssowl.model.NewsItem;
import net.sourceforge.rssowl.util.shop.StringShop;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.Text;

import java.util.Iterator;
import java.util.List;

/**
 * Abstract class of all parser classes in RSSOwl. Provides some methods to
 * access values of the XML document. Is also responsible to parse Namespaces if
 * available.
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public abstract class AbstractFeedParser {

  /** Namespace: Content */
  protected Namespace content;

  /** Namespace: Dublin Core */
  protected Namespace dc;

  /** Namespace: Default */
  protected Namespace defNs;

  /** Current document */
  protected Document document;

  /** Namespace: RDF */
  protected Namespace rdf;

  /** Root Element */
  protected Element root;

  /** Channel to fill with data */
  protected Channel rssChannel;

  /** Namespace: Syndication */
  protected Namespace sy;

  /** URL of the Newsfeed */
  protected String url;

  /**
   * Instantiate a new Parser for this format. Parse for Namespaces.
   * 
   * @param document The document containing the data to parse
   * @param rssChannel The Channel to fill with data from the document
   * @param url The URL of the Newsfeed that is parsed
   * @throws NewsfeedFactoryException If any error occurs
   */
  protected AbstractFeedParser(Document document, Channel rssChannel, String url) throws NewsfeedFactoryException {
    this.document = document;
    this.rssChannel = rssChannel;
    this.url = url;
    rssChannel.setLink(url);
    rssChannel.setDocument(document);

    /** Check if document is ok */
    if (document == null)
      throw new NewsfeedFactoryException(url, null, null, NewsfeedFactoryException.ERROR_INVALID_XML);

    /** Parse Root Element */
    parseRoot();

    /** Check if root element is ok */
    if (root == null)
      throw new NewsfeedFactoryException(url, null, null, NewsfeedFactoryException.ERROR_INVALID_XML);

    /** Parse available Namespaces */
    parseAvailableNamespaces();
  }

  /**
   * Instantiate a new Parser for this format.
   * 
   * @param document The document containing the data to parse
   * @param rssChannel The Channel to fill with data from the document
   * @param url The URL of the Newsfeed that is parsed
   * @param nameSpaces Possible Namespaces of the XML document
   * @throws NewsfeedFactoryException If any error occurs
   */
  protected AbstractFeedParser(Document document, Channel rssChannel, String url, Namespace nameSpaces[]) throws NewsfeedFactoryException {
    this.document = document;
    this.rssChannel = rssChannel;
    this.url = url;
    rssChannel.setLink(url);
    rssChannel.setDocument(document);

    /** Apply Namespaces */
    if (nameSpaces.length == 5) {
      content = nameSpaces[0];
      dc = nameSpaces[1];
      defNs = nameSpaces[2];
      rdf = nameSpaces[3];
      sy = nameSpaces[4];
    }

    /** Parse Root Element */
    parseRoot();

    /** Check if root element is ok */
    if (root == null)
      throw new NewsfeedFactoryException(url, null, null, NewsfeedFactoryException.ERROR_INVALID_XML);
  }

  /** Parse document for namespaces */
  private void parseAvailableNamespaces() {
    defNs = Namespace.NO_NAMESPACE;

    /** Check namespace of root Element */
    Namespace ns = root.getNamespace();
    if (ns.getPrefix().equals("rdf"))
      rdf = ns;
    else if (ns.getPrefix().equals("sy"))
      sy = ns;
    else if (ns.getPrefix().equals("dc"))
      dc = ns;
    else if (ns.getPrefix().equals("content"))
      content = ns;
    else if (ns.getPrefix().equals(""))
      defNs = ns;

    /** Get additional namespaces */
    Iterator namespaces = root.getAdditionalNamespaces().iterator();
    while (namespaces.hasNext()) {
      Namespace namespace = (Namespace) namespaces.next();

      /** Dublin Core */
      if (namespace.getPrefix().equalsIgnoreCase("dc"))
        dc = namespace;

      /** Syndication */
      else if (namespace.getPrefix().equalsIgnoreCase("sy"))
        sy = namespace;

      /** RDF */
      else if (namespace.getPrefix().equalsIgnoreCase("rdf"))
        rdf = namespace;

      /** Content */
      else if (namespace.getPrefix().equalsIgnoreCase("content"))
        content = namespace;

      /** Default Namespace */
      else if (namespace.getPrefix().equalsIgnoreCase(""))
        defNs = namespace;
    }
  }

  /**
   * Parse the Root Element from the XML document.
   * 
   * @throws NewsfeedFactoryException If the root element is not set.
   */
  private void parseRoot() throws NewsfeedFactoryException {
    try {
      root = document.getRootElement();
    } catch (IllegalStateException e) {
      throw new NewsfeedFactoryException(url, null, null, NewsfeedFactoryException.ERROR_INVALID_XML);
    }
  }

  /**
   * Get an attribute value from the document
   * 
   * @param element The element's name
   * @param attribute The attributes name
   * @return String the value or null
   */
  protected String getAttributeValue(Element element, String attribute) {
    if (element == null)
      return null;

    String attribValue = element.getAttributeValue(attribute);
    if (attribValue != null)
      return attribValue;

    /** Try with default Namespace */
    return getAttributeValue(element, attribute, defNs);
  }

  /**
   * Get an attribute value from the document
   * 
   * @param element The element's name
   * @param attribute The attributes name
   * @param ns The namespace for the attribute
   * @return String the value or null
   */
  protected String getAttributeValue(Element element, String attribute, Namespace ns) {
    if (element == null)
      return null;

    return element.getAttributeValue(attribute, ns);
  }

  /**
   * Get the Child Element of a parent Element
   * 
   * @param parent The parent of the child
   * @param name The name of the child
   * @return Element The child element if existing
   */
  protected Element getChildElement(Element parent, String name) {
    if (parent == null)
      return null;

    Element child = parent.getChild(name);
    if (child != null)
      return child;

    /** Try with default Namespace */
    return getChildElement(parent, name, defNs);
  }

  /**
   * Get the Child Element of a parent Element
   * 
   * @param parent The parent of the child
   * @param name The name of the child
   * @param ns The namespace for the child
   * @return Element The child element if existing
   */
  protected Element getChildElement(Element parent, String name, Namespace ns) {
    if (parent == null)
      return null;

    return parent.getChild(name, ns);
  }

  /**
   * Get a list of children from the given element.
   * 
   * @param parent The parent element
   * @param name The name of the children
   * @return List A list of child elements
   */
  protected List getChildren(Element parent, String name) {
    if (parent == null)
      return null;

    List children = parent.getChildren(name);
    if (!children.isEmpty())
      return children;

    return getChildren(parent, name, defNs);
  }

  /**
   * Get a list of children from the given element.
   * 
   * @param parent The parent element
   * @param name The name of the children
   * @param ns The namespace
   * @return List A list of child elements
   */
  protected List getChildren(Element parent, String name, Namespace ns) {
    if (parent == null)
      return null;

    return parent.getChildren(name, ns);
  }

  /**
   * Get a value from the document
   * 
   * @param element The element's name
   * @param parent The parent element
   * @return String the value or null
   */
  protected String getChildValue(String element, Element parent) {
    if (parent == null)
      return null;

    Element child = parent.getChild(element);
    if (child != null)
      return child.getTextTrim();

    /** Try with default Namespace */
    return getChildValue(element, parent, defNs);
  }

  /**
   * Get a value from the document
   * 
   * @param element The element's name
   * @param parent The parent element
   * @param ns The Namespace to use
   * @return String the value or null
   */
  protected String getChildValue(String element, Element parent, Namespace ns) {
    if (parent == null)
      return null;

    Element child = parent.getChild(element, ns);
    if (child != null)
      return child.getTextTrim();

    return null;
  }

  /**
   * Parse elements that are defined in the specifications and add them to the
   * Channel.
   * 
   * @throws NewsfeedFactoryException If any error occurs
   */
  protected abstract void parse() throws NewsfeedFactoryException;

  /**
   * Parse the content module namespace
   * 
   * @param item Current working item
   * @param newsItem Current working newsItem
   */
  protected void parseContentModule(Element item, NewsItem newsItem) {

    /** Temp String */
    String str;

    /** Parse Elemenets with Content Module namespace */
    if (content != null) {

      /** Encoded of the NewsItem */
      str = getChildValue("encoded", item, content);

      if (StringShop.isset(str))
        newsItem.setDescription(str);
    }
  }

  /**
   * Parse for the dublin core module namespace
   * 
   * @param item Current working item
   * @param newsItem Current working newsitem
   */
  protected void parseDCModule(Element item, NewsItem newsItem) {

    /** Temp String */
    String str;

    /** Parse Elemenets with Dublin Core Module namespace */
    if (dc != null) {

      /** Title of the NewsItem */
      str = getChildValue("title", item, dc);
      if (StringShop.isset(str))
        newsItem.setTitle(str);

      /** Description of the NewsItem */
      str = getChildValue("description", item, dc);
      if (StringShop.isset(str))
        newsItem.setDescription(str);

      /** Set part of description as title if title is not available */
      if ((newsItem.getTitle() == null || newsItem.getTitle().equals("")) && newsItem.getDescription() != null)
        newsItem.setTitle(newsItem.getDescription(), true);

      /** Date of the NewsItem */
      str = getChildValue("date", item, dc);
      if (StringShop.isset(str)) {
        newsItem.setPubDate(str, true);
        rssChannel.addAvailableNewsItemInfo("TABLE_HEADER_PUBDATE");
      }

      /** publisher of the NewsItem */
      str = getChildValue("publisher", item, dc);
      if (StringShop.isset(str)) {
        newsItem.setPublisher(str);
        rssChannel.addAvailableNewsItemInfo("TABLE_HEADER_PUBLISHER");
      }

      /** Creator of the NewsItem equals author */
      str = getChildValue("creator", item, dc);
      if (StringShop.isset(str)) {
        newsItem.setAuthor(str);
        rssChannel.addAvailableNewsItemInfo("TABLE_HEADER_AUTHOR");
      }

      /** Source of the NewsItem equals author */
      str = getChildValue("source", item, dc);
      if (StringShop.isset(str))
        newsItem.setSource(str);

      /** Category of the newsitem */
      str = getChildValue("subject", item, dc);
      if (StringShop.isset(str)) {
        newsItem.setCategory(str);
        rssChannel.addAvailableNewsItemInfo("TABLE_HEADER_CATEGORY");
      }
    }

    /** Dublin Core Namespace might be declared in child element */
    else {
      List childs = item.getChildren();

      /** Foreach child of this item Element */
      for (int a = 0; a < childs.size(); a++) {
        Element child = (Element) childs.get(a);
        boolean isDC = child.getNamespacePrefix().equals("dc");

        /** Only consider Dublin Core Elements */
        if (isDC) {

          /** Title of the NewsItem */
          if (child.getName().equals("title") && !child.getValue().equals(""))
            newsItem.setTitle(child.getValue());

          /** Description of the newsitem */
          if (child.getName().equals("description") && !child.getValue().equals(""))
            newsItem.setDescription(Text.normalizeString(child.getValue()));

          /** Set part of description as title if title is not available */
          if ((newsItem.getTitle() == null || newsItem.getTitle().equals("")) && newsItem.getDescription() != null)
            newsItem.setTitle(newsItem.getDescription(), true);

          /** Date of the NewsItem */
          if (child.getName().equals("date") && !child.getValue().equals("")) {
            newsItem.setPubDate(child.getValue(), true);
            rssChannel.addAvailableNewsItemInfo("TABLE_HEADER_PUBDATE");
          }

          /** Publisher of the NewsItem */
          if (child.getName().equals("publisher") && !child.getValue().equals("")) {
            newsItem.setPublisher(child.getValue());
            rssChannel.addAvailableNewsItemInfo("TABLE_HEADER_PUBLISHER");
          }

          /** Source of the NewsItem */
          if (child.getName().equals("source") && !child.getValue().equals(""))
            newsItem.setSource(child.getValue());

          /** Creator of the NewsItem */
          if (child.getName().equals("creator") && !child.getValue().equals("")) {
            newsItem.setAuthor(child.getValue());
            rssChannel.addAvailableNewsItemInfo("TABLE_HEADER_AUTHOR");
          }

          /** Category of the newsitem */
          if (child.getName().equals("subject") && !child.getValue().equals("")) {
            newsItem.setCategory(child.getValue());
            rssChannel.addAvailableNewsItemInfo("TABLE_HEADER_CATEGORY");
          }
        }
      }
    }
  }
}