/*
 * Copyright (C) 2010 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.services.jcr.cluster.functional;

import org.exoplatform.common.http.client.HTTPResponse;
import org.exoplatform.common.http.client.ModuleException;
import org.exoplatform.services.jcr.cluster.BaseClusteringFunctionalTest;
import org.exoplatform.services.jcr.cluster.JCRWebdavConnection;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.jcr.query.Query;
import javax.xml.namespace.QName;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.events.StartElement;

/**
 * Class contains set of query tests for cluster environment
 * 
 * @author <a href="mailto:nikolazius@gmail.com">Nikolay Zamosenchuk</a>
 * @version $Id: WebdavQueryTest.java 34360 2009-07-22 23:58:59Z nzamosenchuk $
 *
 */
public class WebdavQueryTest extends BaseClusteringFunctionalTest
{
   public static final String MIME_TEXT_PLAIN = "text/plain";

   public static final String MIME_TEXT_PATCH = "text/x-patch";

   public static final String MIME_TEXT_HTML = "text/html";

   /**
    * Delay between adding nodes and querying repository (in ms) 
    */
   public static final int SLEEP_BEFORE_QUERY = 6000;

   /**
    * Full-text query tests
    */
   public void testFullTextSearch() throws Exception
   {
      JCRWebdavConnection conn = getConnection();
      // Nodes with some text, with unique words in each one in form of <name><content>
      Map<String, String> nodes = new HashMap<String, String>();
      nodes
         .put(
            "JCR_Overview",
            "A JCR is a type of Object Database tailored to the storage, searching, and retrieval of hierarchical data. The JCR API grew o"
               + "ut of the needs of content management systems, which require storage of documents and other binary objects with associated me"
               + "tadata; however, the API is applicable to many additional types of application. In addition to object storage, the JCR provid"
               + "es: APIs for versioning of data; transactions; observation of changes in data; and import or export of data to XML in a standard way.");
      nodes
         .put(
            "JCR_Structure",
            "The data in a JCR consists of a tree of Nodes with associated Properties. Data is stored in the Properties, which may hold simple "
               + "values such as numbers and strings or binary data of arbitrary length. Nodes may optionally have one or more types associated with"
               + " them which dictate the kinds of properties, number and type of child nodes, and certain behavioral characteristics of the nodes. API");

      nodes
         .put(
            "JCR_Queries",
            "A JCR can be queried with XPathQuery, can export portions of its tree to XML in two standard formats and can import hierarchies directly"
               + " from XML. A JCR may optionally support a standardized form of SQL for queries. The Apache Jackrabbit reference implementation of "
               + "JCR also supports the integration of the Apache Lucene search engine to give full text searches of data in the repository.");

      nodes.put("JCR_Impl",
         "eXo Platform JCR implementation on the company wiki. eXo Platform 2 article on theserverside");
      // add nodes
      for (Entry<String, String> entry : nodes.entrySet())
      {
         conn.addNode(entry.getKey(), entry.getValue().getBytes(), MIME_TEXT_PLAIN);
      }
      // wait for indexer to flush volatile index
      sleep();

      // map containing test-case: <SQL query> : <expected nodes>
      Map<String, String[]> sqlCases = new HashMap<String, String[]>();
      sqlCases.put("SELECT * FROM nt:base WHERE CONTAINS(*,'tailored')", new String[]{"JCR_Overview"});
      sqlCases.put("SELECT * FROM nt:base WHERE CONTAINS(*,'XPathQuery')", new String[]{"JCR_Queries"});
      sqlCases.put("SELECT * FROM nt:resource WHERE CONTAINS(*,'API')", new String[]{"JCR_Structure", "JCR_Overview"});
      assertQuery(sqlCases, Query.SQL);

      // map containing test-case: <XPATH query> : <expected nodes>
      Map<String, String[]> xpathCases = new HashMap<String, String[]>();
      xpathCases.put("//element(*, nt:base)[jcr:contains(.,'tailored')]", new String[]{"JCR_Overview"});
      xpathCases.put("//element(*, nt:base)[jcr:contains(.,'XPathQuery')]", new String[]{"JCR_Queries"});
      xpathCases.put("//element(*, nt:resource)[jcr:contains(.,'API')]", new String[]{"JCR_Structure", "JCR_Overview"});
      assertQuery(xpathCases, Query.XPATH);
      // remove created nodes
      for (Entry<String, String> entry : nodes.entrySet())
      {
         conn.removeNode(entry.getKey());
      }
   }

