/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.jdbcconfig.internal; import java.util.Collections; import java.util.Map; import java.util.Set; import org.geoserver.catalog.Info; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.Capabilities; import org.geotools.filter.visitor.CapabilitiesFilterSplitter; import org.geotools.filter.visitor.ClientTransactionAccessor; import org.geotools.filter.visitor.LiteralDemultiplyingFilterVisitor; import org.geotools.filter.visitor.SimplifyingFilterVisitor; import org.opengis.feature.type.FeatureType; import org.opengis.filter.Filter; import org.opengis.filter.sort.SortBy; import org.opengis.filter.sort.SortOrder; class QueryBuilder<T extends Info> { @SuppressWarnings("unused") private static final SortBy DEFAULT_ORDER = CommonFactoryFinder.getFilterFactory().sort("id", SortOrder.ASCENDING); private Integer offset; private Integer limit; private SortBy[] sortOrder; private final boolean isCountQuery; // yuck private final Dialect dialect; private Class<T> queryType; private FilterToCatalogSQL predicateBuilder; private DbMappings dbMappings; private Filter originalFilter; private Filter supportedFilter; private Filter unsupportedFilter; private boolean offsetLimitApplied=false; /** * @param clazz * @param * * */ private QueryBuilder(Dialect dialect, final Class<T> clazz, DbMappings dbMappings, final boolean isCountQuery) { this.dialect = dialect; this.queryType = clazz; this.dbMappings = dbMappings; this.isCountQuery = isCountQuery; this.originalFilter = this.supportedFilter = this.unsupportedFilter = Filter.INCLUDE; } public static <T extends Info> QueryBuilder<T> forCount(Dialect dialect, final Class<T> clazz, DbMappings dbMappings) { return new QueryBuilder<T>(dialect, clazz, dbMappings, true); } public static <T extends Info> QueryBuilder<T> forIds(Dialect dialect, final Class<T> clazz, DbMappings dbMappings) { return new QueryBuilder<T>(dialect, clazz, dbMappings, false); } public Filter getUnsupportedFilter() { return unsupportedFilter; } public Filter getSupportedFilter() { return supportedFilter; } public Map<String, Object> getNamedParameters() { Map<String, Object> params = Collections.emptyMap(); if (predicateBuilder != null) { params = predicateBuilder.getNamedParameters(); } return params; } public QueryBuilder<T> offset(Integer offset) { this.offset = offset; return this; } public QueryBuilder<T> limit(Integer limit) { this.limit = limit; return this; } public QueryBuilder<T> sortOrder(SortBy order) { if(order==null){ this.sortOrder(); } else { this.sortOrder(new SortBy[] {order}); } return this; } public QueryBuilder<T> sortOrder(SortBy... order) { if(order==null || order.length==0){ this.sortOrder=null; } else { this.sortOrder = order; } return this; } public QueryBuilder<T> filter(Filter filter) { this.originalFilter = filter; return this; } private void querySortBy(StringBuilder query, StringBuilder whereClause, SortBy[] orders) { /* * Start with the oid and id from the object table selecting for type and the filter. * * Then left join on oid for each property to sort by to turn it into an attribute. * * The sort each of the created attribute. */ // Need to put together the ORDER BY clause as we go and then add it at the end StringBuilder orderBy = new StringBuilder(); orderBy.append("ORDER BY "); int i = 0; query.append("SELECT id FROM "); query.append("\n (SELECT oid, id FROM object WHERE "); if (queryType!=null) { query.append("type_id in (:types) /* "). append(queryType.getCanonicalName()).append(" */\n AND "); } query.append(whereClause).append(") object"); for(SortBy order: orders) { final String sortProperty = order.getPropertyName().getPropertyName(); final String subSelectName = "subSelect"+i; final String attributeName = "prop"+i; final String propertyParamName = "sortProperty"+i; final Set<Integer> sortPropertyTypeIds; sortPropertyTypeIds = dbMappings.getPropertyTypeIds(queryType, sortProperty); // Store the property type ID as a named parameter Map<String, Object> namedParameters = getNamedParameters(); namedParameters.put(propertyParamName, sortPropertyTypeIds); query.append("\n LEFT JOIN"); query.append("\n (SELECT oid, value ").append(attributeName). append(" FROM \n object_property WHERE property_type IN (:"). append(propertyParamName).append(")) ").append(subSelectName); query.append(" /* ").append(order.getPropertyName().getPropertyName()) .append(" ").append(ascDesc(order)).append(" */"); query.append("\n ON object.oid = ").append(subSelectName).append(".oid"); // Update the ORDER BY clause to be added later if(i>0) orderBy.append(", "); orderBy.append(attributeName).append(" ").append(ascDesc(order)); i++; } query.append("\n ").append(orderBy); } private StringBuilder buildWhereClause() { final SimplifyingFilterVisitor filterSimplifier = new SimplifyingFilterVisitor(); this.predicateBuilder = new FilterToCatalogSQL(this.queryType, this.dbMappings); Capabilities fcs = new Capabilities(FilterToCatalogSQL.CAPABILITIES); FeatureType parent = null; // use this to instruct the filter splitter which filters can be encoded depending on // whether a db mapping for a given property name exists ClientTransactionAccessor transactionAccessor = new ClientTransactionAccessor() { @Override public Filter getUpdateFilter(final String attributePath) { Set<PropertyType> propertyTypes; propertyTypes = dbMappings.getPropertyTypes(queryType, attributePath); final boolean isMappedProp = !propertyTypes.isEmpty(); if (isMappedProp) { // continue normally return null; } // tell the caps filter splitter this property name is not encodable return Filter.EXCLUDE; } @Override public Filter getDeleteFilter() { return null; } }; CapabilitiesFilterSplitter filterSplitter; filterSplitter = new CapabilitiesFilterSplitter(fcs, parent, transactionAccessor); final Filter filter = (Filter) this.originalFilter.accept(filterSimplifier, null); filter.accept(filterSplitter, null); Filter supported = filterSplitter.getFilterPre(); Filter unsupported = filterSplitter.getFilterPost(); Filter demultipliedFilter = (Filter) supported.accept(new LiteralDemultiplyingFilterVisitor(), null); this.supportedFilter = (Filter) demultipliedFilter.accept(filterSimplifier, null); this.unsupportedFilter = (Filter) unsupported.accept(filterSimplifier, null); StringBuilder whereClause = new StringBuilder(); return (StringBuilder) this.supportedFilter.accept(predicateBuilder, whereClause); } public StringBuilder build() { StringBuilder whereClause = buildWhereClause(); StringBuilder query = new StringBuilder(); if (isCountQuery) { if (Filter.INCLUDE.equals(this.originalFilter)) { query.append("select count(oid) from object where type_id in (:types)"); } else { query.append("select count(oid) from object where type_id in (:types) AND (\n"); query.append(whereClause).append("\n)"); } } else { SortBy[] orders = this.sortOrder; if (orders == null) { query.append("select id from object where type_id in (:types) AND (\n"); query.append(whereClause).append(")\n"); query.append(" ORDER BY oid"); } else { querySortBy(query, whereClause, orders); } applyOffsetLimit(query); } return query; } /** * When the query was built, were the offset and limit included. */ public boolean isOffsetLimitApplied() { return offsetLimitApplied; } private static String ascDesc(SortBy order) { return SortOrder.ASCENDING.equals(order.getSortOrder()) ? "ASC" : "DESC"; } protected void applyOffsetLimit(StringBuilder sql) { if(unsupportedFilter.equals(Filter.INCLUDE)){ dialect.applyOffsetLimit(sql, offset, limit); offsetLimitApplied=true; } else { offsetLimitApplied=false; } } }