org.artifactory.storage.db.search.model.VfsQueryDbImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.artifactory.storage.db.search.model.VfsQueryDbImpl.java

Source

/*
 * Artifactory is a binaries repository manager.
 * Copyright (C) 2012 JFrog Ltd.
 *
 * Artifactory is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Artifactory is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Artifactory.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.artifactory.storage.db.search.model;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.lang.StringUtils;
import org.artifactory.sapi.search.InvalidQueryRuntimeException;
import org.artifactory.sapi.search.VfsBoolType;
import org.artifactory.sapi.search.VfsComparatorType;
import org.artifactory.sapi.search.VfsFunctionType;
import org.artifactory.sapi.search.VfsQuery;
import org.artifactory.sapi.search.VfsQueryResult;
import org.artifactory.sapi.search.VfsQueryResultType;
import org.artifactory.sapi.search.VfsQueryRow;
import org.artifactory.storage.StorageException;
import org.artifactory.storage.db.DbService;
import org.artifactory.storage.db.DbType;
import org.artifactory.storage.db.fs.entity.NodePath;
import org.artifactory.storage.db.util.BaseDao;
import org.artifactory.storage.db.util.DbUtils;
import org.artifactory.storage.db.util.JdbcHelper;
import org.artifactory.storage.spring.StorageContextHelper;
import org.artifactory.util.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Collection;
import java.util.EmptyStackException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.Stack;

/**
 * Date: 8/5/11
 * Time: 6:07 PM
 *
 * @author Fred Simon
 */
public class VfsQueryDbImpl implements VfsQuery {
    private static final Logger log = LoggerFactory.getLogger(VfsQueryDbImpl.class);

    private VfsQueryResultType expectedResultType = VfsQueryResultType.ANY_ITEM;
    private final Set<String> repoKeys = Sets.newHashSet();
    private final BaseGroupCriterion defaultGroup;
    protected final List<VfsQueryPathCriterionDbImpl> pathCriteria = Lists.newArrayList();
    private final List<OrderBy> orders = Lists.newArrayList();

    private Stack<BaseGroupCriterion> groups = new Stack<>();
    private VfsQueryCriterionDbImpl currentCriteria = null;

    public VfsQueryDbImpl() {
        defaultGroup = new BaseGroupCriterion();
    }

    @Override
    public VfsQuery expectedResult(@Nonnull VfsQueryResultType itemType) {
        expectedResultType = itemType;
        return this;
    }

    @Override
    public VfsQuery setSingleRepoKey(String repoKey) {
        this.repoKeys.clear();
        this.repoKeys.add(repoKey);
        return this;
    }

    @Override
    public VfsQuery setRepoKeys(Collection<String> repoKeys) {
        if (repoKeys != null) {
            this.repoKeys.clear();
            this.repoKeys.addAll(repoKeys);
        }
        return this;
    }

    @Override
    public VfsQuery orderByAscending(@Nonnull String propertyName) {
        orders.add(new OrderBy(propertyName, true));
        return this;
    }

    @Override
    public VfsQuery orderByDescending(@Nonnull String propertyName) {
        orders.add(new OrderBy(propertyName, false));
        return this;
    }

    @Override
    public VfsQuery name(@Nonnull String nodeName) {
        return prop("node_name").val(nodeName);
    }

    @Override
    public VfsQuery archiveName(@Nonnull String entryName) {
        return prop("entry_name").val(entryName);
    }

    @Override
    public VfsQuery archivePath(@Nonnull String entryPath) {
        return prop("entry_path").val(entryPath);
    }

    @Override
    public VfsQuery prop(@Nonnull String propertyName) {
        addCurrentCriteriaIfNeeded();
        currentCriteria = new VfsQueryCriterionDbImpl(propertyName);
        return this;
    }

    @Override
    public VfsQuery comp(@Nonnull VfsComparatorType comparator) {
        if (currentCriteria == null) {
            throw new IllegalStateException("Cannot add comparator to non existing property!");
        }
        currentCriteria.setComparator(comparator);
        return this;
    }

