001/*
002 * (C) Copyright 2008-2010 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019
020package org.nuxeo.ecm.platform.tag;
021
022import java.util.List;
023
024import org.nuxeo.ecm.core.query.QueryFilter;
025import org.nuxeo.ecm.core.query.QueryParseException;
026import org.nuxeo.ecm.core.storage.sql.ColumnType;
027import org.nuxeo.ecm.core.storage.sql.Model;
028import org.nuxeo.ecm.core.storage.sql.Session.PathResolver;
029import org.nuxeo.ecm.core.storage.sql.jdbc.NXQLQueryMaker;
030import org.nuxeo.ecm.core.storage.sql.jdbc.SQLInfo;
031import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
032import org.nuxeo.ecm.core.storage.sql.jdbc.db.Join;
033import org.nuxeo.ecm.core.storage.sql.jdbc.db.Select;
034import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
035
036/**
037 * Query Maker specialized for tagging queries that need joins.
038 */
039public class TagQueryMaker extends NXQLQueryMaker {
040
041    /** The NXTAG query type. */
042    public static final String NXTAG = "NXTAG";
043
044    public static final String SCHEMA_TAG = "tag";
045
046    public static final String SCHEMA_RELATION = "relation";
047
048    public static final String PROPERTY_SOURCE = "source";
049
050    public static final String PROPERTY_TARGET = "target";
051
052    /**
053     * Makes sure the Tag table is joined with the relation target instead of the hierarchy id.
054     */
055    public static final String TAG_IS_TARGET = "TAGISTARGET: ";
056
057    /**
058     * Adds a COUNT() on the relation source, to count documents.
059     */
060    public static final String COUNT_SOURCE = "COUNTSOURCE: ";
061
062    protected String type;
063
064    protected Table relationTable;
065
066    protected Column firstSelectedColumn;
067
068    @Override
069    public String getName() {
070        return NXTAG;
071    }
072
073    @Override
074    public boolean accepts(String queryType) {
075        return queryType.equals(NXTAG);
076    }
077
078    @Override
079    public Query buildQuery(SQLInfo sqlInfo, Model model, PathResolver pathResolver, String query,
080            QueryFilter queryFilter, Object... params) {
081        if (query.startsWith(TAG_IS_TARGET)) {
082            type = TAG_IS_TARGET;
083        } else if (query.startsWith(COUNT_SOURCE)) {
084            type = COUNT_SOURCE;
085            // SELECT "TAG"."LABEL" AS "_C1",
086            // COUNT(DISTINCT "RELATION"."SOURCE") AS "_C2"
087            // FROM "HIERARCHY"
088            // JOIN "RELATION" ON "HIERARCHY"."ID" = "RELATION"."ID"
089            // JOIN "DUBLINCORE" ON "HIERARCHY"."ID" = "DUBLINCORE"."ID"
090            // JOIN "TAG" ON "RELATION"."TARGET" = "TAG"."ID"
091            // WHERE "HIERARCHY"."PRIMARYTYPE" IN ('Tagging')
092            // AND ("RELATION"."SOURCE" = '47c4c0f7...') -- or IN ()
093            // AND ("DUBLINCORE"."CREATOR" = 'Administrator')
094            // GROUP BY "_C1"
095        } else {
096            throw new QueryParseException("Bad query: " + query);
097        }
098        query = query.substring(type.length());
099        return super.buildQuery(sqlInfo, model, pathResolver, query, queryFilter, params);
100    }
101
102    /**
103     * Adds an initial join on the Relation table, and records it.
104     */
105    @Override
106    protected void fixInitialJoins() {
107        relationTable = getFragmentTable(dataHierTable, SCHEMA_RELATION);
108    }
109
110    /**
111     * Patches the Tag join to join on the relation target instead of the hierarchy id.
112     */
113    @Override
114    protected void addJoin(int kind, String alias, Table table, String column, Table contextTable,
115            String contextColumn, String name, int index, String primaryType) {
116        if (table.getKey().equals(SCHEMA_TAG)) {
117            kind = Join.INNER;
118            contextTable = relationTable;
119            contextColumn = PROPERTY_TARGET;
120        }
121        super.addJoin(kind, alias, table, column, contextTable, contextColumn, name, index, null);
122    }
123
124    @Override
125    protected String getSelectColName(Column col) {
126        String name = super.getSelectColName(col);
127        if (firstSelectedColumn == null) {
128            firstSelectedColumn = col;
129        }
130        if (type == COUNT_SOURCE && col.getTable().getKey().equals(SCHEMA_RELATION)
131                && col.getKey().equals(PROPERTY_SOURCE)) {
132            name = String.format("COUNT(DISTINCT %s)", name);
133        }
134        return name;
135    }
136
137    @Override
138    protected void fixWhatColumns(List<Column> whatColumns) {
139        if (type == COUNT_SOURCE) {
140            // 2nd col is a COUNT -> different type
141            Column targetCol = whatColumns.remove(1);
142            Column countCol = new Column(targetCol.getTable(), null, ColumnType.INTEGER, null);
143            whatColumns.add(countCol);
144        }
145    }
146
147    @Override
148    protected void fixSelect(Select select) {
149        if (type == COUNT_SOURCE) {
150            // add a GROUP BY on first col
151            String name;
152            if (dialect.needsOriginalColumnInGroupBy()) {
153                name = firstSelectedColumn.getFullQuotedName();
154            } else {
155                name = dialect.openQuote() + COL_ALIAS_PREFIX + "1" + dialect.closeQuote();
156            }
157            select.setGroupBy(name);
158        }
159    }
160
161}