package org.jboss.seam.framework; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.faces.model.DataModel; import org.jboss.seam.annotations.Create; import org.jboss.seam.annotations.Transactional; import org.jboss.seam.core.Expressions; import org.jboss.seam.core.Expressions.ValueExpression; import org.jboss.seam.faces.DataModels; import org.jboss.seam.persistence.QueryParser; /** * Base class for components which manage a query * result set. This class may be reused by either * configuration or extension, and may be bound * directly to a view, or accessed by some * intermediate Seam component. * * @author Gavin King * */ public abstract class Query<T, E> extends PersistenceController<T> //TODO: extend MutableController! { private static final Pattern SUBJECT_PATTERN = Pattern.compile("^select\\s+(\\w+(?:\\s*\\.\\s*\\w+)*?)(?:\\s*,\\s*(\\w+(?:\\s*\\.\\s*\\w+)*?))*?\\s+from", Pattern.CASE_INSENSITIVE); private static final Pattern FROM_PATTERN = Pattern.compile("(^|\\s)(from)\\s", Pattern.CASE_INSENSITIVE); private static final Pattern WHERE_PATTERN = Pattern.compile("\\s(where)\\s", Pattern.CASE_INSENSITIVE); private static final Pattern ORDER_PATTERN = Pattern.compile("\\s(order)(\\s)+by\\s", Pattern.CASE_INSENSITIVE); private static final Pattern GROUP_PATTERN = Pattern.compile("\\s(group)(\\s)+by\\s", Pattern.CASE_INSENSITIVE); private static final Pattern ORDER_COLUMN_PATTERN = Pattern.compile("^\\w+(\\.\\w+)*$"); private static final String DIR_ASC = "asc"; private static final String DIR_DESC = "desc"; private static final String LOGIC_OPERATOR_AND = "and"; private static final String LOGIC_OPERATOR_OR = "or"; private String ejbql; private Integer firstResult; private Integer maxResults; private List<ValueExpression> restrictions = new ArrayList<ValueExpression>(0); private String order; private String orderColumn; private String orderDirection; private String restrictionLogicOperator; private String groupBy; private boolean useWildcardAsCountQuerySubject = true; private DataModel dataModel; private String parsedEjbql; private List<ValueExpression> queryParameters; private List<String> parsedRestrictions; private List<ValueExpression> restrictionParameters; private List<Object> queryParameterValues; private List<Object> restrictionParameterValues; public abstract List<E> getResultList(); public abstract E getSingleResult(); public abstract Long getResultCount(); @Create public void validate() { if ( getEjbql()==null ) { throw new IllegalStateException("ejbql is null"); } } /** * Wrap the result set in a JSF {@link DataModel} * * Delegates to {@link DataModels#getDataModel(Query)} * */ @Transactional public DataModel getDataModel() { if (dataModel==null) { dataModel = DataModels.instance().getDataModel(this); } return dataModel; } /** * Get the selected row of the JSF {@link DataModel} * */ public E getDataModelSelection() { return (E) getDataModel().getRowData(); } /** * Get the index of the selected row of the JSF {@link DataModel} * */ public int getDataModelSelectionIndex() { return getDataModel().getRowIndex(); } public void refresh() { clearDataModel(); } /** * Move the result set cursor to the beginning of the last page * */ @Transactional public void last() { setFirstResult( getLastFirstResult().intValue() ); } /** * Move the result set cursor to the beginning of the next page * */ public void next() { setFirstResult( getNextFirstResult() ); } /** * Move the result set cursor to the beginning of the previous page * */ public void previous() { setFirstResult( getPreviousFirstResult() ); } /** * Move the result set cursor to the beginning of the first page * */ public void first() { setFirstResult(0); } protected void clearDataModel() { dataModel = null; } /** * Get the index of the first result of the last page * */ @Transactional public Long getLastFirstResult() { Integer pc = getPageCount(); return pc==null ? null : ( pc.longValue()-1 ) * getMaxResults(); } /** * Get the index of the first result of the next page * */ public int getNextFirstResult() { Integer fr = getFirstResult(); return ( fr==null ? 0 : fr ) + getMaxResults(); } /** * Get the index of the first result of the previous page * */ public int getPreviousFirstResult() { Integer fr = getFirstResult(); Integer mr = getMaxResults(); return mr >= ( fr==null ? 0 : fr ) ? 0 : fr - mr; } /** * Get the total number of pages * */ @Transactional public Integer getPageCount() { if ( getMaxResults()==null ) { return null; } else { int rc = getResultCount().intValue(); int mr = getMaxResults().intValue(); int pages = rc / mr; return rc % mr == 0 ? pages : pages+1; } } protected void parseEjbql() { if (parsedEjbql==null || parsedRestrictions==null) { QueryParser qp = new QueryParser( getEjbql() ); queryParameters = qp.getParameterValueBindings(); parsedEjbql = qp.getEjbql(); List<ValueExpression> restrictionFragments = getRestrictions(); parsedRestrictions = new ArrayList<String>( restrictionFragments.size() ); restrictionParameters = new ArrayList<ValueExpression>( restrictionFragments.size() ); for ( ValueExpression restriction: restrictionFragments ) { QueryParser rqp = new QueryParser( restriction.getExpressionString(), queryParameters.size() + restrictionParameters.size() ); if ( rqp.getParameterValueBindings().size()!=1 ) { throw new IllegalArgumentException("there should be exactly one value binding in a restriction: " + restriction); } parsedRestrictions.add( rqp.getEjbql() ); restrictionParameters.addAll( rqp.getParameterValueBindings() ); } } } protected String getRenderedEjbql() { StringBuilder builder = new StringBuilder().append(parsedEjbql); for (int i=0; i<getRestrictions().size(); i++) { Object parameterValue = restrictionParameters.get(i).getValue(); if ( isRestrictionParameterSet(parameterValue) ) { if ( WHERE_PATTERN.matcher(builder).find() ) { builder.append(" ").append(getRestrictionLogicOperator()).append(" "); } else { builder.append(" where "); } builder.append( parsedRestrictions.get(i) ); } } if (getGroupBy()!=null) { builder.append(" group by ").append(getGroupBy()); } if (getOrder()!=null) { builder.append(" order by ").append( getOrder() ); } return builder.toString(); } protected boolean isRestrictionParameterSet(Object parameterValue) { return parameterValue != null && !"".equals(parameterValue) && (parameterValue instanceof Collection ? !((Collection) parameterValue).isEmpty() : true); } /** * Return the ejbql to used in a count query (for calculating number of * results) * @return String The ejbql query */ protected String getCountEjbql() { String ejbql = getRenderedEjbql(); Matcher fromMatcher = FROM_PATTERN.matcher(ejbql); if ( !fromMatcher.find() ) { throw new IllegalArgumentException("no from clause found in query"); } int fromLoc = fromMatcher.start(2); // TODO can we just create a protected method that builds the query w/o the order by and group by clauses? Matcher orderMatcher = ORDER_PATTERN.matcher(ejbql); int orderLoc = orderMatcher.find() ? orderMatcher.start(1) : ejbql.length(); Matcher groupMatcher = GROUP_PATTERN.matcher(ejbql); int groupLoc = groupMatcher.find() ? groupMatcher.start(1) : orderLoc; Matcher whereMatcher = WHERE_PATTERN.matcher(ejbql); int whereLoc = whereMatcher.find() ? whereMatcher.start(1) : groupLoc; String subject; if (getGroupBy() != null) { subject = "distinct " + getGroupBy(); } else if (useWildcardAsCountQuerySubject) { subject = "*"; } // to be JPA-compliant, we need to make this query like "select count(u) from User u" // however, Hibernate produces queries some databases cannot run when the primary key is composite else { Matcher subjectMatcher = SUBJECT_PATTERN.matcher(ejbql); if ( subjectMatcher.find() ) { subject = subjectMatcher.group(1); } else { throw new IllegalStateException("invalid select clause for query"); } } return new StringBuilder(ejbql.length() + 15).append("select count(").append(subject).append(") "). append(ejbql.substring(fromLoc, whereLoc).replace("join fetch", "join")). append(ejbql.substring(whereLoc, groupLoc)).toString().trim(); } public String getEjbql() { return ejbql; } /** * Set the ejbql to use. Calling this causes the ejbql to be reparsed and * the query to be refreshed */ public void setEjbql(String ejbql) { this.ejbql = ejbql; parsedEjbql = null; refresh(); } /** * Returns the index of the first result of the current page */ public Integer getFirstResult() { return firstResult; } /** * Returns true if the previous page exists */ public boolean isPreviousExists() { return getFirstResult()!=null && getFirstResult()!=0; } /** * Returns true if next page exists */ public abstract boolean isNextExists(); /** * Returns true if the query is paginated, revealing * whether navigation controls are needed. */ public boolean isPaginated() { return isNextExists() || isPreviousExists(); } /** * Set the index at which the page to display should start */ public void setFirstResult(Integer firstResult) { this.firstResult = firstResult; refresh(); } /** * The page size */ public Integer getMaxResults() { return maxResults; } public void setMaxResults(Integer maxResults) { this.maxResults = maxResults; refresh(); } /** * List of restrictions to apply to the query. * * For a query such as 'from Foo f' a restriction could be * 'f.bar = #{foo.bar}' */ public List<ValueExpression> getRestrictions() { return restrictions; } /** * Calling setRestrictions causes the restrictions to be reparsed and the * query refreshed */ public void setRestrictions(List<ValueExpression> restrictions) { this.restrictions = restrictions; parsedRestrictions = null; refresh(); } /** * A convenience method for registering the restrictions from Strings. This * method is primarily intended to be used from Java, not to expose a bean * property for component configuration. Use setRestrictions() for the later. */ public void setRestrictionExpressionStrings(List<String> expressionStrings) { Expressions expressions = new Expressions(); List<ValueExpression> restrictionVEs = new ArrayList<ValueExpression>(expressionStrings.size()); for (String expressionString : expressionStrings) { restrictionVEs.add(expressions.createValueExpression(expressionString)); } setRestrictions(restrictionVEs); } public List<String> getRestrictionExpressionStrings() { List<String> expressionStrings = new ArrayList<String>(); for (ValueExpression restriction : getRestrictions()) { expressionStrings.add(restriction.getExpressionString()); } return expressionStrings; } public String getGroupBy() { return groupBy; } public void setGroupBy(String groupBy) { this.groupBy = groupBy; } /** * The order clause of the query */ public String getOrder() { String column = getOrderColumn(); if (column == null) { return order; } String direction = getOrderDirection(); if (direction == null) { return column; } else { return column + ' ' + direction; } } public void setOrder(String order) { this.order = order; refresh(); } public String getOrderDirection() { return orderDirection; } public void setOrderDirection(String orderDirection) { this.orderDirection = sanitizeOrderDirection(orderDirection); } private String sanitizeOrderDirection(String direction) { if (direction == null || direction.length()==0) { return null; } else if (direction.equalsIgnoreCase(DIR_ASC)) { return DIR_ASC; } else if (direction.equalsIgnoreCase(DIR_DESC)) { return DIR_DESC; } else { throw new IllegalArgumentException("invalid order direction"); } } public String getOrderColumn() { return orderColumn; } public void setOrderColumn(String orderColumn) { this.orderColumn = sanitizeOrderColumn(orderColumn); } private String sanitizeOrderColumn(String columnName) { if (columnName == null || columnName.trim().length() == 0) { return null; } else if (ORDER_COLUMN_PATTERN.matcher(columnName).find()) { return columnName; } else { throw new IllegalArgumentException("invalid order column (\"" + columnName + "\" must match the regular expression \"" + ORDER_COLUMN_PATTERN + "\")"); } } public String getRestrictionLogicOperator() { return restrictionLogicOperator != null ? restrictionLogicOperator : LOGIC_OPERATOR_AND; } public void setRestrictionLogicOperator(String operator) { restrictionLogicOperator = sanitizeRestrictionLogicOperator(operator); } private String sanitizeRestrictionLogicOperator(String operator) { if (operator == null || operator.trim().length() == 0) { return LOGIC_OPERATOR_AND; } if (!(LOGIC_OPERATOR_AND.equals(operator) || LOGIC_OPERATOR_OR.equals(operator))) { throw new IllegalArgumentException("Invalid restriction logic operator: " + operator); } else { return operator; } } protected List<ValueExpression> getQueryParameters() { return queryParameters; } protected List<ValueExpression> getRestrictionParameters() { return restrictionParameters; } private static boolean isAnyParameterDirty(List<ValueExpression> valueBindings, List<Object> lastParameterValues) { if (lastParameterValues==null) return true; for (int i=0; i<valueBindings.size(); i++) { Object parameterValue = valueBindings.get(i).getValue(); Object lastParameterValue = lastParameterValues.get(i); //treat empty strings as null, for consistency with isRestrictionParameterSet() if ( "".equals(parameterValue) ) parameterValue = null; if ( "".equals(lastParameterValue) ) lastParameterValue = null; if ( parameterValue!=lastParameterValue && ( parameterValue==null || !parameterValue.equals(lastParameterValue) ) ) { return true; } } return false; } private static List<Object> getParameterValues(List<ValueExpression> valueBindings) { List<Object> values = new ArrayList<Object>( valueBindings.size() ); for (int i=0; i<valueBindings.size(); i++) { values.add( valueBindings.get(i).getValue() ); } return values; } protected void evaluateAllParameters() { setQueryParameterValues( getParameterValues( getQueryParameters() ) ); setRestrictionParameterValues( getParameterValues( getRestrictionParameters() ) ); } protected boolean isAnyParameterDirty() { return isAnyParameterDirty( getQueryParameters(), getQueryParameterValues() ) || isAnyParameterDirty( getRestrictionParameters(), getRestrictionParameterValues() ); } protected List<Object> getQueryParameterValues() { return queryParameterValues; } protected void setQueryParameterValues(List<Object> queryParameterValues) { this.queryParameterValues = queryParameterValues; } protected List<Object> getRestrictionParameterValues() { return restrictionParameterValues; } protected void setRestrictionParameterValues(List<Object> restrictionParameterValues) { this.restrictionParameterValues = restrictionParameterValues; } protected List<E> truncResultList(List<E> results) { Integer mr = getMaxResults(); if ( mr!=null && results.size() > mr ) { return results.subList(0, mr); } else { return results; } } protected boolean isUseWildcardAsCountQuerySubject() { return useWildcardAsCountQuerySubject; } protected void setUseWildcardAsCountQuerySubject(boolean useCompliantCountQuerySubject) { this.useWildcardAsCountQuerySubject = useCompliantCountQuerySubject; } }