/***************************************************************************
    Copyright          : (C) 2002 by Neoworks Limited. All rights reserved
    URL                : http://www.neoworks.com
 ***************************************************************************/
/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

package com.neoworks.jukex.client.html.standard;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
import java.util.regex.PatternSyntaxException;

import com.neoworks.rdc.ParserException;
import com.neoworks.util.*;
import com.neoworks.jukex.*;
import com.neoworks.jukex.query.JukeXParser;
import com.neoworks.jukex.query.Query;
import com.neoworks.jukex.query.AttributeValueResultSet;
import com.neoworks.jukex.tracksource.*;
import com.neoworks.jukex.tracksource.filter.*;
import com.neoworks.jukex.sqlimpl.InMemoryPlaylist;

import org.apache.log4j.Category;

/**
 * User servlet interface for JukeX.
 *
 * @author Nigel Atkinson (<a href="mailto:nigel@neoworks.com">nigel@neoworks.com</a>)
 */
public class JukeXServlet extends HttpServlet
{
	private static final Category log = Category.getInstance(JukeXServlet.class.getName());
	private static final boolean logDebugEnabled = log.isDebugEnabled();
	private static final boolean logInfoEnabled = log.isInfoEnabled();
	
	private static final String PIPELINE_NAME = "servlet";
	private static final String CONTROL_PLAYBACK_NAME = "office";
	
	private static final String[] alphabet = {"Other","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"};
	private TrackStore ts = null;
	private String contextRootURL = null;
	private TrackSourcePipeline pipeline = null;
	private static int MAX_BROWSE_COLUMNS = 10;