   /**
    * Simple test, searching nodes by given path and concrete name.
    */
   public void testPathSearch() throws Exception
   {
      String testLocalRootName = "testPathSearch";
      JCRWebdavConnection conn = getConnection();
      conn.addDir(testLocalRootName);
      List<String> expected = new ArrayList<String>();
      expected.add("exoString");
      expected.add("exoBoolean");
      expected.add("exoInteger");
      expected.add("exoLong");
      expected.add("exoFloat");
      expected.add("exoDouble");

      for (String name : expected)
      {
         conn.addNode(testLocalRootName + "/" + name, "_data_".getBytes());
      }
      // wait for indexer to flush volatile index
      sleep();

      // map containing test-case: <SQL query> : <expected nodes>
      Map<String, String[]> sqlCases = new HashMap<String, String[]>();
      sqlCases.put("SELECT * FROM nt:base WHERE jcr:path LIKE '/" + testLocalRootName
         + "[%]/%' AND NOT jcr:path LIKE '/" + testLocalRootName + "[%]/%/%' ", expected.toArray(new String[expected
         .size()]));
      sqlCases.put("SELECT * FROM nt:base WHERE fn:name() = 'exoString'", new String[]{"exoString"});
      assertQuery(sqlCases, Query.SQL);

      // map containing test-case: <XPATH query> : <expected nodes>
      Map<String, String[]> xpathCases = new HashMap<String, String[]>();
      xpathCases.put("/jcr:root/" + testLocalRootName + "/ element(*, nt:base)", expected.toArray(new String[expected
         .size()]));
      xpathCases.put("//element(*,nt:file)[fn:name() = 'exoString']", new String[]{"exoString"});
      assertQuery(xpathCases, Query.XPATH);

      conn.removeNode(testLocalRootName);
   }

   /**
    * Test, searching over the repository nodes with concrete value of concrete property.
    * jcr:mimeType is used for querying purposes. 
    */
   public void testPropertyValueSearch() throws Exception
   {
      JCRWebdavConnection conn = getConnection();
      // Nodes with concrete mimetype in form of <name><content>
      Map<String, String> nodes = new HashMap<String, String>();
      // text/plain
      nodes.put("TextDescription", MIME_TEXT_PLAIN);
      nodes.put("SmallNote", MIME_TEXT_PLAIN);
      nodes.put("CalendarMemo", MIME_TEXT_PLAIN);
      nodes.put("GetThisDone", MIME_TEXT_PLAIN);
      // text/patch
      nodes.put("CriticalPath", MIME_TEXT_PATCH);
      nodes.put("BrokenPatch", MIME_TEXT_PATCH);
      // text/html
      nodes.put("FirstPage", MIME_TEXT_HTML);
      nodes.put("AboutGateIn", MIME_TEXT_HTML);
      nodes.put("LicenseAgreement", MIME_TEXT_HTML);
      nodes.put("HomePage", MIME_TEXT_HTML);
      nodes.put("StrangePage", MIME_TEXT_HTML);

      // add nodes
      for (Entry<String, String> entry : nodes.entrySet())
      {
         conn.addNode(entry.getKey(), "content".getBytes(), entry.getValue());
      }
      // wait for indexer to flush volatile index
      sleep();

      // map containing test-case: <SQL query> : <expected nodes>
      Map<String, String[]> sqlCases = new HashMap<String, String[]>();
      sqlCases.put("SELECT * FROM nt:resource WHERE jcr:mimeType ='" + MIME_TEXT_PLAIN + "'", getNodesByMime(nodes,
         MIME_TEXT_PLAIN));
      sqlCases.put("SELECT * FROM nt:resource WHERE jcr:mimeType ='" + MIME_TEXT_HTML + "'", getNodesByMime(nodes,
         MIME_TEXT_HTML));
      sqlCases.put("SELECT * FROM nt:resource WHERE jcr:mimeType LIKE 'text%'", nodes.keySet().toArray(
         new String[nodes.size()]));
      assertQuery(sqlCases, Query.SQL);

      // map containing test-case: <XPATH query> : <expected nodes>
      Map<String, String[]> xpathCases = new HashMap<String, String[]>();
      xpathCases.put("//element(*,nt:resource)[@jcr:mimeType='" + MIME_TEXT_PLAIN + "']", getNodesByMime(nodes,
         MIME_TEXT_PLAIN));
      xpathCases.put("//element(*,nt:resource)[@jcr:mimeType='" + MIME_TEXT_HTML + "']", getNodesByMime(nodes,
         MIME_TEXT_HTML));
      xpathCases.put("//element(*,nt:resource)[jcr:like(@jcr:mimeType, 'text%')]", nodes.keySet().toArray(
         new String[nodes.size()]));
      assertQuery(xpathCases, Query.XPATH);

      // remove created nodes
      for (Entry<String, String> entry : nodes.entrySet())
      {
         conn.removeNode(entry.getKey());
      }
   }