    @Override
    public VfsQuery func(@Nonnull VfsFunctionType function) {
        if (currentCriteria == null) {
            throw new IllegalStateException("Cannot add function to non existing property!");
        }
        currentCriteria.setFunction(function);
        return this;
    }

    @Override
    public VfsQuery val(String... values) {
        if (currentCriteria == null) {
            throw new IllegalStateException("Cannot add filter value to non existing property!");
        }
        currentCriteria.setValue(values);
        return this;
    }

    @Override
    public VfsQuery val(@Nonnull Long value) {
        if (currentCriteria == null) {
            throw new IllegalStateException("Cannot add filter value to non existing property!");
        }
        currentCriteria.setValue(value);
        return this;
    }

    @Override
    public VfsQuery val(@Nonnull Calendar value) {
        if (currentCriteria == null) {
            throw new IllegalStateException("Cannot add filter value to non existing property!");
        }
        currentCriteria.setValue(value);
        return this;
    }

    @Override
    public VfsQuery nextBool(@Nonnull VfsBoolType bool) {
        if (currentCriteria == null) {
            throw new IllegalStateException("Cannot add next bool to non existing property!");
        }
        currentCriteria.nextBool = bool;
        internalAddCriterion(currentCriteria);
        currentCriteria = null;
        return this;
    }

    @Override
    public VfsQuery startGroup() {
        addCurrentCriteriaIfNeeded();
        BaseGroupCriterion newGroup = new BaseGroupCriterion();
        internalAddCriterion(newGroup);
        groups.push(newGroup);
        return this;
    }

    private void addCurrentCriteriaIfNeeded() {
        if (currentCriteria != null) {
            internalAddCriterion(currentCriteria);
            currentCriteria = null;
        }
    }

    @Override
    public VfsQuery endGroup() {
        return endGroup(null);
    }

    @Override
    public VfsQuery endGroup(@Nullable VfsBoolType bool) {
        try {
            BaseGroupCriterion groupCriterion = groups.pop();
            if (currentCriteria != null) {
                groupCriterion.addCriterion(currentCriteria);
                currentCriteria = null;
            }
            if (bool != null) {
                groupCriterion.nextBool = bool;
            }
            return this;
        } catch (EmptyStackException e) {
            throw new IllegalStateException("Cannot end group that did not start!", e);
        }
    }

    private void internalAddCriterion(BaseVfsQueryCriterion criterion) {
        if (!groups.isEmpty()) {
            groups.peek().addCriterion(criterion);
        } else {
            defaultGroup.addCriterion(criterion);
        }
    }

    @Override
    @Nonnull
    public VfsQueryResult execute(int limit) {
        if (!groups.isEmpty()) {
            throw new IllegalStateException("Cannot execute while group still active!");
        }
        addCurrentCriteriaIfNeeded();
        DbSqlQueryBuilder query = new DbSqlQueryBuilder();
        fillSelectBase(query);
        fillExpectedNodeType(query);
        fillRepoFilter(query);
        fillPathCriterion(query);
        fillLimitCriterion(query, limit);
        fillCriteria(query);
        fillOrderBy(query);

        return executeSelect(query, limit);
    }