	/**
	 * Handle a POST request
	 *
	 * @param req The HTTP request
	 * @param res The HTTP response
	 */
	public void doPost(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
	{
		doGet(req,res);
	}

	/**
	 * Handle a GET request
	 *
	 * @param req The HTTP request
	 * @param res The HTTP response
	 */
	public void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
	{
		if (logDebugEnabled) log.debug("JukeX Standard interface Servlet");
		pipeline = TrackSourcePipeline.getPipeline( this.PIPELINE_NAME );
		ControlPlaybackManager cpm = new ControlPlaybackManager();
		ControlPlayback sh = cpm.getControlPlayback(CONTROL_PLAYBACK_NAME);
		int upcomingTrackCount = 5;
		
		// Setup the session
		HttpSession session = req.getSession();
		synchronized ( session )
		{
			if ( session.getAttribute("personalPlaylist") == null ) 
			{
				if (logDebugEnabled) log.debug("Setting up personal playlist");
				this.setupPersonalPlaylist( session );
			}
			else
			{
				if (logDebugEnabled) log.debug("Personal playlist found");
			}
			if ( session.getAttribute("upcomingTrackCount") == null)
			{
				if (logDebugEnabled) log.debug("Setting upcoming track count");
				session.setAttribute("upcomingTrackCount",new Integer(upcomingTrackCount));
			}
			else
			{
				upcomingTrackCount = ((Integer)session.getAttribute("upcomingTrackCount")).intValue();
				if (logDebugEnabled) log.debug("Showing " + upcomingTrackCount + " tracks in upcoming list");
			}
		}
		
		contextRootURL = req.getContextPath()+"/";
		String view = req.getParameter("view");
		String action = req.getParameter("action");
		RequestDispatcher rd = null;

		if (action != null)
		{
			ts = TrackStoreFactory.getTrackStore();
		
			if (action.equals("addtrack.personal"))
			{
				Playlist myPlaylist = (Playlist) session.getAttribute( "personalPlaylist" );
				long id = Long.parseLong( req.getParameter( "trackid" ) );
				Track addTrack = ts.getTrack( id );
				myPlaylist.add( addTrack );
				if (logDebugEnabled) log.debug( "Personal playlist has "+myPlaylist.size()+" tracks" );
			}
			else if (action.equals("browse.setcolumns")) 
			{
				String columnAttributeName = null;
				List columnAttributes = new ArrayList();
				for (int i = 0; i < MAX_BROWSE_COLUMNS; i++)
				{
					columnAttributeName = req.getParameter("browse.column." + String.valueOf(i));
					if (columnAttributeName.equals("none"))
					{
						break;
					}
					columnAttributes.add(ts.getAttribute(columnAttributeName));
				}
				columnAttributes = CollectionUtils.distinctList(columnAttributes);
				session.setAttribute("browseColumnAttributes",columnAttributes);
				String useGroupingStr = req.getParameter("browse.usegrouping");
				if (logDebugEnabled) log.debug("Use grouping checkbox is " + useGroupingStr);
				if (useGroupingStr != null)
				{
					session.setAttribute("browseUseGrouping", new Boolean(useGroupingStr.toUpperCase().equals("ON")));
				} else {
					session.setAttribute("browseUseGrouping", new Boolean(false));
				}
			}
			else if (action.equals("browse.sort")) 
			{
				String attributeName = req.getParameter("browse.sortattribute");
				if (attributeName != null)
				{
					session.setAttribute("browseSortAttribute", ts.getAttribute(attributeName));
				}
			}
			else if (action.equals("updateattributes"))
			{
				int trackId = Integer.parseInt(req.getParameter("trackid"));
				Track t = ts.getTrack(trackId);
				Collection attributes = ts.getAttributes();
				Iterator i = attributes.iterator();
				Attribute a = null;
				String attributeValue = null;
				while (i.hasNext())
				{
					a = (Attribute)i.next();
					attributeValue = req.getParameter("editattributes." + a.getName());
					if (attributeValue != null && !attributeValue.equals(""))
					{
						if (a.getType() == Attribute.TYPE_STRING)
						{
							t.replaceAttributeValues(a,a.getAttributeValue(attributeValue));
						} else {
							try {
								t.replaceAttributeValues(a,a.getAttributeValue(Integer.parseInt(attributeValue)));
							} catch (NumberFormatException nfe) {
								if (logDebugEnabled) log.debug("Badly formatted number in attribute update",nfe);
							}
						}
					} else {
						//remove attribute values here
					}
				}
			}
			else if (action.equals("filter.remove"))
			{
				int index = Integer.parseInt(req.getParameter("filter.index"));
				int filterIndex = Integer.parseInt(req.getParameter("filter.filterindex"));
				FilterPipelineElement fpe = (FilterPipelineElement)pipeline.get(index);
				fpe.removeFilter(filterIndex);
				pipeline.storePipeline();
			}
			else if (action.equals("filter.add")) 
			{
				int index = Integer.parseInt(req.getParameter("filter.index"));
				String filterAttribute = req.getParameter("filter.attribute");
				String filterValue = req.getParameter("filter.value");
				Attribute a = ts.getAttribute(filterAttribute);
				AttributeValue av = null;
				if (a.getType() == Attribute.TYPE_STRING)
				{
					av = a.getAttributeValue(filterValue);
				} else {
					av = a.getAttributeValue(Integer.parseInt(filterValue));
				}
				TrackFilter f = null;
				String filterType = req.getParameter("filter.type");
				try {
					if (filterType.equals("equals"))
					{
						f = new AttributeEqualityTrackFilter(a,av);
					} else if (filterType.equals("startswith")) {
						f = new AttributeStartsWithTrackFilter(a,av);
					} else if (filterType.equals("matches")) {
							f = new AttributeRegexTrackFilter(a,av);
					}
					FilterPipelineElement fpe = (FilterPipelineElement)pipeline.get(index);
					fpe.addFilter(f);
					pipeline.storePipeline();
				} catch (PatternSyntaxException e) {
					log.warn("Could not compile regex" , e);
				}
			}
			else if (action.equals("pplaylist.remove"))
			{
				int index = Integer.parseInt(req.getParameter("pplaylist.index"));
				Playlist myPlaylist = (Playlist) session.getAttribute( "personalPlaylist" );
				try {
					myPlaylist.removeTrack(index);
				} catch (IndexOutOfBoundsException e) {
					log.warn("IndexOutOfBoundsException while attempting to remove track from personal playlist: ", e);
				}
			}
			else if (action.equals("skip")) 
			{
				sh.skip();
			} 
			else if (action.equals("pause")) 
			{
				sh.pause();
			}
			else if (action.equals("play")) 
			{
				if (logDebugEnabled) log.debug("setting track source");
				sh.setTrackSource((TrackSource)pipeline);
				if (logDebugEnabled) log.debug("play...");
				sh.play();
				try {
					Thread.sleep(250);
				} catch (InterruptedException e) { }
			} 
			else if (action.equals("stop")) 
			{
				sh.cpStop();
			} 
			else 
			{
				req.setAttribute("pageContent","<h1>Unknown Action: " + action + "</h1>");
			}
		}

		if (view == null)
		{
			view = "index";
		}
		if ( view.equals("browse") )
		{
			// Browse JSP invoked here.
			//if (logDebugEnabled) log.debug("Viewing browse page...");
			rd = handleBrowse( req , res );
			//if (logDebugEnabled) log.debug("rd="+rd);
		}
		else if ( view.equals("browsecolumns") )
		{
			// JSP invoked here.
			rd = handleBrowseColumns( req , res);
		}
		else if ( view.equals("editattributes") )
		{
			// JSP invoked here.
			rd = handleEditTrackAttributes( req , res , Long.parseLong(req.getParameter("trackid")));
		}
		else if ( view.equals("trackattributes") )
		{
			// JSP invoked here.
			rd = handleTrackAttributes( req , res , Long.parseLong(req.getParameter("trackid")));
		}
		else if ( view.equals("advancedsearch") )
		{
			rd = handleAdvancedSearch(req, res, buildQueryTable(req));
		}
		else if ( view.equals("search") )
		{
			rd = handleSearch(req,res);
		}
		else if ( view.equals("filter") )
		{
			// JSP invoked here.
			rd = handleFilter( req , res );
		}
		else if ( view.equals("left")  || view.equals("sidebar"))
		{
			if (logDebugEnabled) log.debug("Viewing left frame");
			if (view.equals("sidebar"))
			{
				rd = req.getRequestDispatcher("/sidebarFrame.jsp");
			} else {
				rd = req.getRequestDispatcher("/leftFrame.jsp");
			}
		} 
		else if ( view.equals("topleft") || view.equals("topsidebar")) 
		{
			Long remainingPlayTime = null;
			if (sh.isPaused())
			{
				// 30 seconds
				remainingPlayTime = new Long(30000);
			} else {
				remainingPlayTime = new Long(sh.getRemainingPlayingTime());
			}
			req.setAttribute( "timeremaining" , remainingPlayTime );
			req.setAttribute( "playing" , sh.getPlaying() );
			req.setAttribute( "upcoming" , pipeline.peekTracks(upcomingTrackCount) );
			req.setAttribute( "paused" , new Boolean(sh.isPaused()) );
			Playlist personalPlaylist = (Playlist) req.getSession().getAttribute( "personalPlaylist" );
			Playlist copyPlaylist = null;
			synchronized(personalPlaylist)
			{
				copyPlaylist = (Playlist)personalPlaylist.clone();
			}
			req.setAttribute( "pplaylist" , copyPlaylist );

			if (view.equals("topleft"))
			{
				rd = req.getRequestDispatcher("/topLeftFrame.jsp");
			} else {
				rd = req.getRequestDispatcher("/topSidebarFrame.jsp");
			}
		}
		else if ( view.equals("bottomleft") || view.equals("bottomsidebar") )
		{
			req.setAttribute( "playing" , sh.getPlaying() );
			req.setAttribute( "paused" , new Boolean(sh.isPaused()) );

			if (view.equals("bottomleft"))
			{
				rd = req.getRequestDispatcher("/bottomLeftFrame.jsp");
			} else {
				rd = req.getRequestDispatcher("/bottomSidebarFrame.jsp");
			}
		}
		else if ( view.equals("right") ) 
		{
			if (logDebugEnabled) log.debug("Viewing right frame");
			rd = req.getRequestDispatcher("/rightFrame.jsp");
		}
		else if ( view.equals("index") ) 
		{
			if (logDebugEnabled) log.debug("front page");
			rd = req.getRequestDispatcher("/index.jsp");
		}
		else
		{
			rd = req.getRequestDispatcher("/unknown.jsp");
		}
				
		ClassLoader loader = Thread.currentThread().getContextClassLoader();
		req.setAttribute("resources", ResourceBundle.getBundle("InterfaceResources", req.getLocale(), loader));
		rd.forward(req,res);
	}

	private List buildQueryTable(HttpServletRequest req)
	{
		if (logDebugEnabled) log.debug("Building query table...");
		List retVal = new ArrayList();
		TrackStore ts = TrackStoreFactory.getTrackStore();
		for (int i = 0; i < 10; i++)
		{
			for (int j = 0; j < 10; j++)
			{
				if (logDebugEnabled) log.debug("checking for parameter [asearch.attribute." + i + "." + j + "]");
				if (req.getParameter("asearch.attribute." + i + "." + j) != null && !req.getParameter("asearch.attribute." + i + "." + j).equals(""))
				{
					if (logDebugEnabled) log.debug("Found parameter");
					try {
						retVal.get(i);
					} catch (IndexOutOfBoundsException e) {
						if (logDebugEnabled) log.debug("Adding row to query table at " + i);
						retVal.add(new ArrayList());
					}
					((List)retVal.get(i)).add(new AttributeQuery(ts.getAttribute(req.getParameter("asearch.attribute." + i + "." + j)),req.getParameter("asearch.comparator." + i + "." + j),req.getParameter("asearch.text." + i + "." + j)));
				} else {
					break;
				}
			}
		}

		if (logDebugEnabled) log.debug("Getting new sub query (" + req.getParameter("asearch.attribute") + " | " + req.getParameter("asearch.comparator") + " | " + req.getParameter("asearch.text") + " )");
		AttributeQuery aq = new AttributeQuery(ts.getAttribute(req.getParameter("asearch.attribute")),req.getParameter("asearch.comparator"),req.getParameter("asearch.text"));
		if (req.getParameter("asearch.relop") != null && req.getParameter("asearch.relop").equals("OR")) {
			if (logDebugEnabled) log.debug("Adding OR term");
			if (retVal.size() > 0)
			{
				((List)retVal.get(retVal.size() - 1)).add(aq);
			} else {
				List subQuery = new ArrayList();
				subQuery.add(aq);
				retVal.add(subQuery);
			}
		} else if ((req.getParameter("asearch.attribute") != null) && !( (req.getParameter("asearch.relop") != null) && (req.getParameter("asearch.relop").equals(" ")) ) ) {
			if (logDebugEnabled) log.debug("Adding AND term");
			List subQuery = new ArrayList();
			subQuery.add(aq);
			retVal.add(subQuery);
		}
		return retVal;
	}

	private RequestDispatcher handleBrowseColumns(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
	{
		req.setAttribute("servlet", "JukeX");
		req.setAttribute("allAttributes", TrackStoreFactory.getTrackStore().getAttributes());
		req.setAttribute("maxBrowseColumns", new Integer(MAX_BROWSE_COLUMNS));
		req.setAttribute("queryString", req.getParameter("query.letter"));
		req.setAttribute("useGrouping", req.getSession().getAttribute("browseUseGrouping"));
		req.setAttribute("browseColumnAttributes", req.getSession().getAttribute("browseColumnAttributes"));
		if ( req.getParameter("returnview")!=null && !req.getParameter("returnview").equals("") )
		{
			req.setAttribute("returnView", req.getParameter("returnview"));
		}
		return req.getRequestDispatcher("/browsecolumns.jsp");
	}

	private RequestDispatcher handleEditTrackAttributes(HttpServletRequest req, HttpServletResponse res, long id) throws ServletException, IOException
	{
		TrackStore ts = TrackStoreFactory.getTrackStore();
		req.setAttribute("attributes", ts.getAttributes());
		req.setAttribute("track", ts.getTrack(id));
		return req.getRequestDispatcher("/edittrackattributes.jsp");
	}

	private RequestDispatcher handleTrackAttributes(HttpServletRequest req, HttpServletResponse res, long id) throws ServletException, IOException
	{
		TrackStore ts = TrackStoreFactory.getTrackStore();
		req.setAttribute("attributes", ts.getAttributes());
		req.setAttribute("track", ts.getTrack(id));
		return req.getRequestDispatcher("/trackattributes.jsp");
	}

	private RequestDispatcher handleAdvancedSearch(HttpServletRequest req, HttpServletResponse res, List queryTable) throws ServletException, IOException
	{
		RequestDispatcher retVal = req.getRequestDispatcher("/advancedsearch.jsp");
		TrackStore ts = TrackStoreFactory.getTrackStore();
		Collection attributes = ts.getAttributes();
		req.setAttribute("attributes", attributes);

		HttpSession session = SearchHelper.prepareSearchSession(req);
		List columnAttributes = (List)session.getAttribute("browseColumnAttributes");

		if (req.getParameter("asearch.clearquery") == null && queryTable.size() > 0)
		{
			if (logDebugEnabled) log.debug("Not restarting query");
			req.setAttribute("queries",queryTable);
			if (req.getParameter("asearch.runquery") != null)
			{
				if (logDebugEnabled) log.debug("Running query");
				StringBuffer queryString = new StringBuffer();
				StringBuffer whereClauseString = new StringBuffer();
				Iterator cai = columnAttributes.iterator();
				Attribute a = null;
				queryString.append("SELECT ");
				while (cai.hasNext())
				{
					a = (Attribute)cai.next();
					queryString.append(a.getName());
					if (cai.hasNext())
					{
						queryString.append(",");
					}
				}
				queryString.append(" WHERE ");
		
				Iterator i = queryTable.iterator();
				Iterator j = null;
				AttributeQuery aq = null;
				List subQueries = null;
				while (i.hasNext())
				{
					subQueries = (List)i.next();
					j = subQueries.iterator();
					whereClauseString.append("(");
					while (j.hasNext())
					{
						aq = (AttributeQuery)j.next();
						whereClauseString.append(aq);
						if (j.hasNext())
						{
							whereClauseString.append(" OR ");
						}
					}
					whereClauseString.append(")");
					if (i.hasNext())
					{
						whereClauseString.append(" AND ");
					}
				}
				queryString.append(whereClauseString);
				try {
					req.setAttribute( "tracklist" , SearchHelper.performSearch(queryString.toString()));
					session.setAttribute("currentQuery", whereClauseString.toString());
					req.setAttribute("queryString", whereClauseString.toString());
				} catch (ParserException pe) {
					req.setAttribute("error", "Badly formed query");
				}
				retVal = req.getRequestDispatcher("/browse.jsp");
			}
		}
		req.setAttribute( "browseColumnAttributes" , columnAttributes );
		req.setAttribute("sortAttribute",session.getAttribute("browseSortAttribute"));
		req.setAttribute("useGrouping",session.getAttribute("browseUseGrouping"));
		return retVal;
	}

	private RequestDispatcher handleSearch(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
	{
		RequestDispatcher retVal = req.getRequestDispatcher("/browse.jsp");
		String quickSearch = req.getParameter("quicksearch");
		String artistSearch = req.getParameter("search.artist");
		String albumSearch = req.getParameter("search.album");
		String titleSearch = req.getParameter("search.title");
		String numberSearch = req.getParameter("search.tracknumber");
		StringBuffer queryString = new StringBuffer();
		StringBuffer whereClauseString = new StringBuffer();
		HttpSession session = SearchHelper.prepareSearchSession(req);
		List columnAttributes = (List)session.getAttribute("browseColumnAttributes");

		if (quickSearch != null && !quickSearch.equals(""))
		{
			Iterator cai = columnAttributes.iterator();
			Attribute a = null;
			queryString.append("SELECT ");
			while (cai.hasNext())
			{
				a = (Attribute)cai.next();
				queryString.append(a.getName());
				if (cai.hasNext())
				{
					queryString.append(",");
				}
			}
			queryString.append(" WHERE ");
			whereClauseString.append("Artist = \"%");
			whereClauseString.append(quickSearch);
			whereClauseString.append("%\" OR Album = \"%");
			whereClauseString.append(quickSearch);
			whereClauseString.append("%\" OR Title = \"%");
			whereClauseString.append(quickSearch);
			whereClauseString.append("%\"");
			queryString.append(whereClauseString);
			try {
				req.setAttribute( "tracklist" , SearchHelper.performSearch(queryString.toString()));
			} catch (ParserException pe) {
				req.setAttribute("error","Badly formed query");
			}
		} else if (artistSearch != null && !artistSearch.equals("") || albumSearch != null && !albumSearch.equals("") || titleSearch != null && !titleSearch.equals("") || numberSearch != null && !numberSearch.equals("")) {
			Iterator cai = columnAttributes.iterator();
			Attribute a = null;
			queryString.append("SELECT ");
			while (cai.hasNext())
			{
				a = (Attribute)cai.next();
				queryString.append(a.getName());
				if (cai.hasNext())
				{
					queryString.append(",");
				}
			}
			queryString.append(" WHERE ");
			boolean needsRelop = false;
			if (artistSearch != null && !artistSearch.equals(""))
			{
				whereClauseString.append("Artist = \"%");
				whereClauseString.append(artistSearch);
				whereClauseString.append("%\"");
				needsRelop = true;
			}
			if (albumSearch != null && !albumSearch.equals(""))
			{
				if (needsRelop)
				{
					whereClauseString.append(" AND ");
				}
				whereClauseString.append("Album = \"%");
				whereClauseString.append(albumSearch);
				whereClauseString.append("%\"");
				needsRelop = true;
			}
			if (titleSearch != null && !titleSearch.equals(""))
			{
				if (needsRelop)
				{
					whereClauseString.append(" AND ");
				}
				whereClauseString.append("Title = \"%");
				whereClauseString.append(titleSearch);
				whereClauseString.append("%\"");
				needsRelop = true;
			}
			try {
				if (numberSearch != null && !numberSearch.equals(""))
				{
						Integer.parseInt(numberSearch);
						if (needsRelop)
						{
							whereClauseString.append(" AND ");
						}
						whereClauseString.append("TrackNumber = ");
						whereClauseString.append(numberSearch);
				}
				queryString.append(whereClauseString);
				try {
					req.setAttribute( "tracklist" , SearchHelper.performSearch(queryString.toString()));
				} catch (ParserException pe) {
					req.setAttribute("error","Badly formed query");
				}
			} catch (NumberFormatException e) {
				if (needsRelop)
				{
					// We still have a valid query
					queryString.append(whereClauseString);
					try {
						req.setAttribute( "tracklist" , SearchHelper.performSearch(queryString.toString()));
					} catch (ParserException pe) {
						req.setAttribute("error","Badly formed query");
					}
				}
			}
		} else {
			retVal = req.getRequestDispatcher("/search.jsp");
		}
		session.setAttribute("currentQuery", whereClauseString.toString());
		req.setAttribute( "browseColumnAttributes" , columnAttributes );
		req.setAttribute("sortAttribute",session.getAttribute("browseSortAttribute"));
		req.setAttribute("useGrouping",session.getAttribute("browseUseGrouping"));
		req.setAttribute("queryString",whereClauseString.toString());
		return retVal;
	}

	private RequestDispatcher handleFilter(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
	{
		req.setAttribute("filter", pipeline.get(Integer.parseInt(req.getParameter("filter.index"))));
		req.setAttribute("pipeline", pipeline);
		return req.getRequestDispatcher("/filter.jsp");
	}

	private RequestDispatcher handleBrowse(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException
	{
		// Order of business.  Define the alphabet.  Build a query and put in the results if required.
		try
		{
			StringBuffer query = new StringBuffer();
			String browseLetter = req.getParameter( "query.letter" );
			if (logInfoEnabled) log.info( "Searching for browseLetter..." + browseLetter );

			HttpSession session = SearchHelper.prepareSearchSession(req);

			List columnAttributes = (List)session.getAttribute("browseColumnAttributes");
			Iterator i = columnAttributes.iterator();
			Attribute searchAttribute = null;
			Attribute a = null;
			boolean validQuery = false;

			String currentQuery = (String)session.getAttribute("currentQuery");
			if (logInfoEnabled) log.info("Found saved search " + currentQuery);

			query.append("SELECT ");
			if (i.hasNext())
			{
				searchAttribute = (Attribute)i.next();
				query.append(searchAttribute.getName());
				while (i.hasNext())
				{
					a = (Attribute)i.next();
					if (searchAttribute.getType() != Attribute.TYPE_STRING)
					{
						searchAttribute = a;
					}
					query.append(",");
					query.append(a.getName());
				}
			}
			query.append(" WHERE ");
			if ( browseLetter != null )
			{
				query.append(searchAttribute.getName());
				if (browseLetter.startsWith("Other"))
				{
					// special case for attributes that do not begin with an alpha character
					query.append(" REGEXP \"^([^a-zA-Z]|$)\"");
				} else {
					query.append(" = \"").append(Escaper.stringEscape(browseLetter)).append("%\"");
				}
				// forget the saved query
				if (logInfoEnabled) log.info("Forgetting saved query");
				session.removeAttribute("currentQuery");
				validQuery = true;

			} else if (currentQuery != null) {
				if (logInfoEnabled) log.info("Running saved query");
				query.append(currentQuery);
				validQuery = true;
				req.setAttribute("queryString",currentQuery);
			}
			query.append(" ORDER BY ").append(((Attribute)session.getAttribute("browseSortAttribute")).getName()).append(" ").append(session.getAttribute("browseSortOrder"));
			if (validQuery)
			{
				if (logInfoEnabled) log.info( "Searching with JukeXQL "+query.toString() );
				
				Query q = ((JukeXParser)JukeXParser.createParser( query.toString() )).parse();
				AttributeValueResultSet trackList = q.getAttributeValues();
				req.setAttribute( "tracklist" , trackList );
			}
			req.setAttribute("browseColumnAttributes",columnAttributes);
			req.setAttribute("browseLetter",browseLetter);
			req.setAttribute("sortAttribute",session.getAttribute("browseSortAttribute"));
			req.setAttribute("useGrouping",session.getAttribute("browseUseGrouping"));
		}
		catch ( Exception e )
		{
			log.warn( "Exception encountered whilst creating browse page" , e );
		}
		
		return req.getRequestDispatcher("/browse.jsp");
	}

	/**
	 * Create a personal playlist and associate it with the session. This may
	 * (but should not) involve creating the RoundRobinPipelineElement
	 *
	 * @param session The HttpSession
	 */
	private void setupPersonalPlaylist( HttpSession session )
	{
		log.debug("Setting up session...");
		Playlist myPlaylist = new InMemoryPlaylist();
		session.setAttribute( "personalPlaylist" , myPlaylist );
		ListIterator i = this.pipeline.listIterator();
		
		// See if we can find the Round Robin pipeline element, and if not
		// make a new one and add this playlist into it.
		
		RoundRobinPipelineElement rrpe = null;
		
		while ( i.hasNext() )
		{
			Object thing = i.next();
			if ( thing instanceof RoundRobinPipelineElement )
			{
				rrpe = (RoundRobinPipelineElement) thing;
				log.debug("Found RoundRobinPipelineElement...");
			}
		}
		
		if (rrpe != null)
		{
			rrpe.addTrackSource( myPlaylist );
		} else {
			log.warn("No RoundRobinPipelineElement found");
		}
	}
}
