/*****************************************************************
* 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.jdbc.ColumnDescriptor;
import org.apache.cayenne.map.EntityResolver;
import org.apache.cayenne.map.MapLoader;
import org.apache.cayenne.map.Procedure;
import org.apache.cayenne.util.XMLEncoder;
import org.apache.cayenne.util.XMLSerializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A query based on Procedure. Can be used as a select query, or as a query of an
* arbitrary complexity, performing data modification, selecting data (possibly with
* multiple result sets per call), returning values via OUT parameters.
* <h3>Execution with DataContext</h3>
* <h4>Reading OUT parameters</h4>
* <p>
* If a ProcedureQuery has OUT parameters, they are wrapped in a separate List in the
* query result. Such list will contain a single Map with OUT parameter values.
* </p>
* <h4>Using ProcedureQuery as a GenericSelectQuery</h4>
* <p>
* Executing ProcedureQuery via
* {@link org.apache.cayenne.access.DataContext#performQuery(Query)} makes sense only if
* the stored procedure returns a single result set (or alternatively returns a result via
* OUT parameters and no other result sets). It is still OK if data modification occurs as
* a side effect. However if the query returns more then one result set, a more generic
* form should be used:
* {@link org.apache.cayenne.access.DataContext#performGenericQuery(Query)}.
* </p>
*/
public class ProcedureQuery extends AbstractQuery implements ParameterizedQuery, XMLSerializable {
public static final String COLUMN_NAME_CAPITALIZATION_PROPERTY = "cayenne.ProcedureQuery.columnNameCapitalization";
/**
* @since 1.2
*/
protected String resultEntityName;
/**
* @since 1.2
*/
protected Class<?> resultClass;
protected CapsStrategy columnNamesCapitalization;
protected Map<String, Object> parameters = new HashMap<>();
ProcedureQueryMetadata metaData = new ProcedureQueryMetadata();
// TODO: ColumnDescriptor is not XMLSerializable so we can't store it in a DataMap
/**
* @since 1.2
*/
protected List<ColumnDescriptor[]> resultDescriptors;
/**
* Creates an empty procedure query. The query would fetch DataRows. Fetching
* Persistent objects can be achieved either by using
* {@link #ProcedureQuery(String, Class)} constructor or by calling
* {@link #setFetchingDataRows(boolean)} and {@link #setResultEntityName(String)}
* methods.
*/
public ProcedureQuery() {
// for backwards compatibility we go against usual default...
metaData.setFetchingDataRows(true);
}
/**
* Creates a ProcedureQuery based on a Procedure object. The query would fetch
* DataRows. Fetching Persistent objects can be achieved either by using
* {@link #ProcedureQuery(String, Class)} constructor or by calling
* {@link #setFetchingDataRows(boolean)} and {@link #setResultEntityName(String)}
* methods.
*/
public ProcedureQuery(Procedure procedure) {
// for backwards compatibility we go against usual default...
metaData.setFetchingDataRows(true);
setRoot(procedure);
}
/**
* Creates a ProcedureQuery based on a stored procedure. The query would fetch
* DataRows. Fetching Persistent objects can be achieved either by using
* {@link #ProcedureQuery(String, Class)} constructor or by calling
* {@link #setFetchingDataRows(boolean)} and {@link #setResultEntityName(String)}
* methods.
*
* @param procedureName A name of the stored procedure. For this query to work, a
* procedure with this name must be mapped in Cayenne.
*/
public ProcedureQuery(String procedureName) {
// for backwards compatibility we go against usual default...
metaData.setFetchingDataRows(true);
setRoot(procedureName);
}
/**
* @since 1.1
*/
public ProcedureQuery(Procedure procedure, Class<?> resultType) {
setRoot(procedure);
this.resultClass = resultType;
}
/**
* @since 1.1
*/
public ProcedureQuery(String procedureName, Class<?> resultType) {
setRoot(procedureName);
this.resultClass = resultType;
}
/**
* @since 1.2
*/
@Override
public QueryMetadata getMetaData(EntityResolver resolver) {
metaData.resolve(
root,
resultClass != null ? resultClass : resultEntityName,
resolver,
this);
return metaData;
}
/**
* Returns a List of descriptors for query ResultSets in the order they are returned
* by the stored procedure.
* <p>
* <i>Note that if a procedure returns ResultSet in an OUT parameter, it is returned
* prior to any other result sets (though in practice database engines usually support
* only one mechanism for returning result sets. </i>
* </p>
*
* @since 1.2
*/
public List<ColumnDescriptor[]> getResultDescriptors() {
return resultDescriptors != null ? resultDescriptors : Collections.<ColumnDescriptor[]>emptyList();
}
/**
* Adds a descriptor for a single ResultSet. More than one descriptor can be added by
* calling this method multiple times in the order of described ResultSet appearance
* in the procedure results.
*
* @since 1.2
*/
public synchronized void addResultDescriptor(ColumnDescriptor[] descriptor) {
if (resultDescriptors == null) {
resultDescriptors = new ArrayList<>(2);
}
resultDescriptors.add(descriptor);
}
/**
* Removes result descriptor from the list of descriptors.
*
* @since 1.2
*/
public void removeResultDescriptor(ColumnDescriptor[] descriptor) {
if (resultDescriptors != null) {
resultDescriptors.remove(descriptor);
}
}
/**
* Calls "makeProcedure" on the visitor.
*
* @since 1.2
*/
@Override
public SQLAction createSQLAction(SQLActionVisitor visitor) {
return visitor.procedureAction(this);
}
/**
* 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
if (properties == null) {
properties = Collections.emptyMap();
}
Object columnNamesCapitalization = properties
.get(COLUMN_NAME_CAPITALIZATION_PROPERTY);
this.columnNamesCapitalization = (columnNamesCapitalization != null)
? CapsStrategy
.valueOf(columnNamesCapitalization.toString().toUpperCase())
: null;
metaData.initWithProperties(properties);
}
/**
* Prints itself as XML to the provided PrintWriter.
*
* @since 1.1
*/
public void encodeAsXML(XMLEncoder encoder) {
encoder.print("<query name=\"");
encoder.print(getName());
encoder.print("\" factory=\"");
encoder.print("org.apache.cayenne.map.ProcedureQueryBuilder");
encoder.print("\" root=\"");
encoder.print(MapLoader.PROCEDURE_ROOT);
String rootString = null;
if (root instanceof String) {
rootString = root.toString();
}
else if (root instanceof Procedure) {
rootString = ((Procedure) root).getName();
}
if (rootString != null) {
encoder.print("\" root-name=\"");
encoder.print(rootString);
}
if (resultEntityName != null) {
encoder.print("\" result-entity=\"");
encoder.print(resultEntityName);
}
encoder.println("\">");
encoder.indent(1);
metaData.encodeAsXML(encoder);
if (getColumnNamesCapitalization() != CapsStrategy.DEFAULT) {
encoder.printProperty(
COLUMN_NAME_CAPITALIZATION_PROPERTY,
getColumnNamesCapitalization().name());
}
encoder.indent(-1);
encoder.println("</query>");
}
/**
* Creates and returns a new ProcedureQuery built using this query as a prototype and
* substituting template parameters with the values from the map.
*
* @since 1.1
*/
public Query createQuery(Map<String, ?> parameters) {
// create a query replica
ProcedureQuery query = new ProcedureQuery();
if (root != null) {
query.setRoot(root);
}
query.setResultEntityName(resultEntityName);
query.metaData.copyFromInfo(this.metaData);
query.setParameters(parameters);
// TODO: implement algorithm for building the name based on the original name and
// the hashcode of the map of parameters. This way query clone can take advantage
// of caching.
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();
}
/**
* Adds a named parameter to the internal map of parameters.
*
* @since 1.1
*/
public synchronized void addParameter(String name, Object value) {
parameters.put(name, value);
}
/**
* @since 1.1
*/
public synchronized void removeParameter(String name) {
parameters.remove(name);
}
/**
* Returns a map of procedure parameters.
*
* @since 1.1
*/
public Map<String, ?> getParameters() {
return parameters;
}
/**
* Sets a map of parameters.
*
* @since 1.1
*/
public synchronized void setParameters(Map<String, ?> parameters) {
this.parameters.clear();
if (parameters != null) {
this.parameters.putAll(parameters);
}
}
/**
* Cleans up all configured parameters.
*
* @since 1.1
*/
public synchronized void clearParameters() {
this.parameters.clear();
}
/**
* @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);
}
/**
* @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();
}
/**
* @since 1.2
*/
public String getResultEntityName() {
return resultEntityName;
}
/**
* @since 1.2
*/
public void setResultEntityName(String resultEntityName) {
this.resultEntityName = resultEntityName;
}
/**
* @since 3.0
*/
public CapsStrategy getColumnNamesCapitalization() {
return columnNamesCapitalization != null
? columnNamesCapitalization
: CapsStrategy.DEFAULT;
}
/**
* @since 3.0
*/
public void setColumnNamesCapitalization(CapsStrategy columnNameCapitalization) {
this.columnNamesCapitalization = columnNameCapitalization;
}
/**
* 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();
}
}