/*
* Copyright (C) 2012 Tirasa
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.tirasa.hct.repository;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.Query;
import javax.jcr.query.RowIterator;
import net.tirasa.hct.cocoon.sax.Constants;
import net.tirasa.hct.cocoon.sax.Constants.Availability;
import org.apache.jackrabbit.JcrConstants;
import org.hippoecm.repository.HippoStdNodeType;
import org.hippoecm.repository.api.HippoNodeType;
import org.hippoecm.repository.translation.HippoTranslationNodeType;
import org.onehippo.taxonomy.api.TaxonomyNodeTypes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HCTQuery extends HCTTraversal {
public enum Type {
FOLDER_DOCS,
TAXONOMY_DOCS
}
private static final Logger LOG = LoggerFactory.getLogger(HCTQuery.class);
private transient String returnType;
private long page;
private final transient Set<String> returnFields;
private boolean returnTags = false;
private boolean returnTaxonomies = false;
private boolean returnImages = false;
private boolean returnRelatedDocs = false;
private boolean includeFolders = false;
private final transient HCTQueryFilter filter;
private final transient StringBuilder orderBy;
private transient String sqlQuery;
private final transient Map<String, String> taxonomies;
private transient Session session;
public HCTQuery() {
super();
returnFields = new HashSet<String>();
filter = new HCTQueryFilter();
orderBy = new StringBuilder();
taxonomies = new HashMap<String, String>();
}
public boolean isIncludeFolders() {
return includeFolders;
}
public void setIncludeFolders(final boolean includeFolders) {
this.includeFolders = includeFolders;
}
public long getPage() {
return page;
}
public void setPage(final long page) {
this.page = page;
}
public boolean isReturnTags() {
return returnTags;
}
public void setReturnTags(final boolean returnTags) {
this.returnTags = returnTags;
}
public boolean isReturnTaxonomies() {
return returnTaxonomies;
}
public void setReturnTaxonomies(final boolean returnTaxonomies) {
this.returnTaxonomies = returnTaxonomies;
}
public boolean isReturnImages() {
return returnImages;
}
public void setReturnImages(final boolean returnImages) {
this.returnImages = returnImages;
}
public boolean isReturnRelatedDocs() {
return returnRelatedDocs;
}
public void setReturnRelatedDocs(final boolean returnRelatedDocs) {
this.returnRelatedDocs = returnRelatedDocs;
}
public String getReturnType() {
return returnType;
}
public void setReturnType(final String returnType) {
this.returnType = returnType;
}
public Set<String> getReturnFields() {
return returnFields;
}
public boolean addReturnField(final String returnField) {
return returnField != null && returnFields.add(returnField);
}
public boolean removeReturnField(final String returnField) {
return returnField != null && returnFields.remove(returnField);
}
public HCTQueryFilter getFilter() {
return filter;
}
public void addOrderByAscending(final String propertyName) {
orderBy.append(Constants.QUERY_DEFAULT_SELECTOR).append('.').append('[').append(propertyName).append(']').
append(" ASC, ");
}
public void addOrderByDescending(final String propertyName) {
orderBy.append(Constants.QUERY_DEFAULT_SELECTOR).append('.').append('[').append(propertyName).append(']').
append(" DESC, ");
}
public Type getType() {
return base == null
? null
: base.startsWith("/content/taxonomies")
? Type.TAXONOMY_DOCS
: Type.FOLDER_DOCS;
}
public void setSession(final Session session) {
this.session = session;
}
public HCTQueryResult execute(final Locale locale, final Availability availability)
throws RepositoryException {
buildSQLQuery(locale, availability);
LOG.debug("Elaborated JCR/SQL2 query: {}", getSQLQuery());
final Query query = session.getWorkspace().getQueryManager().createQuery(getSQLQuery(), Query.JCR_SQL2);
// first execute without boundaries (only to take total result size)
final long totalResultSize = page == 0 ? 0 : query.execute().getRows().getSize();
// then execute with page and offset, for actual result - ONLY if size > 0 was provided
if (size > 0) {
query.setLimit(size);
if (page > 0) {
query.setOffset((page - 1) * size);
}
}
LOG.debug("About to execute {}", query.getStatement());
final RowIterator result = query.execute().getRows();
final long totalPages = page == 0 ? 1L : (totalResultSize % size == 0
? totalResultSize / size
: totalResultSize / size + 1);
return new HCTQueryResult(locale, page, totalPages, result);
}
public Map<String, String> getTaxonomies() {
return taxonomies;
}
private void findTaxonomies(final Node node, final int targetDepth)
throws RepositoryException {
if (targetDepth >= node.getDepth()) {
taxonomies.put(node.getProperty(TaxonomyNodeTypes.HIPPOTAXONOMY_KEY).getString(), node.getPath());
for (final NodeIterator nodes = node.getNodes(); nodes.hasNext();) {
final Node child = nodes.nextNode();
if (TaxonomyNodeTypes.NODETYPE_HIPPOTAXONOMY_CATEGORY.equals(child.getPrimaryNodeType().getName())) {
findTaxonomies(child, targetDepth);
}
}
}
}
private void findDepthFrontier(final Node node, final Set<String> frontier, final int targetDepth)
throws RepositoryException {
if (targetDepth == node.getDepth()) {
frontier.add(node.getPath());
} else {
for (final NodeIterator nodes = node.getNodes(); nodes.hasNext();) {
final Node child = nodes.nextNode();
if (HippoStdNodeType.NT_FOLDER.
equals(child.getPrimaryNodeType().getName())
|| TaxonomyNodeTypes.NODETYPE_HIPPOTAXONOMY_CATEGORY.
equals(child.getPrimaryNodeType().getName())) {
findDepthFrontier(child, frontier, targetDepth);
}
}
}
}
private void addCondsToWhereClause(final List<String> conds, final StringBuilder clause, final String op) {
boolean firstItem = clause.length() == 0;
for (String cond : conds) {
clause.insert(0, '(');
if (!firstItem) {
clause.append(op).append(' ');
} else {
firstItem = false;
}
clause.append(cond).append(") ");
}
}
private void addJoinsAndCondsToQuery(final Map<HCTDocumentChildNode, HCTQueryFilter.ChildQueryFilter> children,
final StringBuilder query, final StringBuilder whereClause, final StringBuilder andCondClause,
final StringBuilder orCondClause) {
for (Map.Entry<HCTDocumentChildNode, HCTQueryFilter.ChildQueryFilter> entry : children.entrySet()) {
query.append("INNER JOIN [").append(entry.getKey().getType()).append("] AS ").
append(entry.getKey().getSelector()).append(" ON ISCHILDNODE(").append(entry.getKey().getSelector()).
append(", ").append(Constants.QUERY_DEFAULT_SELECTOR).append(") ");
whereClause.insert(0, '(');
whereClause.append("AND NAME(").append(entry.getKey().getSelector()).append(") = '").
append(entry.getKey().getName()).append("') ");
addCondsToWhereClause(entry.getValue().getAndConds(), andCondClause, "AND");
addCondsToWhereClause(entry.getValue().getOrConds(), orCondClause, "OR");
}
}
private void buildSQLQuery(final Locale locale, final Availability availability) throws RepositoryException {
LOG.debug("Query type: {}", getType());
final String actualBase = getType() == Type.TAXONOMY_DOCS ? "/content/documents" : base;
LOG.debug("Search base: {}", actualBase);
final StringBuilder whereClause =
new StringBuilder("ISDESCENDANTNODE(").append(Constants.QUERY_DEFAULT_SELECTOR).append(", '").
append(actualBase).append("') ");
final Node baseNode = session.getNode(actualBase);
if (getType() == Type.TAXONOMY_DOCS) {
final Node taxonomyBaseNode = session.getNode(base);
if (!TaxonomyNodeTypes.NODETYPE_HIPPOTAXONOMY_CATEGORY.equals(
taxonomyBaseNode.getPrimaryNodeType().getName())) {
throw new InvalidQueryException(base + " is not of type "
+ TaxonomyNodeTypes.NODETYPE_HIPPOTAXONOMY_CATEGORY);
}
taxonomies.clear();
findTaxonomies(taxonomyBaseNode, depth > 0 ? taxonomyBaseNode.getDepth() + depth - 1 : Integer.MAX_VALUE);
final StringBuilder taxonomySubclause = new StringBuilder();
for (String taxonomy : taxonomies.keySet()) {
if (taxonomySubclause.length() > 0) {
taxonomySubclause.append("OR ");
}
taxonomySubclause.insert(0, '(');
taxonomySubclause.append(Constants.QUERY_DEFAULT_SELECTOR).append('.').append('[').
append(TaxonomyNodeTypes.HIPPOTAXONOMY_KEYS).append("] = '").append(taxonomy).append("') ");
}
whereClause.insert(0, '(');
whereClause.append("AND ").append(taxonomySubclause).append(") ");
LOG.debug("Searching with taxonomies: {}", taxonomies);
} else if (depth > 0) {
final Set<String> depthFrontier = new HashSet<String>();
findDepthFrontier(baseNode, depthFrontier, baseNode.getDepth() + depth);
for (String depthFrontierPath : depthFrontier) {
whereClause.insert(0, '(');
whereClause.append("AND NOT ISDESCENDANTNODE(").append(Constants.QUERY_DEFAULT_SELECTOR).append(",'").
append(depthFrontierPath).append("')) ");
}
}
// locale
whereClause.insert(0, '(');
whereClause.append("AND ").append(Constants.QUERY_DEFAULT_SELECTOR).append('.').append('[').
append(HippoTranslationNodeType.LOCALE).append("] = '").append(locale).append("') ");
// availability
whereClause.insert(0, '(');
whereClause.append("AND ").append(Constants.QUERY_DEFAULT_SELECTOR).append('.').append('[').
append(HippoNodeType.HIPPO_AVAILABILITY).append("] = '").append(availability.name()).append("') ");
final StringBuilder andCondClause = new StringBuilder();
final StringBuilder orCondClause = new StringBuilder();
addCondsToWhereClause(filter.getAndConds(), andCondClause, "AND");
addCondsToWhereClause(filter.getOrConds(), orCondClause, "OR");
final StringBuilder query = new StringBuilder("SELECT ").append(Constants.QUERY_DEFAULT_SELECTOR).
append(".[").append(JcrConstants.JCR_UUID).append("] FROM [").append(returnType).append("] AS ").
append(Constants.QUERY_DEFAULT_SELECTOR).append(' ');
addJoinsAndCondsToQuery(filter.getChildConds(), query, whereClause, andCondClause, orCondClause);
query.append("WHERE ").append(whereClause);
if (andCondClause.length() > 0) {
query.append("AND ").append(andCondClause);
}
if (orCondClause.length() > 0) {
query.append("OR ").append(orCondClause);
}
if (orderBy.length() > 2) {
query.append("ORDER BY ").append(orderBy.toString().substring(0, orderBy.length() - 2));
}
sqlQuery = query.toString();
}
public String getSQLQuery() {
return sqlQuery;
}
}