/**
   * Process the traversal start event for a query {@link org.plasma.query.model.Property property}
   * within an {@link org.plasma.query.model.Expression expression} just traversing the property
   * path if exists and capturing context information for the current {@link
   * org.plasma.query.model.Expression expression}.
   *
   * @see org.plasma.query.visitor.DefaultQueryVisitor#start(org.plasma.query.model.Property)
   */
  @Override
  public void start(Property property) {
    org.plasma.query.model.FunctionValues function = property.getFunction();
    if (function != null)
      throw new GraphFilterException(
          "aggregate functions only supported in subqueries not primary queries");

    Path path = property.getPath();
    PlasmaType targetType = (PlasmaType) this.rootType;
    if (path != null) {
      for (int i = 0; i < path.getPathNodes().size(); i++) {
        AbstractPathElement pathElem = path.getPathNodes().get(i).getPathElement();
        if (pathElem instanceof WildcardPathElement)
          throw new DataAccessException(
              "wildcard path elements applicable for 'Select' clause paths only, not 'Where' clause paths");
        String elem = ((PathElement) pathElem).getValue();
        PlasmaProperty prop = (PlasmaProperty) targetType.getProperty(elem);
        targetType = (PlasmaType) prop.getType(); // traverse
      }
    }
    PlasmaProperty endpointProp = (PlasmaProperty) targetType.getProperty(property.getName());
    this.contextProperty = endpointProp;
    this.contextType = targetType;
    this.contextPropertyPath = property.asPathString();

    super.start(property);
  }
  /**
   * Assembles a data object of the given target type by first forming a query using the given
   * key/property pairs. If an existing data object is mapped for the given key pairs, the existing
   * data object is linked.
   *
   * @param targetType the type for the data object to be assembled
   * @param source the source data object
   * @param sourceProperty the source property
   * @param childKeyPairs the key pairs for the data object to be assembled
   */
  protected void assemble(
      PlasmaType targetType,
      PlasmaDataObject source,
      PlasmaProperty sourceProperty,
      List<PropertyPair> childKeyPairs,
      int level) {
    Set<Property> props = this.collector.getProperties(targetType, level);
    if (props == null) props = EMPTY_PROPERTY_SET;

    if (log.isDebugEnabled())
      log.debug(
          String.valueOf(level)
              + ":assemble: "
              + source.getType().getName()
              + "."
              + sourceProperty.getName()
              + "->"
              + targetType.getName()
              + ": "
              + props);

    List<List<PropertyPair>> result =
        this.getPredicateResult(targetType, sourceProperty, props, childKeyPairs);

    if (log.isDebugEnabled()) log.debug(String.valueOf(level) + ":results: " + result.size());

    Map<PlasmaDataObject, List<PropertyPair>> resultMap =
        this.collectResults(targetType, source, sourceProperty, result);

    // now traverse
    Iterator<PlasmaDataObject> iter = resultMap.keySet().iterator();
    while (iter.hasNext()) {
      PlasmaDataObject target = iter.next();
      List<PropertyPair> row = resultMap.get(target);
      // traverse singular results props
      for (PropertyPair pair : row) {
        if (pair.getProp().isMany() || pair.getProp().getType().isDataType())
          continue; // only singular reference props
        if (!pair.isQueryProperty())
          continue; // property is a key or other property not explicitly cited in the source query,
                    // don't traverse it

        List<PropertyPair> nextKeyPairs = this.getNextKeyPairs(target, pair, level);

        if (log.isDebugEnabled())
          log.debug(
              String.valueOf(level)
                  + ":traverse: ("
                  + pair.getProp().isMany()
                  + ") "
                  + pair.getProp().toString()
                  + ":"
                  + String.valueOf(pair.getValue()));
        assemble(
            (PlasmaType) pair.getProp().getType(), target, pair.getProp(), nextKeyPairs, level + 1);
      }

      // traverse multi props based, not on the results
      // row, but on keys within this data object
      for (Property p : props) {
        PlasmaProperty prop = (PlasmaProperty) p;
        if (!prop.isMany() || prop.getType().isDataType()) continue; // only many reference props

        List<PropertyPair> childKeyProps = this.getChildKeyProps(target, targetType, prop);
        if (log.isDebugEnabled())
          log.debug(
              String.valueOf(level)
                  + ":traverse: ("
                  + prop.isMany()
                  + ") "
                  + prop.toString()
                  + " - "
                  + childKeyProps.toArray().toString());
        assemble((PlasmaType) prop.getType(), target, prop, childKeyProps, level + 1);
      }
    }
  }
  private List<List<PropertyPair>> findResults(
      Query query, PropertySelectionCollector collector, PlasmaType type, Connection con) {
    Object[] params = new Object[0];
    JDBCDataConverter converter = JDBCDataConverter.INSTANCE;

    if (log.isDebugEnabled()) {
      log(query);
    }

    AliasMap aliasMap = new AliasMap(type);

    Map<Type, List<String>> selectMap = collector.getResult();

    // construct a filter adding to alias map
    JDBCFilterAssembler filterAssembler = null;
    Where where = query.findWhereClause();
    if (where != null) {
      filterAssembler = new JDBCFilterAssembler(where, type, aliasMap);
      params = filterAssembler.getParams();
    }

    JDBCOrderingDeclarationAssembler orderingDeclAssembler = null;
    OrderBy orderby = query.findOrderByClause();
    if (orderby != null)
      orderingDeclAssembler = new JDBCOrderingDeclarationAssembler(orderby, type, aliasMap);
    JDBCGroupingDeclarationAssembler groupingDeclAssembler = null;
    GroupBy groupby = query.findGroupByClause();
    if (groupby != null)
      groupingDeclAssembler = new JDBCGroupingDeclarationAssembler(groupby, type, aliasMap);

    String rootAlias = aliasMap.getAlias(type);
    StringBuilder sqlQuery = new StringBuilder();
    sqlQuery.append("SELECT ");

    int i = 0;
    List<String> names = selectMap.get(type);
    for (String name : names) {
      PlasmaProperty prop = (PlasmaProperty) type.getProperty(name);
      if (prop.isMany() && !prop.getType().isDataType()) continue;
      if (i > 0) sqlQuery.append(", ");
      sqlQuery.append(rootAlias);
      sqlQuery.append(".");
      sqlQuery.append(prop.getPhysicalName());
      i++;
    }

    // construct a FROM clause from alias map
    sqlQuery.append(" FROM ");
    Iterator<PlasmaType> it = aliasMap.getTypes();
    int count = 0;
    while (it.hasNext()) {
      PlasmaType aliasType = it.next();
      String alias = aliasMap.getAlias(aliasType);
      if (count > 0) sqlQuery.append(", ");
      sqlQuery.append(aliasType.getPhysicalName());
      sqlQuery.append(" ");
      sqlQuery.append(alias);
      count++;
    }

    // append WHERE filter
    if (filterAssembler != null) {
      sqlQuery.append(" ");
      sqlQuery.append(filterAssembler.getFilter());
    }

    // set the result range
    // FIXME: Oracle specific
    if (query.getStartRange() != null && query.getEndRange() != null) {
      if (where == null) sqlQuery.append(" WHERE ");
      else sqlQuery.append(" AND ");
      sqlQuery.append("ROWNUM >= ");
      sqlQuery.append(String.valueOf(query.getStartRange()));
      sqlQuery.append(" AND ROWNUM <= ");
      sqlQuery.append(String.valueOf(query.getEndRange()));
    }

    if (orderingDeclAssembler != null) {
      sqlQuery.append(" ");
      sqlQuery.append(orderingDeclAssembler.getOrderingDeclaration());
    }

    if (groupingDeclAssembler != null) {
      sqlQuery.append(" ");
      sqlQuery.append(groupingDeclAssembler.getGroupingDeclaration());
    }

    List<List<PropertyPair>> rows = new ArrayList<List<PropertyPair>>();
    PreparedStatement statement = null;
    ResultSet rs = null;
    try {
      statement =
          con.prepareStatement(
              sqlQuery.toString(),
              ResultSet.TYPE_FORWARD_ONLY, /*ResultSet.TYPE_SCROLL_INSENSITIVE,*/
              ResultSet.CONCUR_READ_ONLY);

      // set params
      // note params are pre-converted
      // to string in filter assembly
      // FIXME: are parameters relevant in SQL in this context?
      if (filterAssembler != null) {
        params = filterAssembler.getParams();
        if (params != null)
          for (i = 0; i < params.length; i++) statement.setString(i + 1, String.valueOf(params[i]));
      }

      if (log.isDebugEnabled()) {
        if (params == null || params.length == 0) {
          log.debug("executing: " + sqlQuery.toString());
        } else {
          StringBuilder paramBuf = new StringBuilder();
          paramBuf.append(" [");
          for (int p = 0; p < params.length; p++) {
            if (p > 0) paramBuf.append(", ");
            paramBuf.append(String.valueOf(params[p]));
          }
          paramBuf.append("]");
          log.debug("executing: " + sqlQuery.toString() + " " + paramBuf.toString());
        }
      }

      statement.execute();
      rs = statement.getResultSet();
      int numcols = rs.getMetaData().getColumnCount();
      ResultSetMetaData rsMeta = rs.getMetaData();
      List<PropertyPair> row = null;
      PropertyPair pair = null;
      while (rs.next()) {
        row = new ArrayList<PropertyPair>();
        rows.add(row);
        for (i = 1; i <= numcols; i++) {
          String columnName = rsMeta.getColumnName(i);
          int columnType = rsMeta.getColumnType(i);
          PlasmaProperty prop = (PlasmaProperty) type.getProperty(columnName);
          Object value = converter.fromJDBCDataType(rs, i, columnType, prop);
          pair = new PropertyPair(prop, value);
          pair.setColumn(i);
          row.add(pair);
        }
      }
    } catch (Throwable t) {
      StringBuffer buf = this.generateErrorDetail(t, sqlQuery.toString(), filterAssembler);
      log.error(buf.toString());
      throw new DataAccessException(t);
    } finally {
      try {
        if (rs != null) rs.close();
        if (statement != null) statement.close();
      } catch (SQLException e) {
        log.error(e.getMessage(), e);
      }
    }
    return rows;
  }
  private int countResults(Connection con, Query query, PlasmaType type) {
    int result = 0;
    Object[] params = new Object[0];
    JDBCFilterAssembler filterAssembler = null;

    StringBuilder sqlQuery = new StringBuilder();
    AliasMap aliasMap = new AliasMap(type);

    sqlQuery.append("SELECT COUNT(*)");
    sqlQuery.append(aliasMap.getAlias(type));
    Statement statement = null;
    ResultSet rs = null;

    try {
      Where where = query.findWhereClause();
      if (where != null) {
        filterAssembler = new JDBCFilterAssembler(where, type, aliasMap);
        sqlQuery.append(" ");
        sqlQuery.append(filterAssembler.getFilter());
        params = filterAssembler.getParams();

        if (log.isDebugEnabled()) {
          log.debug("filter: " + filterAssembler.getFilter());
        }
      } else {
        sqlQuery.append(" FROM ");
        sqlQuery.append(type.getPhysicalName());
        sqlQuery.append(" ");
        sqlQuery.append(aliasMap.getAlias(type));
      }
      if (query.getStartRange() != null && query.getEndRange() != null)
        log.warn(
            "query range (start: "
                + query.getStartRange()
                + ", end: "
                + query.getEndRange()
                + ") ignored for count operation");

      if (log.isDebugEnabled()) {
        log.debug("queryString: " + sqlQuery.toString());
        log.debug("executing...");
      }

      statement =
          con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
      statement.execute(sqlQuery.toString());
      rs = statement.getResultSet();
      rs.first();
      result = rs.getInt(1);
    } catch (Throwable t) {
      StringBuffer buf = this.generateErrorDetail(t, sqlQuery.toString(), filterAssembler);
      log.error(buf.toString());
      throw new DataAccessException(t);
    } finally {
      try {
        if (rs != null) rs.close();
        if (statement != null) statement.close();
      } catch (SQLException e) {
        log.error(e.getMessage(), e);
      }
    }
    return result;
  }