gt: 		    		Customer customer = user.getCustomer();
context: package org.skyve.impl.web.service.smartclient;
  
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.lang.reflect.InvocationTargetException;
  import java.security.Principal;
  import java.security.SecureRandom;
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.SortedMap;
  import java.util.TreeMap;
  
  import javax.servlet.ServletException;
  import javax.servlet.http.HttpServlet;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import javax.servlet.http.HttpSession;
  
  import org.locationtech.jts.geom.Geometry;
  import org.skyve.CORE;
  import org.skyve.cache.StateUtil;
  import org.skyve.content.MimeType;
  import org.skyve.domain.Bean;
  import org.skyve.domain.ChildBean;
  import org.skyve.domain.HierarchicalBean;
  import org.skyve.domain.MapBean;
  import org.skyve.domain.PersistentBean;
  import org.skyve.domain.messages.Message;
  import org.skyve.domain.messages.SessionEndedException;
  import org.skyve.domain.messages.ValidationException;
  import org.skyve.domain.types.converters.Converter;
  import org.skyve.impl.bind.BindUtil;
  import org.skyve.impl.domain.messages.SecurityException;
  import org.skyve.impl.metadata.model.document.field.ConvertableField;
  import org.skyve.impl.metadata.model.document.field.Enumeration;
  import org.skyve.impl.metadata.repository.AbstractRepository;
  import org.skyve.impl.persistence.AbstractPersistence;
  import org.skyve.impl.util.TagUtil;
  import org.skyve.impl.util.UtilImpl;
  import org.skyve.impl.web.AbstractWebContext;
  import org.skyve.impl.web.SortParameterImpl;
  import org.skyve.web.SortParameter;
  import org.skyve.impl.web.WebUtil;
  import org.skyve.metadata.MetaDataException;
  import org.skyve.metadata.SortDirection;
  import org.skyve.metadata.customer.Customer;
  import org.skyve.metadata.model.Attribute;
  import org.skyve.metadata.model.document.Association;
  import org.skyve.metadata.model.document.Document;
  import org.skyve.metadata.model.document.DomainType;
  import org.skyve.metadata.module.Module;
  import org.skyve.metadata.module.query.MetaDataQueryDefinition;
  import org.skyve.metadata.module.query.MetaDataQueryProjectedColumn;
  import org.skyve.metadata.module.query.MetaDataQueryColumn;
  import org.skyve.metadata.user.User;
  import org.skyve.metadata.view.TextOutput.Sanitisation;
  import org.skyve.metadata.view.model.list.DocumentQueryListModel;
  import org.skyve.metadata.view.model.list.Filter;
  import org.skyve.metadata.view.model.list.ListModel;
  import org.skyve.metadata.view.model.list.Page;
  import org.skyve.persistence.DocumentQuery.AggregateFunction;
  import org.skyve.util.Binder;
  import org.skyve.util.Binder.TargetMetaData;
  import org.skyve.util.JSON;
  import org.skyve.util.OWASP;
  import org.skyve.util.Util;
  
  /**
   * Service for list views.
   */
  public class SmartClientListServlet extends HttpServlet {
  	private static final long serialVersionUID = 1L;
  	
  	static final String ISC_META_DATA_PREFIX = "isc_metaDataPrefix";
  	static final String ISC_DATA_FORMAT = "isc_dataFormat";
  	static final String OLD_VALUES = "_oldValues";
  	static final String ISC_INTERNAL_GRID_COLUMN_NAME_PREFIX = "$";
  	static final String ISC_JSON_PREFIX = "<SCRIPT>//'\"]]>>isc_JSONResponseStart>>";
  	static final String ISC_JSON_SUFFIX = "//isc_JSONResponseEnd";
  
  	@Override
  	protected void doGet(HttpServletRequest request, HttpServletResponse response) 
  	throws ServletException, IOException {
  		UtilImpl.LOGGER.info("SmartClientList - get....");
  		processRequest(request, response);
  	}
  	
  	@Override
      protected void doPost(HttpServletRequest request, HttpServletResponse response) 
  	throws ServletException, IOException {
  		UtilImpl.LOGGER.info("SmartClientList - post....");
  		processRequest(request, response);
  	}
  	
  	// NB - Never throw ServletException as this will halt the SmartClient Relogin flow.
      private static void processRequest(HttpServletRequest request, HttpServletResponse response) 
  	throws IOException {
          response.setContentType(MimeType.json.toString());
          response.setCharacterEncoding(Util.UTF8);
  		response.addHeader("Cache-control", "private,no-cache,no-store"); // never
  		response.addDateHeader("Expires", 0); // never
  
      	// Send CSRF Token as a response header (must be done before getting the writer)
  		String currentCsrfTokenString = UtilImpl.processStringValue(request.getParameter(AbstractWebContext.CSRF_TOKEN_NAME));
  		Integer currentCsrfToken = (currentCsrfTokenString == null) ? null : Integer.valueOf(currentCsrfTokenString);
  		Integer newCsrfToken = currentCsrfToken;
  		String operationType = OWASP.sanitise(Sanitisation.text, Util.processStringValue(request.getParameter("_operationType")));
  		// If this is a mutating request, we'll definitely need a new CSRF Token
  		if (Operation.fetch.toString().equals(operationType)) {
  			if (newCsrfToken == null) {
  				newCsrfToken = Integer.valueOf(new SecureRandom().nextInt());
  			}
  		}
  		else {
  			newCsrfToken = Integer.valueOf(new SecureRandom().nextInt());
  		}
      	response.setIntHeader("X-CSRF-TOKEN", newCsrfToken.intValue());
  
  		try (PrintWriter pw = response.getWriter()) {
  			String dataSource = OWASP.sanitise(Sanitisation.text, Util.processStringValue(request.getParameter("_dataSource")));
  	        if (operationType == null) {
  	        	pw.append("{}");
  	        	return;
  	        }
  	        Operation operation = Operation.valueOf(operationType);
  	        AbstractPersistence persistence = null;
  	        
  	        try {
  				try {
  					// use the view's conversation if it was sent down from the client
  					String webId = OWASP.sanitise(Sanitisation.text, Util.processStringValue(request.getParameter(AbstractWebContext.CONTEXT_NAME)));
  					AbstractWebContext webContext = StateUtil.getCachedConversation(webId, request, response);
  					if (webContext != null) {
  						if (request.getParameter(AbstractWebContext.CONTINUE_CONVERSATION) != null) {
  				        	UtilImpl.LOGGER.info("USE VIEW CONVERSATION!!!!");
  				            persistence = webContext.getConversation();
  				            persistence.setForThread();
  						}
  			        }
  			        // if no conversation to use, start a new one
  			        if (persistence == null) {
  			            persistence = AbstractPersistence.get();
  			            persistence.evictAllCached();
  			        }
  
  			        persistence.begin();
  			    	Principal userPrincipal = request.getUserPrincipal();
  			    	User user = WebUtil.processUserPrincipalForRequest(request,
  			    														(userPrincipal == null) ? null : userPrincipal.getName(),
  	    																true);
  					if (user == null) {
  						throw new SessionEndedException(request.getLocale());
  					}
  			    	persistence.setUser(user);
  	
  					Bean bean = WebUtil.getConversationBeanFromRequest(webContext, request);
  			    	Customer customer = user.getCustomer();
  			        Module module = null;
  			        Document drivingDocument = null;
  			        ListModel<Bean> model = null;
  					MetaDataQueryDefinition query = null;
  					
  			        if (dataSource != null) {
  			        	// '_' split could be 2, 3 or 4 tokens
  			        	// 2 - module_query, module_document (default query)
  			        	// >3 - module_query_attribute (picklist), module_document_attribute (default query picklist)
  			        	// '__' - module_document__model
  			        	String[] tokens = dataSource.split("_");
  						module = customer.getModule(tokens[0]);
  						
  						// model type of request
  						if (dataSource.contains("__")) {
  							drivingDocument = module.getDocument(customer, tokens[1]);
  							model = CORE.getRepository().getListModel(customer, drivingDocument, tokens[3], true);
  							if (model == null) {
  								throw new ServletException("DataSource does not reference a valid model " + tokens[3]);
  							}
  							model.setBean(bean);
  							drivingDocument = model.getDrivingDocument();
  						}
  						// query type of request
  						else {
  							String documentOrQueryName = tokens[1];
  							query = module.getMetaDataQuery(documentOrQueryName);
  							// not a query, must be a document
  							if (query == null) {
  								query = module.getDocumentDefaultQuery(customer, documentOrQueryName);
  							}
  							if (query == null) {
  								throw new ServletException("DataSource does not reference a valid query " + documentOrQueryName);
  							}
  					        DocumentQueryListModel<Bean> queryModel = new DocumentQueryListModel<>();
  					        queryModel.setQuery(query);
  					        model = queryModel;
  					        drivingDocument = module.getDocument(customer, query.getDocumentName());
  						}
  			        }
  			        else {
  			        	throw new ServletException("No datasource name in the request.");
  			        }
  			        
  			        SortedMap<String, Object> parameters = new TreeMap<>();
  					java.util.Enumeration<String> names = request.getParameterNames();
  					while (names.hasMoreElements()) {
  						String name = names.nextElement();
  						if (ISC_META_DATA_PREFIX.equals(name) || 
  								ISC_DATA_FORMAT.equals(name) ||
  								// $ is sent down when list grid is in expander mode with hidden columns
  								// and a record flag is cleared
  								name.startsWith(ISC_INTERNAL_GRID_COLUMN_NAME_PREFIX) ||
  								// don't need to add the criteria parameter to the internal parameters map
  								// as these are processed specifically by fetch...
  								name.equals("criteria")) {
  							continue;
  						}
  						String value = request.getParameter(name);
  						if ((! name.isEmpty()) && name.charAt(0) != '_') {
  							// no '.' allowed in smart client field names
  							name = BindUtil.unsanitiseBinding(name);
  							
  							// "null" can be sent by Smart Client
  							if (value != null) {
  								if ((value.length() == 0) || "null".equals(value)) {
  									value = null;
  								}
  							}
  							parameters.put(name, OWASP.unescapeHtmlChars(value));
  						}
  					}
  					for (String name : parameters.keySet()) {
  						UtilImpl.LOGGER.info(name + " = " + parameters.get(name));
  					}
  
  					String tagId = OWASP.sanitise(Sanitisation.text, Util.processStringValue(request.getParameter("_tagId")));
  					// "null" can be sent by Smart Client
  					if (tagId != null) {
  						if ((tagId.length() == 0) || "null".equals(tagId)) {
  							tagId = null;
  						}
  					}
  
  					HttpSession session = request.getSession();
  
  					switch (operation) {
  					case fetch:
  						if (! user.canReadDocument(drivingDocument)) {
  							throw new SecurityException("read this data", user.getName());
  						}
  						String _startRow = Util.processStringValue(request.getParameter("_startRow"));
  						int startRow = (_startRow == null) ? 0 : Integer.parseInt(_startRow);
  						String _endRow = Util.processStringValue(request.getParameter("_endRow"));
  						int endRow = (_endRow == null) ? Integer.MAX_VALUE : Integer.parseInt(_endRow);
  						String textMatchStyle = OWASP.sanitise(Sanitisation.text, Util.processStringValue(request.getParameter("_textMatchStyle")));
  						SmartClientFilterOperator operator = null;
  						if (textMatchStyle != null) {
  							operator = SmartClientFilterOperator.valueOf(textMatchStyle);
  						}
  	
  						String[] sortBys = request.getParameterValues("_sortBy");
  						SortParameter[] sortParameters = null;
  						if (sortBys != null) {
  							sortParameters = new SortParameter[sortBys.length];
  							for (int i = 0; i < sortBys.length; i++) {
  								String sortBy = OWASP.sanitise(Sanitisation.text, Util.processStringValue(sortBys[i]));
  								
  								SortParameter sortParameter = new SortParameterImpl();
  								if (sortBy.startsWith("-")) {
  									sortParameter.setDirection(SortDirection.descending);
  									sortBy = sortBy.substring(1);
  								}
  								else {
  									sortParameter.setDirection(SortDirection.ascending);
  								}
  								sortParameter.setBy(BindUtil.unsanitiseBinding(sortBy));
  								
  								sortParameters[i] = sortParameter;
  							}
  						}
  	
  						String summary = OWASP.sanitise(Sanitisation.text, Util.processStringValue(request.getParameter("_summary")));
  						if ("".equals(summary)) {
  							summary = null;
  						}
  						fetch(module,
  								drivingDocument,
  								startRow, 
  								endRow, 
  								operator, 
  								request.getParameterValues("criteria"),
  								sortParameters,
  								(summary == null) ? null : AggregateFunction.valueOf(summary),
  								// include a summary extra row (for list grids)
  								request.getParameterMap().containsKey("_summary"),
  								tagId,
  								parameters, 
  								persistence, 
  								pw,
  								model);
  						break;
  					case add:
  						checkCsrfToken(session, request, response, currentCsrfToken);
  						
  						if (! user.canCreateDocument(drivingDocument)) {
  							throw new SecurityException("create this data", user.getName());
  						}
  						break;
  					case update:
  						checkCsrfToken(session, request, response, currentCsrfToken);
  
  						String bizTagged = (String) parameters.get(PersistentBean.TAGGED_NAME);
  						if ("TAG".equals(bizTagged)) {
  							tag(customer, module, model, tagId, parameters, pw);
  						}
  						else if ("UNTAG".equals(bizTagged)) {
  							untag(customer, module, model, tagId, parameters, pw);
  						}
  						else {
  							if (! user.canUpdateDocument(drivingDocument)) {
  								throw new SecurityException("update this data", user.getName());
  							}
  		
  							boolean rowIsTagged = false;
  							String oldValuesJSON = request.getParameter(OLD_VALUES);
  							if (oldValuesJSON != null) {
  								rowIsTagged = oldValuesJSON.contains(PersistentBean.TAGGED_NAME + "\":true");
  							}
  							update(module, 
  									model, 
  									rowIsTagged,
  									parameters, 
  									persistence, 
  									pw);
  						}
  						break;
  					case remove:
  						checkCsrfToken(session, request, response, currentCsrfToken);
  						
  						if (! user.canDeleteDocument(drivingDocument)) {
  							throw new SecurityException("delete this data", user.getName());
  						}
  	
  						remove(model, parameters, pw);
  						break;
  					default:
  					}
  					
  					// Replace CSRF token
  					StateUtil.replaceToken(session, currentCsrfToken, newCsrfToken);
  					
  					// serialize and cache conversation, if applicable
  			    	if (webContext != null) {
  			    		StateUtil.cacheConversation(webContext);
  			    	}
  				}
  				catch (InvocationTargetException e) {
  					throw e.getTargetException();
  				}
  			}
  			catch (Throwable t) {
  		    	t.printStackTrace();
  		    	if (persistence != null) {
  		    		persistence.rollback();
  		    	}
  	
  		    	SmartClientEditServlet.produceErrorResponse(t, operation, false, pw);
  			}
  		    finally {
  		    	if (persistence != null) {
  		    		persistence.commit(true);
  		    	}
  		    }
  		}
  	}
  	
      private static void fetch(Module module,
      							Document queryDocument,
  								int startRow,
  								int endRow,
  								SmartClientFilterOperator operator,
  								String[] criteria,
  								SortParameter[] sortParameters,
  								AggregateFunction summaryType,
  								boolean includeExtraSummaryRow,
  								String tagId,
  								SortedMap<String, Object> parameters,
  								AbstractPersistence persistence,
  								PrintWriter pw,
  								ListModel<Bean> model)
  	throws Exception {
  		StringBuilder message = new StringBuilder(256);
  
  		User user = persistence.getUser();
  		Customer customer = user.getCustomer();
  
  		model.setStartRow(startRow);
  		model.setEndRow(endRow);
  		model.setSortParameters(sortParameters);
  		model.setSummary(summaryType);
  		model.setSelectedTagId(tagId);
  		
  		// Add filter criteria to query
  		addFilterCriteriaToQuery(module, queryDocument, user, operator, criteria, parameters, tagId, model);
  
  		Page page = model.fetch();
  		List<Bean> beans = page.getRows();
  		
  		// Sanitise rows
  		// Note that HTML escaping is taken care by SC client-side for data grid columns
  		OWASP.sanitiseAndEscapeListModelRows(beans, model.getColumns(), false);
  		
  		Bean summaryBean = page.getSummary();
  		if (includeExtraSummaryRow) {
  			BindUtil.set(summaryBean, PersistentBean.FLAG_COMMENT_NAME, summaryType);
  			beans.add(summaryBean);
  		}
  		long totalRows = page.getTotalRows();
  		if (UtilImpl.COMMAND_TRACE) UtilImpl.LOGGER.info(String.format("totalRows = %d, row size = %d", 
  																		Long.valueOf(page.getTotalRows()), 
  																		Integer.valueOf(page.getRows().size())));
  		Set<String> projections = model.getProjections();
  
  		message.append("{\"response\":{");
  		message.append("\"status\":0,");
  		// If SmartClient requests a start row > what we have in the set
  		// (maybe a criteria has constrained the set such that a page we were at doesn't exist any more)
  		// then just send back a start row of 0.
  		message.append("\"startRow\":").append((startRow > totalRows) ? 0 : startRow);
  		message.append(",\"endRow\":");
  		message.append(Math.min(totalRows, endRow));
  		message.append(",\"totalRows\":");
  		message.append(totalRows);
  		message.append(",\"data\":");
  		message.append(JSON.marshall(customer, beans, projections));
  		message.append("}}");
  		pw.append(message);
      }
      
  	private static void addFilterCriteriaToQuery(Module module,
  													Document document,
  		    										User user,
  		    										SmartClientFilterOperator filterOperator,
  		    										String[] criteria,
  		    										SortedMap<String, Object> parameters,
  		    										String tagId,
  													ListModel<Bean> model) 
      throws Exception {
      	SortedMap<String, Object> mutableParameters = new TreeMap<>(parameters);
      	CompoundFilterOperator compoundFilterOperator = CompoundFilterOperator.and;
      	String operatorParameter = (String) mutableParameters.get("operator");
      	if (operatorParameter != null) { // advanced criteria
      		try {
      			compoundFilterOperator = CompoundFilterOperator.valueOf(operatorParameter);
      		}
      		catch (@SuppressWarnings("unused") Exception e) {
      			// NB Smart Client sometimes sends extraneous half-arsed requests 
      			// through the advanced filter builder with a compound filter parameter of 'equals'.
      			// Ignore these and leave compound filter criteria set to 'and'.
      		}
      		// Produce advanced criteria parameter
      		List<Map<String, Object>> advancedCriteria = null;
      		if (criteria == null) {
      			advancedCriteria = new ArrayList<>(0);
      		}
      		else {
      			advancedCriteria = new ArrayList<>(criteria.length);
          		for (String jsonCriteria : criteria) {
      				// Get each criterium name, operator and operands
  					@SuppressWarnings("unchecked")
  					Map<String, Object> criterium = (Map<String, Object>) JSON.unmarshall(user, jsonCriteria);
      				advancedCriteria.add(criterium);
          		}
      		}
      		addAdvancedFilterCriteriaToQuery(module,
      											document,
      											user,
      											compoundFilterOperator,
      											advancedCriteria,
      											tagId,
  												model);
      		
      		mutableParameters.remove("operator");
      		mutableParameters.remove("criteria");
      	}
  
      	// simple criteria or extra criteria from grid filter parameters
      	addSimpleFilterCriteriaToQuery(module,
  										document,
  										user.getCustomer(),
  										filterOperator,
  										mutableParameters,
  										tagId,
  										model);
      }
  
  	static void checkCsrfToken(HttpSession session,
  								HttpServletRequest request,
  								HttpServletResponse response,
  								Integer currentCsrfToken)
  	throws ServletException {
  /* TODO Commented out until I can work out the async problems in SC
  		if (! StateUtil.checkToken(session, currentCsrfToken)) {
  			WebUtil.logout(request, response);
  			throw new java.lang.SecurityException("CSRF attack detected");
  		}
  */
  	}
  	
  	/**
       * Add simple criteria to the query.
       * 
       * @param module
       * @param document
       * @param customer
       * @param query	Add the filter criteria to this query.
       * @param filterOperator	The default operator to use for all filter critiera.
       * @param criteria	A Map of name value pairs
       * @throws Exception
       */
      public static void addSimpleFilterCriteriaToQuery(Module module,
  														Document document,
  														Customer customer,
  			    										SmartClientFilterOperator filterOperator,
  			    										Map<String, Object> criteria,
  			    										String tagId,
  		    											ListModel<Bean> model) 
      throws Exception {
      	Filter filter = model.getFilter();
      	
      	// This doesn't need to set up a sub-filter as its just ANDing criteria
      	// and the other criteria already added should have taken care of bracketing
      	// for correct operator precedence.
      	for (String binding : criteria.keySet()) {
  			Object value = criteria.get(binding);
  			if ("".equals(value)) {
  				value = null;
  			}
  			
  			binding = BindUtil.unsanitiseBinding(binding);
  			boolean parameter = (binding.charAt(0) == ':');
  			if (parameter) {
  				binding = binding.substring(1); // lose the colon
  			}
  			
  			// Determine the type and converter of the filtered attribute
  			Converter<?> converter = null;
      		Class<?> type = String.class;
      		
      		// Must be a valid property if we are adding a filter criteria
      		// Not necessarily a valid property binding if processing a query parameter
      		TargetMetaData target = null;
      		// set to true if equals should be used instead of substring
      		boolean equalsOperatorRequired = false;
      		try {
  				target = BindUtil.getMetaDataForBinding(customer, 
  															module, 
  															document, 
  															binding);
      		}
      		catch (MetaDataException e ) {
      			if (! parameter) {
      				throw e;
      			}
      		}
  			if (target != null) {
  				Attribute attribute = target.getAttribute();
  				if (attribute != null) {
  					if (attribute instanceof Enumeration) {
  						type = AbstractRepository.get().getEnum((Enumeration) attribute);
  						equalsOperatorRequired = true;
  					}
  					else {
  						type = attribute.getAttributeType().getImplementingType();
  					}
  					if (DomainType.constant.equals(attribute.getDomainType())) {
  						equalsOperatorRequired = true;
  					}
  					if (attribute instanceof ConvertableField) {
  						ConvertableField field = (ConvertableField) attribute;
  						converter = field.getConverterForCustomer(customer);
  					}
  					else if (attribute instanceof Association) {
  						equalsOperatorRequired = true;
  						if (! parameter) {
  							type = String.class;
  							binding = String.format("%s.%s", binding, Bean.DOCUMENT_ID);
  						}
  					}
  				}
  			}
  
  			if (value != null) {
  				value = fromString(binding, 
  									"value", 
  									value.toString(),
  									customer, 
  									converter, 
  									type);
  			}
  
  			if (parameter) {
  				model.putParameter(binding, value);
  			}
  			else {
  				equalsOperatorRequired = equalsOperatorRequired || 
  											(value instanceof Date) ||
  											(value instanceof Number) ||
  											(value instanceof Boolean);
  				addCriteriumToFilter(binding, 
  										equalsOperatorRequired ? SmartClientFilterOperator.equals : filterOperator, 
  										value, 
  										null, 
  										null,
  										tagId,
  										filter);
  			}
  		}
  	}
  
      /**
       * Add advanced filter criteria to a query.
       * @param module
       * @param document
       * @param customer
       * @param user
       * @param query	The query to add the filter criteria to.
       * @param compoundFilterOperator	The compound filter operator to use between criteria
       * @param criteria	List of advanced critiera.
       * @throws Exception
       */
  	public static void addAdvancedFilterCriteriaToQuery(Module module,
  															Document document,
  															User user,
  															CompoundFilterOperator compoundFilterOperator,
  															List<Map<String, Object>> criteria,
  															String tagId,
  															ListModel<?> model)
  	throws Exception {
  		// We have to unconditionally add a new filter here as the metadata query
  		// might have a filter stanza in it which we can't detect and we need to ensure
  		// any filtering done here is AND'd with any existing criteria (hard-coded in a filter stanza or not)
  		Filter filter = model.getFilter();
  		Filter newFilter = model.newFilter();
  		addAdvancedFilterCriteriaToQueryInternal(module,
  													document,
  													user,
  													compoundFilterOperator,
  													criteria,
  													tagId,
  													model,
  													newFilter);
  		if (! newFilter.isEmpty()) {
  			filter.addAnd(newFilter);
  		}
  	}
      
  	private static final String HIERARCHICAL_PARENT_ID_SUFFIX = "." + HierarchicalBean.PARENT_ID;
  
  	private static void addAdvancedFilterCriteriaToQueryInternal(Module module,
  																	Document document,
  																	User user,
  						    										CompoundFilterOperator compoundFilterOperator,
  						    										List<Map<String, Object>> criteria,
  						    										String tagId,
  																	ListModel<?> model,
  																	Filter filter)
      throws Exception {
  		if (criteria != null) {
  			boolean firstCriteriaIteration = true; // the first filter criteria encountered - not a bound parameter
  			for (Map<String, Object> criterium : criteria) {
  				if (UtilImpl.COMMAND_TRACE) UtilImpl.LOGGER.info("criterium = " + JSON.marshall(criterium));
  				String binding = ((String) criterium.get("fieldName"));
  				binding = BindUtil.unsanitiseBinding(binding);
  				SmartClientFilterOperator filterOperator = SmartClientFilterOperator.valueOf((String) criterium.get("operator"));
  
  				if (binding == null) { // advanced criteria
  					Filter subFilter = model.newFilter();
  					CompoundFilterOperator subCompoundFilterOperator = CompoundFilterOperator.valueOf(filterOperator.toString());
  					@SuppressWarnings("unchecked")
  					List<Map<String, Object>> subCritiera = (List<Map<String, Object>>) criterium.get("criteria");
  					addAdvancedFilterCriteriaToQueryInternal(module,
  																document,
  																user,
  																subCompoundFilterOperator,
  																subCritiera,
  																tagId,
  																model,
  																subFilter);
  					if (! subFilter.isEmpty()) {
  						if (CompoundFilterOperator.or.equals(compoundFilterOperator)) {
  							filter.addOr(subFilter);
  						}
  						else { // not is taken into account below
  							filter.addAnd(subFilter);
  						}
  					}
  				}
  				else { // simple criteria
  					Object value = criterium.get("value");
  		    		String valueString = null;
  		    		if (value != null) {
  		    			valueString = value.toString();
  			    		if ("".equals(valueString)) {
  			    			valueString = null;
  			    		}
  		    		}
  	
  		    		boolean parameter = (binding.charAt(0) == ':');
  		    		if (parameter) {
  		    			binding = binding.substring(1);
  		    		}
  		    		
  		    		// Determine the type and converter of the filtered attribute
  