    private VfsQueryResult executeSelect(DbSqlQueryBuilder query, int limit) {
        if (limit != Integer.MAX_VALUE) {
            DbService dbService = StorageContextHelper.get().beanForType(DbService.class);
            DbType databaseType = dbService.getDatabaseType();
            boolean supportsLimitSyntax = databaseType == DbType.MYSQL;
            if (supportsLimitSyntax) {
                query.append(" LIMIT ").append(String.valueOf(limit));
            }
        }

        ResultSet rs = null;
        try {
            log.debug("Executing search query: {}", query);
            List<Object> params = query.params;
            JdbcHelper jdbcHelper = StorageContextHelper.get().beanForType(JdbcHelper.class);
            rs = jdbcHelper.executeSelect(query.sqlQuery.toString(),
                    (Object[]) params.toArray(new Object[params.size()]));
            LinkedHashMap<NodePath, VfsQueryRow> map = Maps.newLinkedHashMap();
            int nbLines = 0;
            while (rs.next()) {
                if (nbLines >= limit) {
                    break;
                }
                nbLines++;
                int pos = 1;
                long nodeId = rs.getLong(pos++);
                boolean file = rs.getBoolean(pos++);
                NodePath nodePath = new NodePath(rs.getString(pos++), BaseDao.emptyIfNullOrDot(rs.getString(pos++)),
                        BaseDao.emptyIfNullOrDot(rs.getString(pos++)), file);
                VfsQueryRow row = map.get(nodePath);
                if (row == null) {
                    if (hasArchiveEntries()) {
                        row = new VfsQueryRowDbImpl(nodeId, file, nodePath, rs.getString(pos++),
                                rs.getString(pos++));
                    } else {
                        row = new VfsQueryRowDbImpl(nodeId, file, nodePath);
                    }
                    map.put(nodePath, row);
                } else {
                    if (hasArchiveEntries()) {
                        ((VfsQueryRowDbImpl) row).addArchiveEntry(rs.getString(pos++), rs.getString(pos++));
                    } else {
                        log.warn("Got multiple times the same node " + nodePath + " in query " + query);
                    }
                }
                log.debug("Used {} params", pos);
            }
            if (nbLines >= limit) {
                nbLines = -1;
            }
            return new VfsQueryResultDbImpl(map.values(), nbLines);
        } catch (SQLException e) {
            throw new StorageException("Could not execute query '" + query + "' due to:" + e.getMessage(), e);
        } finally {
            DbUtils.close(rs);
        }
    }

    protected void fillSelectBase(DbSqlQueryBuilder query) {
        query.append("SELECT nodes.node_id, nodes.node_type, nodes.repo, nodes.node_path, nodes.node_name");
        if (hasArchiveEntries()) {
            query.append(", archive_paths.entry_path, archive_names.entry_name");
        }
        query.append(" FROM nodes");
        if (hasArchiveEntries()) {
            query.append(", indexed_archives, indexed_archives_entries, archive_paths, archive_names");
        }
        if (defaultGroup.hasPropertyFilter()) {
            query.append(", node_props");
        }
        if (defaultGroup.hasStatisticFilter()) {
            query.append(" LEFT JOIN stats ON stats.node_id = nodes.node_id");
            query.append(" LEFT JOIN stats_remote ON stats_remote.node_id = nodes.node_id");
        }
        query.append(" WHERE ");
        if (defaultGroup.hasPropertyFilter()) {
            query.addNextBoolIfNeeded();
            query.append("node_props.node_id = nodes.node_id");
            query.nextBool = VfsBoolType.AND;
        }
        if (hasArchiveEntries()) {
            query.addNextBoolIfNeeded();
            query.append("indexed_archives.archive_sha1 = nodes.sha1_actual");
            query.nextBool = VfsBoolType.AND;
            query.addNextBoolIfNeeded();
            query.append("indexed_archives.indexed_archives_id = indexed_archives_entries.indexed_archives_id");
            query.nextBool = VfsBoolType.AND;
            query.addNextBoolIfNeeded();
            query.append("indexed_archives_entries.entry_path_id = archive_paths.path_id");
            query.nextBool = VfsBoolType.AND;
            query.addNextBoolIfNeeded();
            query.append("indexed_archives_entries.entry_name_id = archive_names.name_id");
            query.nextBool = VfsBoolType.AND;
        }
    }

    private boolean hasArchiveEntries() {
        return expectedResultType == VfsQueryResultType.ARCHIVE_ENTRY;
    }

    protected void fillExpectedNodeType(DbSqlQueryBuilder query) {
        switch (expectedResultType) {
        case ANY_ITEM:
            // No filter here
            break;
        case FILE:
        case ARCHIVE_ENTRY:
            query.addNextBoolIfNeeded();
            query.append("nodes.node_type=1");
            query.nextBool = VfsBoolType.AND;
            break;
        case FOLDER:
            query.addNextBoolIfNeeded();
            query.append("nodes.node_type=0");
            query.nextBool = VfsBoolType.AND;
            break;
        default:
            throw new IllegalStateException("Query node type " + expectedResultType + " not supported yet!");
        }
    }

