All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.java.ao.Query Maven / Gradle / Ivy

Go to download

This is the core library for Active Objects. It is generic and can be embedded in any environment. As such it is generic and won't contain all connection pooling, etc.

There is a newer version: 6.1.1
Show newest version
/*
 * Copyright 2007 Daniel Spiewak
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *	    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.ao;

import net.java.ao.schema.TableNameConverter;
import net.java.ao.schema.info.EntityInfo;
import net.java.ao.schema.info.FieldInfo;
import net.java.ao.types.TypeInfo;
import net.java.ao.types.TypeManager;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.collect.Maps.newHashMap;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

/**
 * A cross-database representation of a SELECT query, independent of database provider type.
 * 

* Use with {@link net.java.ao.DatabaseProvider#renderQuery(Query, net.java.ao.schema.TableNameConverter, boolean)} * to produce the database-specific SQL. * * @author Daniel Spiewak */ public class Query implements Serializable { public enum QueryType { SELECT } private final static String PRIMARY_KEY_FIELD = "''''primary_key_field''''"; protected static final Pattern ALIAS_PATTERN = Pattern.compile("(.+?)\\s+(?:[aA][sS]\\s+)?(\\w+)$"); protected static final Pattern AGGREGATE_FUNCTION_PATTERN = Pattern.compile("(\\S+)\\((\\S+)\\)"); private final QueryType type; private String fields; private boolean distinct = false; private Class> tableType; private String table; private String whereClause; private Object[] whereParams; private String orderClause; private String groupClause; private String havingClause; private int limit = -1; private int offset = -1; private Map>, String> joins; private Map>, String> aliases = newHashMap(); /** * Create a {@link Query} and set the field list in the {@code SELECT} clause. * * @param fields The fields to select, as comma-delimited field names. Spaces are OK. Must not contain {@code "*"}. * @throws java.lang.IllegalArgumentException if fields contains {@code "*"} */ public Query(QueryType type, String fields) { validateSelectFields(fields); this.type = type; this.fields = fields; joins = new LinkedHashMap>, String>(); } public Iterable getFields() { return getFields(Function.identity()); } public List getFieldMetadata() { return getFields(this::getSingleFieldMetadata); } private List getFields(Function resultMapper) { if (fields.contains(PRIMARY_KEY_FIELD)) { return Collections.emptyList(); } else { return Arrays.stream(fields.split(",")) .map(String::trim) .map(resultMapper) .collect(toList()); } } void setFields(String[] fields) { if (fields.length == 0) { return; } StringBuilder builder = new StringBuilder(); for (String field : fields) { builder.append(field).append(','); } if (fields.length > 1) { builder.setLength(builder.length() - 1); } this.fields = builder.toString(); } void resolvePrimaryKey(FieldInfo fieldInfo) { fields = fields.replaceAll(PRIMARY_KEY_FIELD, fieldInfo.getName()); } public Query distinct() { distinct = true; return this; } public Query from(Class> tableType) { table = null; this.tableType = tableType; return this; } public Query from(String table) { tableType = null; this.table = table; return this; } public Query where(String clause, Object... params) { whereClause = clause; setWhereParams(params); return this; } public Query order(String clause) { orderClause = clause; return this; } public Query group(String clause) { groupClause = clause; return this; } public Query having(String clause) { havingClause = clause; return this; } public Query limit(int limit) { this.limit = limit; return this; } public Query offset(int offset) { this.offset = offset; return this; } public Query alias(Class> table, String alias) { if (aliases.containsValue(alias)) { throw new ActiveObjectsException("There is already a table aliased '" + alias + "' for this query!"); } aliases.put(table, alias); return this; } public String getAlias(Class> table) { return aliases.get(table); } public Query join(Class> join, String on) { joins.put(join, on); return this; } public Query join(Class> join) { joins.put(join, null); return this; } public boolean isDistinct() { return distinct; } public void setDistinct(boolean distinct) { this.distinct = distinct; } public Class> getTableType() { return tableType; } public void setTableType(Class> tableType) { this.tableType = tableType; } public String getTable() { return table; } public void setTable(String table) { this.table = table; } public String getWhereClause() { return whereClause; } public void setWhereClause(String whereClause) { this.whereClause = whereClause; } public Object[] getWhereParams() { return whereParams; } public void setWhereParams(Object[] whereParams) { this.whereParams = whereParams; if (whereParams != null) { for (int i = 0; i < whereParams.length; i++) { if (whereParams[i] instanceof RawEntity) { whereParams[i] = Common.getPrimaryKeyValue((RawEntity) whereParams[i]); } } } } public String getOrderClause() { return orderClause; } public void setOrderClause(String orderClause) { this.orderClause = orderClause; } public String getGroupClause() { return groupClause; } public void setGroupClause(String groupClause) { this.groupClause = groupClause; } public String getHavingClause() { return havingClause; } public void setHavingClause(String havingClause) { this.havingClause = havingClause; } public int getLimit() { return limit; } public void setLimit(int limit) { this.limit = limit; } public int getOffset() { return offset; } public void setOffset(int offset) { this.offset = offset; } public Map>, String> getJoins() { return Collections.unmodifiableMap(joins); } public void setJoins(Map>, String> joins) { this.joins = joins; } public QueryType getType() { return type; } public String[] getCanonicalFields(EntityInfo entityInfo) { String[] back = fields.split(","); List result = new ArrayList(); for (String fieldName : back) { final String trimmedFieldName = fieldName.trim(); Optional alias = getAlias(trimmedFieldName); if (alias.isPresent()) { result.add(alias.get()); } else if (trimmedFieldName.equals("*")) { for (FieldInfo fieldInfo : entityInfo.getFields()) { result.add(fieldInfo.getName()); } } else { result.add(trimmedFieldName); } } return result.toArray(new String[result.size()]); } protected Optional getAlias(final String field) { Matcher matcher = ALIAS_PATTERN.matcher(field); if (matcher.find()) { return Optional.of(matcher.group(2)); } return Optional.empty(); } protected FieldMetadata getSingleFieldMetadata(final String field) { final Matcher aliasMatcher = ALIAS_PATTERN.matcher(field); final String maybeAlias; final String actualFieldAndMaybeAggregate; if (aliasMatcher.find()) { maybeAlias = aliasMatcher.group(2); actualFieldAndMaybeAggregate = aliasMatcher.group(1); } else { maybeAlias = null; actualFieldAndMaybeAggregate = field; } final Matcher aggregateMatcher = AGGREGATE_FUNCTION_PATTERN.matcher(actualFieldAndMaybeAggregate); final String maybeAggregateFunction; final String actualField; if (aggregateMatcher.find()) { actualField = aggregateMatcher.group(2); maybeAggregateFunction = aggregateMatcher.group(1); } else { actualField = actualFieldAndMaybeAggregate; maybeAggregateFunction = null; } return new FieldMetadata(actualField, maybeAlias, maybeAggregateFunction); } protected String toSQL(EntityInfo, K> entityInfo, DatabaseProvider provider, TableNameConverter converter, boolean count) { if (this.tableType == null && table == null) { this.tableType = entityInfo.getEntityType(); } resolvePrimaryKey(entityInfo.getPrimaryKey()); return provider.renderQuery(this, converter, count); } @SuppressWarnings("unchecked") protected void setParameters(EntityManager manager, PreparedStatement stmt) throws SQLException { if (whereParams != null) { final TypeManager typeManager = manager.getProvider().getTypeManager(); for (int i = 0; i < whereParams.length; i++) { if (whereParams[i] == null) { manager.getProvider().putNull(stmt, i + 1); } else { Class javaType = whereParams[i].getClass(); if (whereParams[i] instanceof RawEntity) { javaType = ((RawEntity) whereParams[i]).getEntityType(); } TypeInfo typeInfo = typeManager.getType(javaType); typeInfo.getLogicalType().putToDatabase(manager, stmt, i + 1, whereParams[i], typeInfo.getJdbcWriteType()); } } } } /** * Create a {@link Query} which will select the primary key field of the entity. * * @return non-null Query */ public static Query select() { return select(PRIMARY_KEY_FIELD); } /** * Create a {@link Query} and set the field list in the {@code SELECT} clause. * * @param fields The fields to select, as comma-delimited field names. Spaces are OK. Must not contain {@code "*"}. * @return non-null Query * @throws java.lang.IllegalArgumentException if fields contains {@code "*"} */ public static Query select(String fields) { return new Query(QueryType.SELECT, fields); } /* * @throws java.lang.IllegalArgumentException if fields contains {@code "*"} */ private static void validateSelectFields(String fields) { if (fields == null) { return; } // Assuming we won't have a "legitimate" '*' in a quoted field name if (fields.contains("*")) { throw new IllegalArgumentException("fields must not contain '*' - got '" + fields + "'"); } } public static class FieldMetadata { private final String columnName; private final String alias; private final String aggregateFunction; public FieldMetadata(@Nonnull String columnName, @Nullable String alias, @Nullable String aggregateFunction) { this.columnName = requireNonNull(columnName); this.alias = alias; this.aggregateFunction = aggregateFunction; } public String getColumnName() { return columnName; } public Optional getAlias() { return Optional.ofNullable(alias); } public Optional getAggregateFunction() { return Optional.ofNullable(aggregateFunction); } public String renderField(boolean quoteColumnName, boolean quoteAlias, String quoteCharacter) { final StringBuilder builder = new StringBuilder(); if (aggregateFunction != null) { builder.append(aggregateFunction); builder.append("("); }; if (quoteColumnName) { builder.append(quoteCharacter); } builder.append(Common.shorten(columnName, Integer.MAX_VALUE)); if (quoteColumnName) { builder.append(quoteCharacter); } if (aggregateFunction != null) { builder.append(")"); }; if (alias != null) { builder.append(" as "); if (quoteAlias) { builder.append(quoteCharacter); } builder.append(alias); if (quoteAlias) { builder.append(quoteCharacter); } }; return builder.toString(); } } }