/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * This program 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 General Public License and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.enterprise.server.util; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.persistence.EntityManager; import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.persistence.Query; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.annotations.IndexColumn; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.authz.Permission; import org.rhq.core.domain.criteria.AlertCriteria; import org.rhq.core.domain.criteria.Criteria; import org.rhq.core.domain.criteria.ResourceCriteria; import org.rhq.core.domain.criteria.ResourceGroupCriteria; import org.rhq.core.domain.criteria.ResourceOperationHistoryCriteria; import org.rhq.core.domain.criteria.SubjectCriteria; import org.rhq.core.domain.operation.OperationRequestStatus; import org.rhq.core.domain.resource.ResourceCategory; import org.rhq.core.domain.search.SearchSubsystem; import org.rhq.core.domain.server.PersistenceUtility; import org.rhq.core.domain.tagging.Tag; import org.rhq.core.domain.util.OrderingField; import org.rhq.core.domain.util.PageControl; import org.rhq.core.domain.util.PageOrdering; import org.rhq.core.util.exception.ThrowableUtil; import org.rhq.enterprise.server.search.SearchExpressionException; import org.rhq.enterprise.server.search.execution.SearchTranslationManager; /** * A query generator used to generate queries with specific filtering, prefetching, or sorting requirements. * * @author Joseph Marques */ public final class CriteriaQueryGenerator { private static final Log LOG = LogFactory.getLog(CriteriaQueryGenerator.class); public enum AuthorizationTokenType { RESOURCE, // specifies the resource alias to join on for standard res-group-role-subject authorization checking GROUP, // specifies the group alias to join on for standard group-role-subject authorization checking BUNDLE, // specifies the bundle alias to join on for standard bundle-bundleGroup-role-subject authorization checking BUNDLE_GROUP // specifies the bundle group alias to join on for standard bundleGroup-role-subject authorization checking } private Criteria criteria; private String searchExpressionWhereClause; private Subject subject; private String authorizationPermsFragment; private String authorizationCustomConditionFragment; private int authorizationSubjectId; private String alias; private String className; private String projection; private String countProjection; private String groupByClause; private String havingClause; private String fromClause; private boolean groupByOrderAliases; private static String NL = System.getProperty("line.separator"); private static List<String> EXPRESSION_START_KEYWORDS; private List<Field> persistentBagFields = new ArrayList<Field>(); private List<Field> joinFetchFields = new ArrayList<Field>(); static { EXPRESSION_START_KEYWORDS = new ArrayList<String>(2); EXPRESSION_START_KEYWORDS.add("NOT"); EXPRESSION_START_KEYWORDS.add("EXISTS"); } public CriteriaQueryGenerator(Criteria criteria) { this(LookupUtil.getSubjectManager().getOverlord(), criteria); } public CriteriaQueryGenerator(Subject subject, Criteria criteria) { this.subject = subject; this.criteria = criteria; this.className = criteria.getPersistentClass().getSimpleName(); this.alias = this.criteria.getAlias(); initializeJPQLFragmentFromSearchExpression(); } /** * set this to override FROM clause * @param fromClause new clause */ public void overrideFromClause(String fromClause) { this.fromClause = fromClause; } /** * if enabled, adds ordering field aliases to GROUP BY statement. This is useful in case of custom projection together with * {@link #overrideFromClause(String)} and {@link #setGroupByClause(String)}. * @param groupByOrderAliases */ public void setGroupByOrderAliases(boolean groupByOrderAliases) { this.groupByOrderAliases = groupByOrderAliases; } public void setAuthorizationCustomConditionFragment(String fragment) { this.authorizationCustomConditionFragment = fragment; } public void setAuthorizationResourceFragment(AuthorizationTokenType type, int subjectId) { String defaultFragment; if (type == AuthorizationTokenType.RESOURCE) { defaultFragment = "resource"; setAuthorizationResourceFragment(type, defaultFragment, subjectId); } else if (type == AuthorizationTokenType.GROUP) { defaultFragment = "group"; setAuthorizationResourceFragment(type, defaultFragment, subjectId); } else { throw new IllegalArgumentException(this.getClass().getSimpleName() + " does not yet support generating resource queries for '" + type + "' token types"); } } private String fixFilterOverride(String expression, String fieldName) { boolean fuzzyMatch = expression.toLowerCase().contains(" like ") && !expression.toLowerCase().contains("select"); // Don't fuzzy match subselects boolean wantCaseInsensitiveMatch = !criteria.isCaseSensitive() && fuzzyMatch; while (expression.indexOf('?') != -1) { String replacement = ":" + fieldName; expression = expression.replaceFirst("\\?", replacement); } // if the override expression does not follow the usual format of ( field operator expression ) // then don't prepend the alias or deal with other special handling. The override must be left // explicit. if (!expressionStartsWithKeyword(expression)) { if (wantCaseInsensitiveMatch) { int indexOfFirstSpace = expression.indexOf(" "); String filterToken = expression.substring(0, indexOfFirstSpace); expression = "LOWER( " + alias + "." + filterToken + " ) " + expression.substring(indexOfFirstSpace); } else { expression = alias + "." + expression; } } if (fuzzyMatch) { expression += QueryUtility.getEscapeClause(); } return expression; } private boolean expressionStartsWithKeyword(String expression) { expression = expression.trim(); int i = expression.trim().indexOf(" "); String startToken = expression.substring(0, i); return EXPRESSION_START_KEYWORDS.contains(startToken.toUpperCase()); } public void setAuthorizationResourceFragment(AuthorizationTokenType type, String fragment, int subjectId) { this.authorizationSubjectId = subjectId; if (type == AuthorizationTokenType.RESOURCE) { setAuthorizationCustomConditionFragment(getEnhancedResourceAuthorizationWhereFragment(fragment, subjectId)); } else if (type == AuthorizationTokenType.GROUP) { // support for: 1) role-based for groups, 2) role-based for containing cluster groups, 3) private groups setAuthorizationCustomConditionFragment(getEnhancedGroupAuthorizationWhereFragment(fragment, subjectId)); } else { throw new IllegalArgumentException(this.getClass().getSimpleName() + " does not yet support generating queries for '" + type + "' token types"); } // If the query results are narrowed by requiredPerms generate the fragment now. It's done // here for two reasons. First, it seems to make sense to apply this only when an authFragment is // being used. Second, because one day the query may be less brute force and may modify or // leverage the joinFragment above. But, after extensive trying a more elegant // query could not be constructed due to Hibernate limitations. So, for now, here it is... List<Permission> requiredPerms = this.criteria.getRequiredPermissions(); if (!(null == requiredPerms || requiredPerms.isEmpty())) { this.authorizationPermsFragment = "" // + "( SELECT COUNT(DISTINCT p)" + NL // + " FROM Subject innerSubject" + NL // + " JOIN innerSubject.roles r" + NL // + " JOIN r.permissions p" + NL // + " WHERE innerSubject.id = " + this.authorizationSubjectId + NL // + " AND p IN ( :requiredPerms ) ) = :requiredPermsSize" + NL; } } private String getEnhancedResourceAuthorizationWhereFragment(String fragment, int subjectId) { String customAuthzFragment = "" // + "( %aliasWithFragment%.id IN ( SELECT %innerAlias%.id " + NL // + " FROM %alias% innerAlias " + NL // + " JOIN %innerAlias%.implicitGroups g JOIN g.roles r JOIN r.subjects s " + NL // + " WHERE s.id = %subjectId% ) )" + NL; // String aliasReplacement = criteria.getAlias() + (fragment != null ? "." + fragment : ""); String innerAliasReplacement = "innerAlias" + (fragment != null ? "." + fragment : ""); customAuthzFragment = customAuthzFragment.replace("%alias%", criteria.getAlias()); customAuthzFragment = customAuthzFragment.replace("%aliasWithFragment%", aliasReplacement); customAuthzFragment = customAuthzFragment.replace("%innerAlias%", innerAliasReplacement); customAuthzFragment = customAuthzFragment.replace("%subjectId%", String.valueOf(subjectId)); return customAuthzFragment; } private String getEnhancedGroupAuthorizationWhereFragment(String fragment, int subjectId) { String customAuthzFragment = "" // + "( %aliasWithFragment%.id IN ( SELECT %innerAlias%.id " + NL // + " FROM %alias% innerAlias " + NL // + " JOIN %innerAlias%.roles r JOIN r.subjects s " + NL // + " WHERE s.id = %subjectId% )" + NL // + " OR" + NL // + " %aliasWithFragment%.id IN ( SELECT %innerAlias%.id " + NL // + " FROM %alias% innerAlias " + NL // + " JOIN %innerAlias%.clusterResourceGroup crg JOIN crg.roles r JOIN r.subjects s " + NL // + " WHERE crg.recursive = true AND s.id = %subjectId% )" + NL // + " OR" + NL // + " %aliasWithFragment%.id IN ( SELECT %innerAlias%.id" + NL // + " FROM %alias% innerAlias " + NL // + " JOIN %innerAlias%.subject s" + NL // + " WHERE s.id = %subjectId% ) ) " + NL; String aliasReplacement = criteria.getAlias() + (fragment != null ? "." + fragment : ""); String innerAliasReplacement = "innerAlias" + (fragment != null ? "." + fragment : ""); customAuthzFragment = customAuthzFragment.replace("%alias%", criteria.getAlias()); customAuthzFragment = customAuthzFragment.replace("%aliasWithFragment%", aliasReplacement); customAuthzFragment = customAuthzFragment.replace("%innerAlias%", innerAliasReplacement); customAuthzFragment = customAuthzFragment.replace("%subjectId%", String.valueOf(subjectId)); return customAuthzFragment; } public void setAuthorizationBundleFragment(AuthorizationTokenType type, int subjectId) { if (type == AuthorizationTokenType.BUNDLE) { setAuthorizationBundleFragment(type, subjectId, "bundle"); } else if (type == AuthorizationTokenType.BUNDLE_GROUP) { setAuthorizationBundleFragment(type, subjectId, "bundleGroup"); } else { throw new IllegalArgumentException(this.getClass().getSimpleName() + " does not yet support generating bundle queries for '" + type + "' token types"); } } public void setAuthorizationBundleFragment(AuthorizationTokenType type, int subjectId, String fragment) { if (type == AuthorizationTokenType.BUNDLE) { setAuthorizationBundleFragment(subjectId, fragment); } else if (type == AuthorizationTokenType.BUNDLE_GROUP) { setAuthorizationBundleGroupFragment(subjectId, fragment); } else { throw new IllegalArgumentException(this.getClass().getSimpleName() + " does not yet support generating bundle queries for '" + type + "' token types"); } } private void setAuthorizationBundleFragment(int subjectId, String fragment) { this.authorizationSubjectId = subjectId; String customAuthzFragment = "" // + "( %aliasWithFragment%.id IN ( SELECT %innerAlias%.id " + NL // + " FROM %alias% innerAlias " + NL // + " JOIN %innerAlias%.bundleGroups g JOIN g.roles r JOIN r.subjects s " + NL // + " WHERE s.id = %subjectId% ) )" + NL; // String aliasReplacement = criteria.getAlias() + (fragment != null ? "." + fragment : ""); String innerAliasReplacement = "innerAlias" + (fragment != null ? "." + fragment : ""); customAuthzFragment = customAuthzFragment.replace("%alias%", criteria.getAlias()); customAuthzFragment = customAuthzFragment.replace("%aliasWithFragment%", aliasReplacement); customAuthzFragment = customAuthzFragment.replace("%innerAlias%", innerAliasReplacement); customAuthzFragment = customAuthzFragment.replace("%subjectId%", String.valueOf(subjectId)); this.authorizationCustomConditionFragment = customAuthzFragment; // If the query results are narrowed by requiredPerms generate the fragment now. It's done // here for two reasons. First, it seems to make sense to apply this only when an authFragment is // being used. Second, because one day the query may be less brute force and may modify or // leverage the joinFragment above. But, after extensive trying a more elegant // query could not be constructed due to Hibernate limitations. So, for now, here it is... List<Permission> requiredPerms = this.criteria.getRequiredPermissions(); if (!(null == requiredPerms || requiredPerms.isEmpty())) { this.authorizationPermsFragment = "" // + "( SELECT COUNT(DISTINCT p)" + NL // + " FROM Subject innerSubject" + NL // + " JOIN innerSubject.roles r" + NL // + " JOIN r.permissions p" + NL // + " WHERE innerSubject.id = " + this.authorizationSubjectId + NL // + " AND p IN ( :requiredPerms ) ) = :requiredPermsSize" + NL; } } private void setAuthorizationBundleGroupFragment(int subjectId, String fragment) { this.authorizationSubjectId = subjectId; String customAuthzFragment = "" // + "( %aliasWithFragment%.id IN ( SELECT %innerAlias%.id " + NL // + " FROM %alias% innerAlias " + NL // + " JOIN %innerAlias%.roles r JOIN r.subjects s " + NL // + " WHERE s.id = %subjectId% ) ) " + NL; String aliasReplacement = criteria.getAlias() + (fragment != null ? "." + fragment : ""); String innerAliasReplacement = "innerAlias" + (fragment != null ? "." + fragment : ""); customAuthzFragment = customAuthzFragment.replace("%alias%", criteria.getAlias()); customAuthzFragment = customAuthzFragment.replace("%aliasWithFragment%", aliasReplacement); customAuthzFragment = customAuthzFragment.replace("%innerAlias%", innerAliasReplacement); customAuthzFragment = customAuthzFragment.replace("%subjectId%", String.valueOf(subjectId)); this.authorizationCustomConditionFragment = customAuthzFragment; // If the query results are narrowed by requiredPerms generate the fragment now. It's done // here for two reasons. First, it seems to make sense to apply this only when an authFragment is // being used. Second, because one day the query may be less brute force and may modify or // leverage the joinFragment above. But, after extensive trying a more elegant // query could not be constructed due to Hibernate limitations. So, for now, here it is... List<Permission> requiredPerms = this.criteria.getRequiredPermissions(); if (!(null == requiredPerms || requiredPerms.isEmpty())) { this.authorizationPermsFragment = "" // + "( SELECT COUNT(DISTINCT p)" + NL // + " FROM Subject innerSubject" + NL // + " JOIN innerSubject.roles r" + NL // + " JOIN r.permissions p" + NL // + " WHERE innerSubject.id = " + this.authorizationSubjectId + NL // + " AND p IN ( :requiredPerms ) ) = :requiredPermsSize" + NL; } } public String getParameterReplacedQuery(boolean countQuery) { String query = getQueryString(countQuery); for (Map.Entry<String, Object> critField : getFilterFields(criteria).entrySet()) { Object value = critField.getValue(); if (value instanceof Tag) { Tag tag = (Tag) value; query = query.replace(":tagNamespace", tag.getNamespace()); query = query.replace(":tagSemantic", tag.getSemantic()); query = query.replace(":tagName", tag.getName()); } else { value = getParameterReplacedValue(critField.getKey(), value); if (LOG.isDebugEnabled()) { LOG.debug("Bind: (" + critField.getKey() + ", " + value + ")"); } query = query.replace(":" + critField.getKey(), String.valueOf(value)); } } if (null != this.authorizationPermsFragment) { List<Permission> requiredPerms = this.criteria.getRequiredPermissions(); String perms = requiredPerms.toString(); // [data1, data, data3] query = query.replace(":requiredPerms", perms.subSequence(1, perms.length() - 1)); // remove first/last characters query = query.replace(":requiredPermsSize", String.valueOf(requiredPerms.size())); } return query; } private String getParameterReplacedValue(String filter, Object value) { String returnValue; if (value instanceof String) { returnValue = "'" + prepareStringBindValue(filter, (String) value) + "'"; } else if (value instanceof Enum<?>) { // note: this strategy won't work for entities with multiple enums that are persisted differently EnumType type = getPersistenceEnumType(value.getClass()); if (type == EnumType.STRING) { returnValue = "'" + String.valueOf(value) + "'"; } else { returnValue = String.valueOf(value); } } else if (value instanceof List<?>) { List<?> valueList = (List<?>) value; StringBuilder results = new StringBuilder(); boolean first = true; for (Object nextValue : valueList) { if (first) { first = false; } else { results.append(","); } results.append(getParameterReplacedValue(filter, nextValue)); } returnValue = results.toString(); } else { returnValue = String.valueOf(value); } return returnValue; } // calculates @Enumerated(EnumType.STRING) or @Enumerated(EnumType.ORDINAL) private EnumType getPersistenceEnumType(Class<?> enumFieldType) { for (Field nextField : getClass().getFields()) { nextField.setAccessible(true); if (nextField.getType().equals(enumFieldType)) { Enumerated enumeratedAnnotation = nextField.getAnnotation(Enumerated.class); if (enumeratedAnnotation != null) { return enumeratedAnnotation.value(); } } } return EnumType.STRING; // catch-all } public String getQueryString(boolean countQuery) { StringBuilder results = new StringBuilder(); PageControl pc = getPageControl(criteria); results.append("SELECT "); List<String> fetchFields = getFetchFields(criteria); boolean useJoinFetch = projection == null && pc.isUnlimited() && !fetchFields.isEmpty(); if (countQuery) { if (countProjection != null) { //just use whatever we are told results.append(countProjection).append(NL); } else if (groupByClause == null) { // non-grouped method // use count(*) instead of count(alias) due to https://bugzilla.redhat.com/show_bug.cgi?id=699842 results.append("COUNT(*)").append(NL); } else { // gets the count of the number of aggregate/grouped rows // NOTE: this only works when the groupBy is a single element, as opposed to a list of elements results.append("COUNT(DISTINCT ").append(groupByClause).append(")").append(NL); } } else { if (projection == null) { //we need to just return distinct results when using JOIN FETCH otherwise we might see duplicates //in the result set and create discrepancy between the data query and the count query (which doesn't //use the JOIN FETCH but only the WHERE clause). if (useJoinFetch) { results.append("DISTINCT "); } results.append(alias).append(NL); } else { results.append(projection).append(NL); } } if (fromClause != null) { results.append("FROM ").append(fromClause).append(' ').append(NL); } else { results.append("FROM ").append(className).append(' ').append(alias).append(NL); } if (!countQuery) { /* * don't fetch in the count query to avoid: "query specified join fetching, * but the owner of the fetched association was not present in the select list" */ for (String fetchField : fetchFields) { if (isPersistentBag(fetchField)) { addPersistentBag(fetchField); } else { if (this.projection == null) { /* * if not altering the projection, join fetching can be used * to retrieve the associated instance in the same SELECT * * We further avoid a JOIN FETCH when executing queries with limits. * Such execution has performance problems that we solve by initializing the fields * "manually" in the CriteriaQueryRunner and by defining a default batch fetch size in the * persistence.xml. */ if (useJoinFetch) { results.append("LEFT JOIN FETCH ").append(alias).append('.').append(fetchField).append(NL); } else { addJoinFetch(fetchField); } } else { /* * if the projection is altered (perhaps converting it into a constructor query), then all * fields specified in the fetch must be in the explicit return list. this is not possible * today with constructor queries, so any altered projection will implicitly disable fetching. * instead, we'll record which fields need to be explicitly fetched after the primary query * returns the bulk of the data, and use a similar methodology at the SLSB layer to eagerly * load those before returning the PageList back to the caller. */ addJoinFetch(fetchField); } } } } // figure out the 'LEFT JOIN's needed for 'ORDER BY' tokens List<String> orderingFieldRequiredJoins = new ArrayList<String>(); List<String> orderingFieldTokens = new ArrayList<String>(); List<String> orderingFieldAliases = new ArrayList<String>(); for (OrderingField orderingField : pc.getOrderingFields()) { PageOrdering ordering = orderingField.getOrdering(); String fieldName = orderingField.getField(); String override = criteria.getJPQLSortOverride(fieldName); String suffix = (override == null) ? fieldName : override; /* * do not prefix the alias when: * * 1) if the suffix is numerical, which allows us to sort by column ordinal * 2) if the user wants full control and has explicitly chosen to disable alias prepending */ boolean doNotPrefixAlias = isNumber(suffix) || criteria.hasCustomizedSorting(); String sortFragment = doNotPrefixAlias ? suffix : (alias + "." + suffix); if (criteria.hasCustomizedSorting()) { // customized sorting does not get LEFT JOIN expressions added orderingFieldTokens.add(sortFragment + " " + ordering); continue; } int lastDelimiterIndex = sortFragment.lastIndexOf('.'); if (lastDelimiterIndex == -1) { // does not require joins, just add the ordering field token directly orderingFieldTokens.add(sortFragment + " " + ordering); orderingFieldAliases.add(sortFragment); continue; } int firstDelimiterIndex = sortFragment.indexOf('.'); if (firstDelimiterIndex == lastDelimiterIndex) { // only one dot implies its a property/field directly off of the primary alias // thus, also does not require joins, just add the ordering field token directly orderingFieldTokens.add(sortFragment + " " + ordering); orderingFieldAliases.add(sortFragment); continue; } String expressionRoot = sortFragment.substring(0, lastDelimiterIndex); String expressionLeaf = sortFragment.substring(lastDelimiterIndex + 1); int expressionRootIndex = orderingFieldRequiredJoins.indexOf(expressionRoot); String joinAlias; if (expressionRootIndex == -1) { // new join joinAlias = "orderingField" + orderingFieldRequiredJoins.size(); orderingFieldRequiredJoins.add(expressionRoot); results.append("LEFT JOIN ").append(expressionRoot).append(" ").append(joinAlias).append(NL); } else { joinAlias = "orderingField" + expressionRootIndex; } orderingFieldAliases.add(joinAlias + "." + expressionLeaf); orderingFieldTokens.add(joinAlias + "." + expressionLeaf + " " + ordering); } Map<String, Object> filterFields = getFilterFields(criteria); String conjunctiveFragment = criteria.isFiltersOptional() ? "OR " : "AND "; boolean wantCaseInsensitiveMatch = !criteria.isCaseSensitive(); // criteria StringBuilder conjunctiveResults = new StringBuilder(); boolean firstCrit = true; for (Map.Entry<String, Object> filterField : filterFields.entrySet()) { Object filterFieldValue = filterField.getValue(); // if this filter field is non-binding (that is, the query has no parameter whose value is to be bound for the field) // and that filter field is turned off, do nothing and continue to the next filter. // this in effect does not filter on this field at all. if (Criteria.NonBindingOverrideFilter.OFF.equals(filterFieldValue)) { continue; } if (firstCrit) { firstCrit = false; } else { conjunctiveResults.append(NL).append(conjunctiveFragment); } String fieldName = filterField.getKey(); String override = criteria.getJPQLFilterOverride(fieldName); String fragment; if (override != null) { fragment = fixFilterOverride(override, fieldName); } else { String operator = "="; if (filterFieldValue instanceof String) { operator = "like"; if (wantCaseInsensitiveMatch) { fragment = "LOWER( " + alias + "." + fieldName + " ) " + operator + " :" + fieldName; } else { fragment = alias + "." + fieldName + " " + operator + " :" + fieldName; } fragment += QueryUtility.getEscapeClause(); } else { fragment = alias + "." + fieldName + " " + operator + " :" + fieldName; } } conjunctiveResults.append(fragment).append(' '); } if (conjunctiveResults.length() > 0 || authorizationPermsFragment != null || authorizationCustomConditionFragment != null || searchExpressionWhereClause != null) { results.append("WHERE "); if (conjunctiveResults.length() > 0) { results.append("( ").append(conjunctiveResults).append(")"); } } // authorization if (authorizationPermsFragment != null) { if (firstCrit) { firstCrit = false; } else { // always want AND for security, regardless of conjunctiveFragment results.append(NL).append(" AND "); } results.append(this.authorizationPermsFragment).append(" "); } if (authorizationCustomConditionFragment != null) { if (firstCrit) { firstCrit = false; } else { // always want AND for security, regardless of conjunctiveFragment results.append(NL).append(" AND "); } results.append(this.authorizationCustomConditionFragment); } if (searchExpressionWhereClause != null) { if (!firstCrit) { // always want to additionally filter by translated from the RHQL search expression results.append(NL).append(" AND "); } results.append(searchExpressionWhereClause); } if (!countQuery) { // group by clause if (groupByClause != null) { results.append(NL).append("GROUP BY ").append(groupByClause); if (groupByOrderAliases) { results.append(", "); for (String field : orderingFieldAliases) { if (!field.equals(groupByClause)) { // avoid duplicities in groupby fields results.append(field).append(", "); } } results.deleteCharAt(results.length() - 2); // always delete last comma } } // having clause if (havingClause != null) { results.append(NL).append("HAVING ").append(havingClause); } // ordering clause boolean first = true; for (String next : orderingFieldTokens) { if (first) { results.append(NL).append("ORDER BY "); first = false; } else { results.append(", "); } results.append(next); } } results.append(NL); if (LOG.isDebugEnabled()) { LOG.debug(results); } return results.toString(); } private boolean isNumber(String input) { if (input == null) { return false; } for (char next : input.toCharArray()) { if (!Character.isDigit(next)) { return false; } } return true; } public List<String> getFetchFields(Criteria criteria) { List<String> results = new ArrayList<String>(); for (Field fetchField : CriteriaUtil.getFields(criteria, Criteria.Type.FETCH)) { Object fetchFieldValue; try { fetchField.setAccessible(true); fetchFieldValue = fetchField.get(criteria); } catch (IllegalAccessException iae) { throw new RuntimeException(iae); } if (fetchFieldValue != null) { boolean shouldFetch = ((Boolean) fetchFieldValue).booleanValue(); if (shouldFetch) { results.add(getCleansedFieldName(fetchField, 5)); } } } // for (String entry : results) { // LOG.info("Fetch: (" + entry + ")"); // } return results; } public static String getCleansedFieldName(Field field, int leadingCharsToStrip) { String fieldNameFragment = field.getName().substring(leadingCharsToStrip); String fieldName = Character.toLowerCase(fieldNameFragment.charAt(0)) + fieldNameFragment.substring(1); return fieldName; } public Map<String, Object> getFilterFields(Criteria criteria) { Map<String, Object> results = new HashMap<String, Object>(); for (Field filterField : CriteriaUtil.getFields(criteria, Criteria.Type.FILTER)) { Object filterFieldValue; try { filterField.setAccessible(true); filterFieldValue = filterField.get(criteria); } catch (IllegalAccessException iae) { throw new RuntimeException(iae); } if (filterFieldValue != null) { results.put(getCleansedFieldName(filterField, 6), filterFieldValue); } } return results; } private void initializeJPQLFragmentFromSearchExpression() { String searchExpression = criteria.getSearchExpression(); if (searchExpression == null) { return; } try { Class<?> entityClass = criteria.getPersistentClass(); SearchTranslationManager searchManager = new SearchTranslationManager(criteria.getAlias(), subject, SearchSubsystem.get(entityClass)); searchManager.setExpression(searchExpression); // translate first, if there was an error we won't add the dangling 'AND' to the where clause String translatedJPQL = searchManager.getJPQLWhereFragment(); if (LOG.isDebugEnabled()) { LOG.debug("Translated JPQL Fragment was: " + translatedJPQL); } if (translatedJPQL != null && !translatedJPQL.trim().isEmpty() && !"( )".equals(translatedJPQL.trim())) { searchExpressionWhereClause = translatedJPQL; } } catch (SearchExpressionException see) { throw see; // bubble up to the top } catch (RuntimeException re) { LOG.error("Could not get JPQL translation for '" + searchExpression + "': " + ThrowableUtil.getAllMessages(re, true)); throw re; // don't wrap exceptions that are already RuntimeExceptions in another RuntimeException } catch (Exception e) { LOG.error("Could not get JPQL translation for '" + searchExpression + "': " + ThrowableUtil.getAllMessages(e, true)); throw new RuntimeException(e); } } private boolean isPersistentBag(String fieldName) { Field field = findField(fieldName); return field != null && isAList(field) && !field.isAnnotationPresent(IndexColumn.class); } private boolean isAList(Field field) { Class<?> fieldType = field.getType(); if (List.class.isAssignableFrom(fieldType)) { return true; } for (Class<?> declaredInterface : fieldType.getInterfaces()) { if (List.class.isAssignableFrom(declaredInterface)) { return true; } } return false; } private void addPersistentBag(String fieldName) { Field f = findField(fieldName); if (f == null) { LOG.warn("Failed to add persistent bag collection [" + fieldName + "] on class [" + criteria.getPersistentClass().getName() + "]. There doesn't seem to be a field of that name on the class or any of its superclasses."); } else { persistentBagFields.add(f); } } private void addJoinFetch(String fieldName) { Field f = findField(fieldName); if (f == null) { LOG.warn("Failed to add join fetch field [" + fieldName + "] on class [" + criteria.getPersistentClass().getName() + "]. There doesn't seem to be a field of that name on the class or any of its superclasses."); } else { joinFetchFields.add(f); } } private Field findField(String fieldName) { Class<?> cls = criteria.getPersistentClass(); while (cls != null) { try { return cls.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { cls = cls.getSuperclass(); } } return null; } /** * <strong>Note:</strong> This method should only be called after {@link #getQueryString(boolean)}} because it is * that method where the persistentBagFields property is initialized. * * @return Returns a list of fields from the persistent class to which the criteria class corresponds. The fields in * the list are themselves instances of List and have "bag" semantics. */ public List<Field> getPersistentBagFields() { return persistentBagFields; } /** * <strong>Note:</strong> This method should only be called after {@link #getQueryString(boolean)}} because it is * that method where the persistentBagFields property is initialized. * <p/> * The elements of the returned list are a (sub)set of the fields that the criteria object specified to be fetched * (using the fetchXXX() methods). If the {@link CriteriaQueryRunner} is not set to automatically fetch all the * fields, you need to manually initialize these fields by for example using * {@link CriteriaQueryRunner#initFetchFields(Object)} method on each of the results. * * @see #alterProjection(String) <code>alterProjection(String)</code> for special attention you need to make when * mixing fetching fields and altered projection. * * @return Returns a list of fields from the persistent class to which the criteria class corresponds. The fields in * the list are fields specified by the criteria to be fetched (using the fetchXXX() methods). */ public List<Field> getJoinFetchFields() { return joinFetchFields; } /** * If you want to return something other than the list of entities represented by the passed Criteria object, * you can alter the projection here to return a customized subset or superset of data. The projection will * only affect the ResultSet for the data query, not the count query. * <p/> * If you are projecting a composite object that does not directly extend the entity your Criteria object * represents, then you will need to manually initialize the persistent bags and fetch fields using the * {@link CriteriaQueryRunner#initFetchFields(Object)} method for each object in the results (for which you need * to instantiate the {@link CriteriaQueryRunner} with automatic fetching switched OFF). <b>Note</b> that this will * NOT work on the composite object itself. You need to pass an instance of the entity class to the *{@link CriteriaQueryRunner#initFetchFields(Object)} method. */ public void alterProjection(String projection) { this.projection = projection; } /** * Sometimes the altered projection ({@link #alterProjection(String)}) might cause the result set to have different * number of results than the default/unaltered projection. Leaving the count query in the default form could then * generate seemingly inconsistent results, where the data query and the count query wouldn't match up. * <p/> * An example of a projection that might alter the number of results is the {@code " distinct ..."} projection that * would only return distinct results from a dataset, while the default count query (COUNT(*)) would produce the * count including duplicate results that were eliminated in the returned data. * <p/> * In these cases one can also alter the count query to count the results the data query will return. * * @param countProjection a complete JPQL fragment expressing the count expression (e.g. * {@code COUNT(DISTINCT ...)}) */ public void alterCountProjection(String countProjection) { this.countProjection = countProjection; } public boolean isProjectionAltered() { return this.projection != null; } /** * The groupBy clause can be set if and only if the projection is altered. The passed argument should not be * prefixed with 'group by'; that part of the query will be auto-generated if the argument is non-null. The * new projection must follow standard rules as they apply to statements with groupBy clauses. */ public void setGroupByClause(String groupByClause) { if (groupByClause != null && projection == null) { throw new IllegalArgumentException("Must alter projection before calling setGroupByClause"); } this.groupByClause = groupByClause; } /** * The having clause can be set if and only if the groupBy clause is set. The passed argument should not be * prefixed with 'having'; that part of the query will be auto-generated if the argument is non-null. The * having clause must follow standard rules as they apply to statements with groupBy clauses. */ public void setHavingClause(String havingClause) { if (havingClause != null && groupByClause == null) { throw new IllegalArgumentException("Must add some groupBy clause before calling setHavingClause"); } this.havingClause = havingClause; } public Query getQuery(EntityManager em) { String queryString = getQueryString(false); Query query = em.createQuery(queryString); setBindValues(query); PersistenceUtility.setDataPage(query, getPageControl(criteria)); return query; } public Query getCountQuery(EntityManager em) { String countQueryString = getQueryString(true); Query query = em.createQuery(countQueryString); setBindValues(query); return query; } private void setBindValues(Query query) { for (Map.Entry<String, Object> critField : getFilterFields(criteria).entrySet()) { Object value = critField.getValue(); if (value instanceof Tag) { Tag tag = (Tag) value; query.setParameter("tagNamespace", tag.getNamespace()); query.setParameter("tagSemantic", tag.getSemantic()); query.setParameter("tagName", tag.getName()); } else if (!(value instanceof Criteria.NonBindingOverrideFilter)) { if (value instanceof String) { value = prepareStringBindValue(critField.getKey(), (String) value); } if (LOG.isDebugEnabled()) { LOG.debug("Bind: (" + critField.getKey() + ", " + value + ")"); } query.setParameter(critField.getKey(), value); } } if (null != this.authorizationPermsFragment) { List<Permission> requiredPerms = this.criteria.getRequiredPermissions(); query.setParameter("requiredPerms", requiredPerms); query.setParameter("requiredPermsSize", (long) requiredPerms.size()); } } private String prepareStringBindValue(String filter, String value) { if (!criteria.isStrict() && !arrayContains(criteria.getStrictFilters(), filter)) { value = "%" + QueryUtility.escapeSearchParameter(value) + "%"; } if (!criteria.isCaseSensitive() && !arrayContains(criteria.getCaseSensitiveFilters(), filter)) { value = value.toLowerCase(); } return value; } private boolean arrayContains(String[] strings, String s) { if (strings == null) { return false; } for (String string : strings) { if (s == null) { if (string == null) { return true; } } else if (s.equals(string)) { return true; } } return false; } public static void main(String[] args) { //testSubjectCriteria(); //testAlertCriteria(); //testInheritanceCriteria(); //testResourceCriteria(); testResourceGroupCriteria(); } public static void testSubjectCriteria() { SubjectCriteria subjectCriteria = new SubjectCriteria(); subjectCriteria.addFilterFirstName("joe"); subjectCriteria.addFilterFactive(true); subjectCriteria.fetchRoles(true); subjectCriteria.addSortName(PageOrdering.ASC); Subject overlord = LookupUtil.getSubjectManager().getOverlord(); CriteriaQueryGenerator subjectGenerator = new CriteriaQueryGenerator(overlord, subjectCriteria); System.out.println(subjectGenerator.getQueryString(false)); System.out.println(subjectGenerator.getQueryString(true)); } public static void testAlertCriteria() { AlertCriteria alertCriteria = new AlertCriteria(); alertCriteria.addFilterName("joe"); alertCriteria.addFilterDescription("query generation is cool"); alertCriteria.addFilterStartTime(42L); alertCriteria.addFilterEndTime(100L); alertCriteria.addFilterResourceIds(1, 2, 3); alertCriteria.fetchAlertDefinition(true); alertCriteria.addSortPriority(PageOrdering.DESC); alertCriteria.addSortName(PageOrdering.ASC); alertCriteria.setPaging(0, 100); alertCriteria.setFiltersOptional(true); //alertCriteria.setCaseSensitive(false); Subject overlord = LookupUtil.getSubjectManager().getOverlord(); CriteriaQueryGenerator generator = new CriteriaQueryGenerator(overlord, alertCriteria); System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true)); generator.setAuthorizationResourceFragment(AuthorizationTokenType.RESOURCE, "definition.resource", 1); System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true)); } public static void testInheritanceCriteria() { ResourceOperationHistoryCriteria historyCriteria = new ResourceOperationHistoryCriteria(); historyCriteria.addFilterResourceIds(1); historyCriteria.addFilterStatus(OperationRequestStatus.FAILURE); Subject overlord = LookupUtil.getSubjectManager().getOverlord(); CriteriaQueryGenerator generator = new CriteriaQueryGenerator(overlord, historyCriteria); System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true)); } public static void testResourceGroupCriteria() { ResourceGroupCriteria groupCriteria = new ResourceGroupCriteria(); groupCriteria.addSortName(PageOrdering.DESC); groupCriteria.addSortResourceTypeName(PageOrdering.ASC); groupCriteria.addSortPluginName(PageOrdering.DESC); CriteriaQueryGenerator generator = new CriteriaQueryGenerator(new Subject(), groupCriteria); System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true)); PageControl customPC = new PageControl(); customPC.addDefaultOrderingField("0", PageOrdering.DESC); customPC.addDefaultOrderingField("name", PageOrdering.DESC); customPC.addDefaultOrderingField("resourceType.name", PageOrdering.ASC); groupCriteria.setPageControl(customPC); System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true)); } public static void testResourceCriteria() { ResourceCriteria resourceCriteria = new ResourceCriteria(); resourceCriteria.addFilterResourceCategories(ResourceCategory.SERVER); resourceCriteria.addFilterName("marques"); resourceCriteria.fetchAgent(true); resourceCriteria.addSortResourceTypeName(PageOrdering.ASC); resourceCriteria.setCaseSensitive(true); resourceCriteria.setFiltersOptional(true); Subject overlord = LookupUtil.getSubjectManager().getOverlord(); CriteriaQueryGenerator generator = new CriteriaQueryGenerator(overlord, resourceCriteria); System.out.println(generator.getQueryString(false)); System.out.println(generator.getQueryString(true)); } public static PageControl getPageControl(Criteria criteria) { PageControl pc; if (criteria.getPageControlOverrides() != null) { pc = criteria.getPageControlOverrides(); } else { if (criteria.getPageNumber() == null || criteria.getPageSize() == null) { pc = PageControl.getUnlimitedInstance(); } else { pc = new PageControl(criteria.getPageNumber(), criteria.getPageSize()); } for (String fieldName : criteria.getOrderingFieldNames()) { for (Field sortField : CriteriaUtil.getFields(criteria, Criteria.Type.SORT)) { if (!sortField.getName().equals(fieldName)) { continue; } Object sortFieldValue; try { sortField.setAccessible(true); sortFieldValue = sortField.get(criteria); } catch (IllegalAccessException iae) { throw new RuntimeException(iae); } if (sortFieldValue != null) { PageOrdering pageOrdering = (PageOrdering) sortFieldValue; pc.addDefaultOrderingField(getCleansedFieldName(sortField, 4), pageOrdering); } } } } // Unless paging is unlimited or it's not supported, add a sort on ID. This ensures that when paging // we always have a consistent ordering. In other words, if the data set is unchanged between pages, there // will no overlap/repetition of rows. Note that this applies even if other sort fields have been // set, because they may still not have unique values. See https://bugzilla.redhat.com/show_bug.cgi?id=966665. if (!pc.isUnlimited() && criteria.isSupportsAddSortId()) { pc.addDefaultOrderingField("id"); } return pc; } }