content: /*****************************************************************
   *   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
   *
   *    https://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 java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import org.apache.cayenne.access.jdbc.ColumnDescriptor;
  import org.apache.cayenne.map.EntityResolver;
  import org.apache.cayenne.map.Procedure;
  
  /**
   * 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 {
  
      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);
      }
  
      /**
       * 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();
      }
  
      /**
       * @return query timeout
       * @since 4.2
       */
      public int getQueryTimeout() {
          return metaData.getQueryTimeout();
      }
  
      /**
       * Set's query timeout
       * @since 4.2
       */
      public void setQueryTimeout(int timeout) {
          metaData.setQueryTimeout(timeout);
      }
  }
  