/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.cayenne.query;
import org.apache.cayenne.access.QueryEngine;
import org.apache.cayenne.map.DataMap;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.MapLoader;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.Procedure;
import org.apache.cayenne.map.SQLResult;
import org.apache.cayenne.util.XMLEncoder;
import org.apache.cayenne.util.XMLSerializable;
import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.collections.Transformer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
/**
* A query that executes unchanged (except for template preprocessing) "raw" SQL
* specified by the user. <h3>Template Script</h3>
* <p>
* SQLTemplate stores a dynamic template for the SQL query that supports
* parameters and customization using Velocity scripting language. The most
* straightforward use of scripting abilities is to build parameterized queries.
* For example:
* </p>
*
* <pre>
* SELECT ID, NAME FROM SOME_TABLE WHERE NAME LIKE $a
* </pre>
* <p>
* <i>For advanced scripting options see "Scripting SQLTemplate" chapter in the
* User Guide. </i>
* </p>
* <h3>Per-Database Template Customization</h3>
* <p>
* SQLTemplate has a {@link #getDefaultTemplate() default template script}, but
* also it allows to configure multiple templates and switch them dynamically.
* This way a single query can have multiple "dialects" specific to a given
* database.
* </p>
*
* @since 1.1
*/
public class SQLTemplate extends AbstractQuery implements ParameterizedQuery, XMLSerializable {
private static final long serialVersionUID = -3073521388289663641L;
public static final String COLUMN_NAME_CAPITALIZATION_PROPERTY = "cayenne.SQLTemplate.columnNameCapitalization";
private static final Transformer nullMapTransformer = new Transformer() {
public Object transform(Object input) {
return (input != null) ? input : Collections.EMPTY_MAP;
}
};
protected String defaultTemplate;
protected Map<String, String> templates;
protected Map<String, ?>[] parameters;
protected List<Object> positionalParams;
protected CapsStrategy columnNamesCapitalization;
protected SQLResult result;
private String dataNodeName;
SQLTemplateMetadata metaData = new SQLTemplateMetadata();
/**
* Creates an empty SQLTemplate. Note this constructor does not specify the
* "root" of the query, so a user must call "setRoot" later to make sure
* SQLTemplate can be executed.
*
* @since 1.2
*/
public SQLTemplate() {
}
/**
* Creates a SQLTemplate without an explicit root.
*
* @since 4.0
*/
public SQLTemplate(String defaultTemplate, boolean isFetchingDataRows) {
setDefaultTemplate(defaultTemplate);
setRoot(null);
setFetchingDataRows(isFetchingDataRows);
}
@Override
public void setRoot(Object value) {
// allow null root...
if (value == null) {
this.root = value;
} else {
super.setRoot(value);
}
}
@Override
public void route(QueryRouter router, EntityResolver resolver, Query substitutedQuery) {
DataMap map = getMetaData(resolver).getDataMap();
QueryEngine engine;
if (map != null) {
engine = router.engineForDataMap(map);
} else {
engine = router.engineForName(getDataNodeName());
}
router.route(engine, this, substitutedQuery);
}
/**
* @since 3.1
*/
public SQLTemplate(DataMap rootMap, String defaultTemplate, boolean isFetchingDataRows) {
setDefaultTemplate(defaultTemplate);
setRoot(rootMap);
setFetchingDataRows(isFetchingDataRows);
}
/**
* @since 1.2
*/
public SQLTemplate(ObjEntity rootEntity, String defaultTemplate) {
setDefaultTemplate(defaultTemplate);
setRoot(rootEntity);
}
/**
* @since 1.2
*/
public SQLTemplate(Class<?> rootClass, String defaultTemplate) {
setDefaultTemplate(defaultTemplate);
setRoot(rootClass);
}
/**
* @since 1.2
*/
public SQLTemplate(DbEntity rootEntity, String defaultTemplate) {
setDefaultTemplate(defaultTemplate);
setRoot(rootEntity);
}
/**
* @since 1.2
*/
public SQLTemplate(String objEntityName, String defaultTemplate) {
setRoot(objEntityName);
setDefaultTemplate(defaultTemplate);
}
/**
* @since 1.2
*/
@Override
public QueryMetadata getMetaData(EntityResolver resolver) {
metaData.resolve(root, resolver, this);
return metaData;
}
/**
* Calls <em>sqlAction(this)</em> on the visitor.
*
* @since 1.2
*/
@Override
public SQLAction createSQLAction(SQLActionVisitor visitor) {
return visitor.sqlAction(this);
}
/**
* Prints itself as XML to the provided PrintWriter.
*
* @since 1.1
*/
@Override
public void encodeAsXML(XMLEncoder encoder) {
encoder.print("<query name=\"");
encoder.print(getName());
encoder.print("\" factory=\"");
encoder.print("org.apache.cayenne.map.SQLTemplateBuilder");
String rootString = null;
String rootType = null;
if (root instanceof String) {
rootType = MapLoader.OBJ_ENTITY_ROOT;
rootString = root.toString();
} else if (root instanceof ObjEntity) {
rootType = MapLoader.OBJ_ENTITY_ROOT;
rootString = ((ObjEntity) root).getName();
} else if (root instanceof DbEntity) {
rootType = MapLoader.DB_ENTITY_ROOT;
rootString = ((DbEntity) root).getName();
} else if (root instanceof Procedure) {
rootType = MapLoader.PROCEDURE_ROOT;
rootString = ((Procedure) root).getName();
} else if (root instanceof Class<?>) {
rootType = MapLoader.JAVA_CLASS_ROOT;
rootString = ((Class<?>) root).getName();
} else if (root instanceof DataMap) {
rootType = MapLoader.DATA_MAP_ROOT;
rootString = ((DataMap) root).getName();
}
if (rootType != null) {
encoder.print("\" root=\"");
encoder.print(rootType);
encoder.print("\" root-name=\"");
encoder.print(rootString);
}
encoder.println("\">");
encoder.indent(1);
metaData.encodeAsXML(encoder);
if (getColumnNamesCapitalization() != CapsStrategy.DEFAULT) {
encoder.printProperty(COLUMN_NAME_CAPITALIZATION_PROPERTY, getColumnNamesCapitalization().name());
}
// encode default SQL
if (defaultTemplate != null) {
encoder.print("<sql><![CDATA[");
encoder.print(defaultTemplate);
encoder.println("]]></sql>");
}
// encode adapter SQL
if (templates != null && !templates.isEmpty()) {
// sorting entries by adapter name
TreeSet<String> keys = new TreeSet<String>(templates.keySet());
for (String key : keys) {
String value = templates.get(key);
if (key != null && value != null) {
String sql = value.trim();
if (sql.length() > 0) {
encoder.print("<sql adapter-class=\"");
encoder.print(key);
encoder.print("\"><![CDATA[");
encoder.print(sql);
encoder.println("]]></sql>");
}
}
}
}
// TODO: support parameter encoding
encoder.indent(-1);
encoder.println("</query>");
}
/**
* Initializes query parameters using a set of properties.
*
* @since 1.1
*/
public void initWithProperties(Map<String, ?> properties) {
// must init defaults even if properties are empty
metaData.initWithProperties(properties);
if (properties == null) {
properties = Collections.emptyMap();
}
Object columnNamesCapitalization = properties.get(COLUMN_NAME_CAPITALIZATION_PROPERTY);
this.columnNamesCapitalization = (columnNamesCapitalization != null) ? CapsStrategy
.valueOf(columnNamesCapitalization.toString().toUpperCase()) : null;
}
/**
* Returns an iterator over parameter sets. Each element returned from the
* iterator is a java.util.Map.
*/
@SuppressWarnings("unchecked")
public Iterator<Map<String, ?>> parametersIterator() {
return (parameters == null || parameters.length == 0) ? IteratorUtils.emptyIterator() : IteratorUtils
.transformedIterator(IteratorUtils.arrayIterator(parameters), nullMapTransformer);
}
/**
* Returns the number of parameter sets.
*/
public int parametersSize() {
return (parameters != null) ? parameters.length : 0;
}
/**
* Initializes named parameter of this query. Note that calling this method
* will reset any positional parameters.
*
* @since 4.0
*/
@SuppressWarnings("unchecked")
public void setParams(Map<String, ?> params) {
// since named parameters are specified, resetting positional
// parameters
this.positionalParams = null;
setParameters(params);
}
/**
* Initializes positional parameters of the query. Parameters are bound in
* the order they are found in the SQL template. If a given parameter name
* is used more than once, only the first occurrence is treated as
* "position", subsequent occurrences are bound with the same value as the
* first one. If template parameters count is different from the array
* parameter count, an exception will be thrown.
* <p>
* Note that calling this method will reset any previously set *named*
* parameters.
*
* @since 4.0
*/
public void setParamsArray(Object... params) {
setParamsList(params != null ? Arrays.asList(params) : null);
}
/**
* Initializes positional parameters of the query. Parameters are bound in
* the order they are found in the SQL template. If a given parameter name
* is used more than once, only the first occurrence is treated as
* "position", subsequent occurrences are bound with the same value as the
* first one. If template parameters count is different from the list
* parameter count, an exception will be thrown.
* <p>
* Note that calling this method will reset any previously set *named*
* parameters.
*
* @since 4.0
*/
public void setParamsList(List<Object> params) {
// since positional parameters are specified, resetting named
// parameters
this.parameters = null;
this.positionalParams = params != null ? new ArrayList<>(params) : null;
}
/**
* Returns a new query built using this query as a prototype and a new set
* of parameters.
*/
public SQLTemplate queryWithParameters(Map<String, ?>... parameters) {
// create a query replica
SQLTemplate query = new SQLTemplate();
query.setRoot(root);
query.setDefaultTemplate(getDefaultTemplate());
if (templates != null) {
query.templates = new HashMap<>(templates);
}
query.metaData.copyFromInfo(this.metaData);
query.setParameters(parameters);
query.setColumnNamesCapitalization(this.getColumnNamesCapitalization());
return query;
}
/**
* Creates and returns a new SQLTemplate built using this query as a
* prototype and substituting template parameters with the values from the
* map.
*
* @since 1.1
*/
@Override
public Query createQuery(Map<String, ?> parameters) {
// create a query replica
SQLTemplate query = new SQLTemplate();
query.setRoot(root);
query.setDefaultTemplate(getDefaultTemplate());
if (templates != null) {
query.templates = new HashMap<>(templates);
}
query.metaData.copyFromInfo(this.metaData);
query.setParams(parameters);
query.setColumnNamesCapitalization(this.getColumnNamesCapitalization());
return query;
}
@Override
protected BaseQueryMetadata getBaseMetaData() {
return metaData;
}
public int getFetchLimit() {
return metaData.getFetchLimit();
}
public void setFetchLimit(int fetchLimit) {
this.metaData.setFetchLimit(fetchLimit);
}
/**
* @since 3.0
*/
public int getFetchOffset() {
return metaData.getFetchOffset();
}
/**
* @since 3.0
*/
public void setFetchOffset(int fetchOffset) {
metaData.setFetchOffset(fetchOffset);
}
public int getPageSize() {
return metaData.getPageSize();
}
public void setPageSize(int pageSize) {
metaData.setPageSize(pageSize);
}
public void setFetchingDataRows(boolean flag) {
metaData.setFetchingDataRows(flag);
}
public boolean isFetchingDataRows() {
return metaData.isFetchingDataRows();
}
/**
* Returns default SQL template for this query.
*/
public String getDefaultTemplate() {
return defaultTemplate;
}
/**
* Sets default SQL template for this query.
*/
public void setDefaultTemplate(String string) {
defaultTemplate = string;
}
/**
* Returns a template for key, or a default template if a template for key
* is not found.
*/
public synchronized String getTemplate(String key) {
if (templates == null) {
return defaultTemplate;
}
String template = templates.get(key);
return (template != null) ? template : defaultTemplate;
}
/**
* Returns template for key, or null if there is no template configured for
* this key. Unlike {@link #getTemplate(String)}this method does not return
* a default template as a failover strategy, rather it returns null.
*/
public synchronized String getCustomTemplate(String key) {
return (templates != null) ? templates.get(key) : null;
}
/**
* Adds a SQL template string for a given key. Note the the keys understood
* by Cayenne must be fully qualified adapter class names. This way the
* framework can related current DataNode to the right template. E.g.
* "org.apache.cayenne.dba.oracle.OracleAdapter" is a key that should be
* used to setup an Oracle-specific template.
*
* @see #setDefaultTemplate(String)
*/
public synchronized void setTemplate(String key, String template) {
if (templates == null) {
templates = new HashMap<>();
}
templates.put(key, template);
}
public synchronized void removeTemplate(String key) {
if (templates != null) {
templates.remove(key);
}
}
/**
* Returns a collection of configured template keys.
*/
public synchronized Collection<String> getTemplateKeys() {
return (templates != null) ? templates.keySet() : Collections.<String> emptyList();
}
/**
* Returns a map of named parameters that will be bound to SQL.
*
* @since 4.0
*/
public Map<String, ?> getParams() {
Map<String, ?> map = (parameters != null && parameters.length > 0) ? parameters[0] : null;
return (map != null) ? map : Collections.<String, Object> emptyMap();
}
/**
* Returns a list of positional parameters that will be bound to SQL.
*
* @since 4.0
*/
public List<Object> getPositionalParams() {
return positionalParams != null ? positionalParams : Collections.emptyList();
}
/**
* Utility method to get the first set of parameters, since most queries
* will only have one.
*/
public Map<String, ?> getParameters() {
return getParams();
}
/**
* Utility method to initialize query with one or more sets of parameters.
*/
@SuppressWarnings("unchecked")
public void setParameters(Map<String, ?>... parameters) {
if (parameters == null) {
this.parameters = null;
} else {
// clone parameters to ensure that we don't have immutable maps that
// are not serializable with Hessian...
this.parameters = new Map[parameters.length];
for (int i = 0; i < parameters.length; i++) {
this.parameters[i] = parameters[i] != null ? new HashMap<>(parameters[i]) : new HashMap<String, Object>();
}
}
}
/**
* @since 1.2
*/
public PrefetchTreeNode getPrefetchTree() {
return metaData.getPrefetchTree();
}
/**
* Adds a prefetch.
*
* @since 1.2
*/
public PrefetchTreeNode addPrefetch(String prefetchPath) {
// by default use JOINT_PREFETCH_SEMANTICS
return metaData.addPrefetch(prefetchPath, PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
}
/**
* Adds a prefetch with specified relationship path to the query.
*
* @since 4.0
*/
public void addPrefetch(PrefetchTreeNode prefetchElement) {
metaData.mergePrefetch(prefetchElement);
}
/**
* @since 1.2
*/
public void removePrefetch(String prefetch) {
metaData.removePrefetch(prefetch);
}
/**
* Adds all prefetches from a provided collection.
*
* @since 1.2
*/
public void addPrefetches(Collection<String> prefetches) {
metaData.addPrefetches(prefetches, PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
}
/**
* Clears all prefetches.
*
* @since 1.2
*/
public void clearPrefetches() {
metaData.clearPrefetches();
}
/**
* Returns a column name capitalization policy applied to selecting queries.
* This is used to simplify mapping of the queries like "SELECT * FROM ...",
* ensuring that a chosen Cayenne column mapping strategy (e.g. all column
* names in uppercase) is portable across database engines that can have
* varying default capitalization. Default (null) value indicates that
* column names provided in result set are used unchanged.
*
* @since 3.0
*/
public CapsStrategy getColumnNamesCapitalization() {
return columnNamesCapitalization != null ? columnNamesCapitalization : CapsStrategy.DEFAULT;
}
/**
* Sets a column name capitalization policy applied to selecting queries.
* This is used to simplify mapping of the queries like "SELECT * FROM ...",
* ensuring that a chosen Cayenne column mapping strategy (e.g. all column
* names in uppercase) is portable across database engines that can have
* varying default capitalization. Default (null) value indicates that
* column names provided in result set are used unchanged.
* <p>
* Note that while a non-default setting is useful for queries that do not
* rely on a #result directive to describe columns, it works for all
* SQLTemplates the same way.
*
* @since 3.0
*/
public void setColumnNamesCapitalization(CapsStrategy columnNameCapitalization) {
this.columnNamesCapitalization = columnNameCapitalization;
}
/**
* Sets an optional explicit mapping of the result set. If result set
* mapping is specified, the result of SQLTemplate may not be a normal list
* of Persistent objects or DataRows, instead it will follow the
* {@link SQLResult} rules.
*
* @since 3.0
*/
public void setResult(SQLResult resultSet) {
this.result = resultSet;
}
/**
* @since 3.0
*/
public SQLResult getResult() {
return result;
}
/**
* Sets statement's fetch size (0 for no default size)
*
* @since 3.0
*/
public void setStatementFetchSize(int size) {
metaData.setStatementFetchSize(size);
}
/**
* @return statement's fetch size
* @since 3.0
*/
public int getStatementFetchSize() {
return metaData.getStatementFetchSize();
}
/**
* Returns a name of the DataNode to use with this SQLTemplate. This
* information will be used during query execution if no other routing
* information is provided such as entity name or class, etc.
*
* @since 4.0
*/
public String getDataNodeName() {
return dataNodeName;
}
/**
* Sets a name of the DataNode to use with this SQLTemplate. This
* information will be used during query execution if no other routing
* information is provided such as entity name or class, etc.
*
* @since 4.0
*/
public void setDataNodeName(String dataNodeName) {
this.dataNodeName = dataNodeName;
}
}