   /**
    * Performs sequence of queries and asserts received results
    * 
    * @param conn
    * @param queryCases
    *          map containing test-case: <query> : <expected nodes>
    * @param lang
    *          Query.SQL or Query.XPATH
    * @throws IOException
    * @throws ModuleException
    * @throws XMLStreamException
    * @throws FactoryConfigurationError
    */
   private void assertQuery(Map<String, String[]> queryCases, String lang) throws IOException, ModuleException,
      XMLStreamException, FactoryConfigurationError
   {
      if (lang.equals(Query.SQL) || lang.equals(Query.XPATH))
      {
         for (JCRWebdavConnection connection : getConnections())
         {
            for (Entry<String, String[]> entry : queryCases.entrySet())
            {
               HTTPResponse response =
                  lang.equals(Query.SQL) ? connection.sqlQuery(entry.getKey()) : connection.xpathQuery(entry.getKey());
               assertEquals(207, response.getStatusCode());
               List<String> found;
               assertEquals(207, response.getStatusCode());
               found = parseNodeNames(response.getData());
               assertTrue("Lists are not equals:\n*found:\t" + found + "\n*expected:\t" + Arrays.asList(entry.getValue()),
                  compareLists(Arrays.asList(entry.getValue()), found));
            }
         }
      }
      else
      {
         fail("Unsupported query language:" + lang);
      }
   }

   /**
    * Given map nodesMap should contain entry: <nodeName>:<mime-type>, this method returns array with names of 
    * nodes that are only of given mime-type.
    * 
    * @param nodesMap
    * @param mime
    * @return
    */
   private String[] getNodesByMime(Map<String, String> nodesMap, String mime)
   {
      List<String> filteredNodes = new ArrayList<String>();
      for (Entry<String, String> entry : nodesMap.entrySet())
      {
         if (entry.getValue().equals(mime))
         {
            filteredNodes.add(entry.getKey());
         }
      }
      return filteredNodes.toArray(new String[filteredNodes.size()]);
   }

   /**
    * returns true if lists are equals (order doesn't matter)
    * 
    * @param expected
    * @param found
    * @return
    */
   private boolean compareLists(Collection<String> expected, Collection<String> found)
   {
      if (expected == null || found == null)
      {
         return false;
      }
      return expected.containsAll(found) && found.containsAll(expected);
   }

   /**
    * Extracts names of nodes from response XML
    * 
    * @param data
    * @return
    * @throws XMLStreamException
    * @throws FactoryConfigurationError
    * @throws IOException
    */
   private List<String> parseNodeNames(byte[] data) throws XMLStreamException, FactoryConfigurationError, IOException
   {
      // flag, that notifies when parser is inside <D:displayname></D:displayname> 
      boolean displayName = false;
      //Set<String> nodes = new HashSet<String>();
      List<String> nodes = new ArrayList<String>();
      InputStream input = new ByteArrayInputStream(data);
      XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(input);
      QName name = QName.valueOf("{DAV:}displayname");
      try
      {
         while (reader.hasNext())
         {
            int eventCode = reader.next();
            switch (eventCode)
            {
               case StartElement.START_ELEMENT : {
                  // if {DAV:}displayname opening element 
                  if (reader.getName().equals(name))
                  {
                     displayName = true;
                  }
                  break;
               }
               case StartElement.CHARACTERS : {
                  if (displayName)
                  {
                     // currently reader is inside <D:displayname>nodeName</D:displayname>
                     // adding name to list if not empty
                     String nodeName = reader.getText();
                     if (nodeName != null && !nodeName.equals(""))
                     {
                        nodes.add(nodeName);
                     }
                  }
                  break;
               }
               default : {
                  displayName = false;
                  break;
               }
            }
         }
      }
      finally
      {
         reader.close();
         input.close();
      }
      return new ArrayList<String>(nodes);
   }

   /**
    * Sleep for SLEEP_BEFORE_QUERY seconds. This is needed because Indexer is asynchronous and 
    * volatile index can be flushed after some time. 
    */
   private void sleep()
   {
      try
      {
         Thread.sleep(SLEEP_BEFORE_QUERY);
      }
      catch (InterruptedException e)
      {
      }
   }
}