    protected void fillRepoFilter(DbSqlQueryBuilder query) {
        if (!repoKeys.isEmpty()) {
            query.addNextBoolIfNeeded();
            query.append(" nodes.repo ");
            if (repoKeys.size() == 1) {
                // Simple path
                String singleRepoKey = repoKeys.iterator().next();
                if (!StringUtils.isBlank(singleRepoKey)) {
                    query.append(" = ");
                    query.addParam(singleRepoKey);
                } else {
                    throw new InvalidQueryRuntimeException("Cannot accept null or empty repo key!");
                }
            } else {
                query.append(' ').append(VfsComparatorType.IN.str).append(' ');
                query.addListParam(repoKeys);
            }
            query.nextBool = VfsBoolType.AND;
        }
    }

    protected void fillCriteria(DbSqlQueryBuilder query) {
        defaultGroup.fill(query);
    }

    protected void fillOrderBy(DbSqlQueryBuilder query) {
        for (OrderBy order : orders) {
            query.append("ORDER BY ").append(order.propertyName);
            if (order.ascending) {
                query.append(" ASC");
            } else {
                query.append(" DESC");
            }
        }
    }

    @Override
    public VfsQuery addPathFilters(String... pathFilters) {
        for (String pathFilter : pathFilters) {
            if (StringUtils.isBlank(pathFilter)) {
                internalAddPathCriterion(createSmartPathCriterion("*"));
            } else {
                internalAddPathCriterion(createSmartPathCriterion(pathFilter));
            }
        }
        return this;
    }

    @Override
    public VfsQuery addPathFilter(String relativePathFilter) {
        // We can have double // in this path,
        // Be careful when we split with slash // will return an empty string
        // IMPORTANT NOTE... Big bug in spilt, splitting "//g1*//" return {"", "", "g1*"} instead of {"", "g1*", ""}
        String[] split = relativePathFilter.split(DbQueryHelper.FORWARD_SLASH);
        for (String path : split) {
            internalAddPathCriterion(createSmartPathCriterion(path));
        }
        return this;
    }

    private VfsQueryPathCriterionDbImpl createSmartPathCriterion(String pathFilter) {
        if (pathFilter == null || pathFilter.contains(DbQueryHelper.FORWARD_SLASH)) {
            throw new InvalidQueryRuntimeException(
                    "Path filter element cannot be null or contain slash: " + pathFilter);
        }
        if (pathFilter.length() == 0 || ALL_PATH_VALUE.equals(pathFilter)) {
            return new VfsQueryPathCriterionDbImpl(VfsComparatorType.ANY, ALL_PATH_VALUE);
        } else {
            if (DbQueryHelper.hasWildcards(pathFilter)) {
                return new VfsQueryPathCriterionDbImpl(VfsComparatorType.CONTAINS, pathFilter);
            } else {
                return new VfsQueryPathCriterionDbImpl(VfsComparatorType.EQUAL, pathFilter);
            }
        }
    }

    private void internalAddPathCriterion(VfsQueryPathCriterionDbImpl criterion) {
        if (criterion != null && criterion.isValid()) {
            pathCriteria.add(criterion);
        }
    }

