protected function generateStatementFromPropertiesAndTypesLists(
        array $selectPropertyIds,
        array $fromTypes,
        $whereClause,
        array $orderByPropertyIds
    ) {
        $statementString = 'SELECT ' . $this->generateStatementPropertyList($selectPropertyIds, false);

        $primaryTable = array_shift($fromTypes);
        list ($primaryTableQueryName, $primaryAlias) = $this->getQueryNameAndAliasForType($primaryTable, 'primary');

        $statementString .= ' FROM ' . $primaryTableQueryName . ' ' . $primaryAlias;

        while (count($fromTypes) > 0) {
            $secondaryTable = array_shift($fromTypes);
            /*
             * we build an automatic alias here, a simple one-byte ASCII value
             * generated based on remaining tables. If 26 tables remain, a "z"
             * is generated. If 1 table remains, an "a" is generated. The alias
             * is, unfortunately, required for the JOIN to work correctly. It
             * only gets used if the type string does not contain an alias.
             */
            $alias = chr(97 + count($fromTypes));
            list ($secondaryTableQueryName, $alias) = $this->getQueryNameAndAliasForType($secondaryTable, $alias);
            $statementString .= ' JOIN ' . $secondaryTableQueryName . ' AS ' . $alias .
                ' ON ' . $primaryAlias . '.cmis:objectId = ' . $alias . '.cmis:objectId';
        }

        if (trim((string) $whereClause)) {
            $statementString .= ' WHERE ' . trim($whereClause);
        }

        if (!empty($orderByPropertyIds)) {
            $statementString .= ' ORDER BY ' . $this->generateStatementPropertyList($orderByPropertyIds, true);
        }
        return trim($statementString);
    }