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