    protected void fillPathCriterion(DbSqlQueryBuilder query) {
        if (pathCriteria.isEmpty()) {
            return;
        }

        VfsQueryPathCriterionDbImpl mainCriterion;
        // Count the path depth that the depth will be equal or greater than if some all path value appears
        int fixedDepth;
        boolean hasAllPathValue = false;
        if (pathCriteria.size() == 1) {
            mainCriterion = pathCriteria.get(0);
            String pathValue = (String) mainCriterion.value;
            VfsComparatorType comp = mainCriterion.getComparator();
            if (comp == VfsComparatorType.ANY || ALL_PATH_VALUE.equals(pathValue)) {
                // Basically no path filter only one and full path
                return;
            }

            // If not a single star needs at least one folder level
            if (!"*".equals(pathValue)) {
                fixedDepth = 1;
            } else {
                // Can be 0 or 1
                fixedDepth = 0;
            }
            if (comp == VfsComparatorType.CONTAINS) {
                StringBuilder builder = new StringBuilder();
                DbQueryHelper.addPathValue(builder, pathValue);
                pathValue = builder.toString();
            }
            mainCriterion.value = pathValue;
        } else {
            fixedDepth = pathCriteria.size();
            boolean hasContains = false;
            for (VfsQueryPathCriterionDbImpl criterion : pathCriteria) {
                VfsComparatorType comp = criterion.getComparator();
                if (comp == VfsComparatorType.CONTAINS || comp == VfsComparatorType.ANY) {
                    hasContains = true;
                    if (!hasAllPathValue && ((String) criterion.value).contains(ALL_PATH_VALUE)) {
                        hasAllPathValue = true;
                    }
                }
            }
            if (hasContains) {
                mainCriterion = new VfsQueryPathCriterionDbImpl(VfsComparatorType.CONTAINS, "");
            } else {
                mainCriterion = new VfsQueryPathCriterionDbImpl(VfsComparatorType.EQUAL, "");
            }
            StringBuilder pathFilterValue = new StringBuilder();
            for (VfsQueryPathCriterionDbImpl criterion : pathCriteria) {
                DbQueryHelper.addPathValue(pathFilterValue, (String) criterion.value);
            }
            mainCriterion.value = pathFilterValue.toString();
            mainCriterion.nextBool = pathCriteria.get(pathCriteria.size() - 1).nextBool;
        }

        query.addNextBoolIfNeeded();

        if (expectedResultType == VfsQueryResultType.FILE) {
            fixedDepth++;
        }

        String finalPathFilterValue = (String) PathUtils.trimSlashes((String) mainCriterion.value);
        boolean endsWithAllPathValue = finalPathFilterValue.endsWith("/%") && hasAllPathValue;
        if (endsWithAllPathValue && fixedDepth > 0) {
            fixedDepth--;
        }

        if (hasAllPathValue) {
            // Use greater than in depth
            if (fixedDepth == 0) {
                // Nothing to do depth always positive
            } else {
                query.append("nodes.depth >= ");
                query.addParam(fixedDepth);
                query.nextBool = VfsBoolType.AND;
            }
        } else {
            if (expectedResultType == VfsQueryResultType.ANY_ITEM) {
                query.append("nodes.depth IN ");
                query.addListParam(Lists.asList(fixedDepth, fixedDepth + 1, new Integer[0]));
                query.nextBool = VfsBoolType.AND;
            } else {
                query.append("nodes.depth = ");
                query.addParam(fixedDepth);
                query.nextBool = VfsBoolType.AND;
            }
        }

        boolean addedOrPathFilter = false;
        if (endsWithAllPathValue) {
            // Needs to check also for equals without /% (full path value param)
            String subPathFilter = finalPathFilterValue.substring(0, finalPathFilterValue.length() - 2);
            if ("%".equals(subPathFilter)) {
                // All path goes => No filter
            } else {
                addedOrPathFilter = true;
                query.addNextBoolIfNeeded();
                query.append('(');
                if (DbQueryHelper.hasWildcards(subPathFilter)) {
                    query.append("nodes.node_path LIKE ");
                    query.addParam(subPathFilter);
                } else {
                    query.append("nodes.node_path = ");
                    query.addParam(subPathFilter);
                }
                query.nextBool = VfsBoolType.OR;
            }
        }

        query.addNextBoolIfNeeded();
        query.append("nodes.node_path ");
        query.append(mainCriterion.getComparator().str).append(' ');
        query.addParam(finalPathFilterValue);
        query.nextBool = mainCriterion.nextBool;
        if (addedOrPathFilter) {
            query.append(')');
        }
    }

    private void fillLimitCriterion(DbSqlQueryBuilder query, int limit) {
        if (limit == Integer.MAX_VALUE) {
            return;
        }

        DbService dbService = StorageContextHelper.get().beanForType(DbService.class);
        DbType databaseType = dbService.getDatabaseType();
        if (databaseType == DbType.ORACLE) {
            query.addNextBoolIfNeeded();
            query.append("ROWNUM <= ").append(String.valueOf(limit));
            query.nextBool = VfsBoolType.AND;
        }
    }

    static class OrderBy {
        String propertyName;
        boolean ascending;

        OrderBy(String propertyName, boolean ascending) {
            this.propertyName = propertyName;
            this.ascending = ascending;
        }
    }

}