package com.packtpub.hibernatesearch.servlet;

import java.io.IOException;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.hibernate.Session;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.packtpub.hibernatesearch.domain.App;
import com.packtpub.hibernatesearch.util.StartupDataLoader;

/**
 * Servlet implementujący kontroler/model obsługujący przeszukiwanie i renderowanie wyników przy pomocy widoku w JSP/JSTL.
 * Adnotacja @Webservlet, dostępna w specyfikacji servletów w wersji 3.0, mapuje ten servlet do URLa "search (np. 
 * "http://localhost:8080/search"). We wcześniejszych wersjach specyfikacji ta konfiguracja znajdowałaby się w pliku "web.xml".
 * Podstawowa logika tej operacji wyszukiwania może być zrefaktoryzowana w celu adaptacji do aplikacji napisanych w Springu, JSFie
 * lub dowolnym innym Javowym frameworku do aplikacji internetowych.
 */
@SuppressWarnings("serial")
@WebServlet("search")
public class SearchServlet extends HttpServlet {

	/**
	 * W tej metodzie zaimplementowane zostały główne funkcjonalności wyszukiwania dla tego servletu. Metoda jest automatycznie wywoływana
	 * przy każdym wywołaniu żądania HTTP POST do zmapowanego adresu URL.
	 */
	@SuppressWarnings("unchecked")
	@Override	
	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		Logger logger = LoggerFactory.getLogger(SearchServlet.class);
		
		// Pobierz słowa kluczowe użytkownika.  Pobierz opcjonalne parametry stronicowania i sortowania lub użyj domyślnych wartości jeśli nie podano.
		String searchString = request.getParameter("searchString") != null ? request.getParameter("searchString").trim() : "";
		String sortField = request.getParameter("sortField") != null ? request.getParameter("sortField").trim() : "relevance";
		int firstResult = request.getParameter("firstResult") != null ? Integer.parseInt(request.getParameter("firstResult")) : 0;
		logger.info("Wyszukuj po [" + searchString + "], sortowanie [" + sortField + "], zacznij od wyniku [" +  firstResult + "]");

		// Rozpocznij sesję Hibernate.
		Session session = StartupDataLoader.openSession();
		
		// Utwórz wrapper Hibernate Search wokół czystej sesji Hibernate
		FullTextSession fullTextSession = Search.getFullTextSession(session);

		// Rozpocznij transkację. W tym przypadku nie jest to niezbędne, ale jest tzw. dobrą praktyką.
		fullTextSession.beginTransaction();

		// Utwórz obiekt typu QueryBuilder pochodzący z Hibernate Search i utwórz go dla odpowiedniego indeksu Lucene (w naszym przypadku indeksu "App").
		QueryBuilder queryBuilder = fullTextSession.getSearchFactory().buildQueryBuilder().forEntity( App.class ).get();
		
		// Użyj QueryBuildera by zbudować w Lucene zapytanie z użyciem słów kluczowych, porównujące słowa kluczowe wprowadzone przez użytkownika z polami 
		// name i description w klasie App, pola name powiązanych aplikacji oraz pola comments wbudowanych obiektów klasy CustomerReview.
		org.apache.lucene.search.Query luceneQuery = null;
		if(searchString.length() > 2 && searchString.startsWith("\"") && searchString.endsWith("\"")) {
			
			// Jeżeli słowa kluczowe wprowadzone przez użytkownika są umieszczone w cudzysłowach, użyj wyszukiwania z użyciem frazy
			String unquotedSearchString = searchString.substring(1, searchString.length() - 1);
			luceneQuery = queryBuilder
					.phrase()
					.onField("name").andField("description").andField("supportedDevices.name").andField("customerReviews.comments")
					.sentence(unquotedSearchString)
					.createQuery();			
		} else {
			
			// Jeżeli słowa kluczowe wprowadzone przez użytkownika nie są umieszczone w cudzysłowach, użyj rozmytego wyszukiwania z użyciem słów kluczowych
			luceneQuery = queryBuilder
					.keyword()
					.fuzzy()
					.withThreshold(0.7f)
					.onFields("name", "description", "supportedDevices.name", "customerReviews.comments")
					.matching(searchString)
					.createQuery();
		}
		FullTextQuery hibernateQuery = fullTextSession.createFullTextQuery(luceneQuery, App.class);  // może być rzutowane do "org.hibernate.Query"

		// Dodaj opcjonalne kryteria sortowania, jeżeli nie zostały ustawione, sortuj wg ważności
		if(sortField.equals("name")) {
			Sort sort = new Sort(new SortField("sorting_name", SortField.STRING));
			hibernateQuery.setSort(sort);
		} else if(sortField.equals("name-reverse")) {
			Sort sort = new Sort(new SortField("sorting_name", SortField.STRING, true));
			hibernateQuery.setSort(sort);
		}

		// Pobierz szacowaną liczbę wyników (UWAGA: nie jest w 100% dokładna, ale nie wymaga odpytania bazy danych 
		// co może być kosztowne przy dużych zbiorach danych).
		int resultSize = hibernateQuery.getResultSize();

		// Wykonaj wyszukiwanie ograniczone do 5 wyników, oraz punktem początkowym w miejscu gdzie poprzednia strona zakończyła
		// prezentację wyników (domyślnie od początku)
		logger.info("Zapytanie == " + hibernateQuery.getQueryString());
		hibernateQuery.setFirstResult(firstResult);
		hibernateQuery.setMaxResults(5);
		List<App> apps = hibernateQuery.list();
		
		// Odłącz wyniki z sesji Hibernate (by uniknąć niechcianej interakcji między warstwą prezentacji i Hibernate,
		// gdy powiązane urządzenia lub wbudowane obiekty typu CustomerReview są przeglądane).
		fullTextSession.clear();

		// Umieść wyniki wyszukiwania w obiekcie żądania HTTP, wraz z parametrami sortowania i stronicowania
		request.setAttribute("searchString", searchString);
		request.setAttribute("sortField", sortField);
		request.setAttribute("apps", apps);
		request.setAttribute("resultSize", resultSize);
		request.setAttribute("firstResult", firstResult);

		// Opróżnij i zamknij sesję Hibernate.
		fullTextSession.getTransaction().commit();
		session.close();
		
		// Przekaż żądanie HTTP, wraz z wynikami wyszukiwania, do widoku zbudowanego JSP/JSTL w celu wyrenderowania strony z wynikami
		getServletContext().getRequestDispatcher("/WEB-INF/pages/search.jsp").forward(request, response);
	}

	/**
	 * Ta metoda jest wywoływana za każdym razem gdy zostanie wywołane żądanie HTTP GET do zmapowanego URLa.  Dla naszego servletu nie ma znaczenia, 
	 * czy zostanie wywołany metodą POST czy GET. W związku z tym przekierowujemy żądanie do metody "doPost()".
	 */
	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {	
		this.doPost(request, response);
	}
	
}
