2 * QueryManagerNuxeoImpl.java
4 * {Purpose of This Class}
6 * {Other Notes Relating to This Class (Optional)}
9 * $LastChangedRevision: $
12 * This document is a part of the source code and related artifacts
13 * for CollectionSpace, an open source collections management system
14 * for museums and related institutions:
16 * http://www.collectionspace.org
17 * http://wiki.collectionspace.org
19 * Copyright © 2009 {Contributing Institution}
21 * Licensed under the Educational Community License (ECL), Version 2.0.
22 * You may not use this file except in compliance with this License.
24 * You may obtain a copy of the ECL 2.0 License at
25 * https://source.collectionspace.org/collection-space/LICENSE.txt
27 package org.collectionspace.services.common.query.nuxeo;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.List;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
35 import org.apache.commons.lang3.StringUtils;
37 import org.collectionspace.services.jaxb.InvocableJAXBSchema;
38 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
39 import org.collectionspace.services.client.IQueryManager;
40 import org.collectionspace.services.common.invocable.InvocableUtils;
41 import org.collectionspace.services.common.storage.DatabaseProductType;
42 import org.collectionspace.services.common.storage.JDBCTools;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
47 public class QueryManagerNuxeoImpl implements IQueryManager {
49 private static String ECM_FULLTEXT_LIKE = "ecm:fulltext"
50 + SEARCH_TERM_SEPARATOR + IQueryManager.SEARCH_LIKE;
51 private static String SEARCH_LIKE_FORM = null;
53 private final Logger logger = LoggerFactory
54 .getLogger(QueryManagerNuxeoImpl.class);
56 // Consider that letters, letter-markers, numbers, '_' and apostrophe are
58 private static Pattern nonWordChars = Pattern
59 .compile("[^\\p{L}\\p{M}\\p{N}_']");
60 private static Pattern kwdTokenizer = Pattern.compile("(?:(['\"])(.*?)(?<!\\\\)(?>\\\\\\\\)*\\1|([^ ]+))");
61 private static Pattern unescapedDblQuotes = Pattern.compile("(?<!\\\\)\"");
62 private static Pattern unescapedSingleQuote = Pattern.compile("(?<!\\\\)'");
63 //private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%]");
64 // HACK to work around Nuxeo regression that tokenizes on '.'.
65 private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%\\.]");
66 private static Pattern kwdSearchHyphen = Pattern.compile(" - ");
67 private static Pattern advSearchSqlWildcard = Pattern.compile(".*?[I]*LIKE\\s*\\\"\\%\\\".*?");
68 // Base Nuxeo document type for all CollectionSpace documents/resources
69 public static String COLLECTIONSPACE_DOCUMENT_TYPE = "CollectionSpaceDocument";
70 public static final String NUXEO_DOCUMENT_TYPE = "Document";
73 private static String getLikeForm(String dataSourceName, String repositoryName, String cspaceInstanceId) {
74 if (SEARCH_LIKE_FORM == null) {
76 DatabaseProductType type = JDBCTools.getDatabaseProductType(dataSourceName, repositoryName, cspaceInstanceId);
77 if (type == DatabaseProductType.MYSQL) {
78 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
79 } else if (type == DatabaseProductType.POSTGRESQL) {
80 SEARCH_LIKE_FORM = IQueryManager.SEARCH_ILIKE;
82 } catch (Exception e) {
83 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
86 return SEARCH_LIKE_FORM;
90 public String getDatasourceName() {
91 return JDBCTools.NUXEO_DATASOURCE_NAME;
94 // TODO: This is currently just an example fixed query. This should
96 // removed or replaced with a more generic method.
101 * org.collectionspace.services.common.query.IQueryManager#execQuery(java
106 public void execQuery(String queryString) {
107 // Intentionally left blank
111 public String createWhereClauseFromAdvancedSearch(String advancedSearch) {
112 String result = null;
114 // Process search term. FIXME: REM - Do we need to perform any string filtering here?
116 if (advancedSearch != null && !advancedSearch.isEmpty()) {
117 // Filtering of advanced searches on a single '%' char, per CSPACE-5828
118 Matcher regexMatcher = advSearchSqlWildcard.matcher(advancedSearch.trim());
119 if (regexMatcher.matches()) {
122 StringBuffer advancedSearchWhereClause = new StringBuffer(
124 result = advancedSearchWhereClause.toString();
133 * @see org.collectionspace.services.common.query.IQueryManager#
134 * createWhereClauseFromKeywords(java.lang.String)
136 // TODO handle keywords containing escaped punctuation chars, then we need
138 // search by matching on the fulltext.simpletext field.
139 // TODO handle keywords containing unescaped double quotes by matching the
141 // against the fulltext.simpletext field.
142 // Both these require using JDBC, since we cannot get to the fulltext table
145 public String createWhereClauseFromKeywords(String keywords) {
146 String result = null;
147 StringBuffer fullTextWhereClause = new StringBuffer();
148 // Split on unescaped double quotes to handle phrases
149 Matcher regexMatcher = kwdTokenizer.matcher(keywords.trim());
150 boolean addNOT = false;
151 boolean newWordSet = true;
152 while (regexMatcher.find()) {
153 String phrase = regexMatcher.group();
154 // Not needed - already trimmed by split:
155 // String trimmed = phrase.trim();
156 // Ignore empty strings from match, or goofy input
157 if (phrase.isEmpty())
159 // Note we let OR through as is
160 if("AND".equalsIgnoreCase(phrase)) {
161 continue; // AND is default
162 } else if("NOT".equalsIgnoreCase(phrase)) {
166 // Next comment block of questionable value...
168 // ignore the special chars except single quote here - can't hurt
169 // TODO this should become a special function that strips things the
170 // fulltext will ignore, including non-word chars and too-short
172 // and escaping single quotes. Can return a boolean for anything
174 // which triggers the back-up search. We can think about whether
176 // short words not in a quoted phrase should trigger the backup.
177 String escapedAndTrimmed = unescapedSingleQuote.matcher(phrase).replaceAll("\\\\'");
178 // If there are non-word chars in the phrase, we need to match the
179 // phrase exactly against the fulltext table for this object
180 // if(nonWordChars.matcher(trimmed).matches()) {
182 // Replace problem chars with spaces. Patches CSPACE-4147,
184 escapedAndTrimmed = kwdSearchProblemChars.matcher(escapedAndTrimmed).replaceAll(" ").trim();
185 escapedAndTrimmed = kwdSearchHyphen.matcher(escapedAndTrimmed).replaceAll(" ").trim();
186 if(escapedAndTrimmed.isEmpty()) {
187 if (logger.isDebugEnabled() == true) {
188 logger.debug("Phrase reduced to empty after replacements: " + phrase);
193 if (fullTextWhereClause.length()==0) {
194 fullTextWhereClause.append(SEARCH_GROUP_OPEN);
197 fullTextWhereClause.append(ECM_FULLTEXT_LIKE + "'");
200 fullTextWhereClause.append(SEARCH_TERM_SEPARATOR);
203 fullTextWhereClause.append("-"); // Negate the next term
206 fullTextWhereClause.append(escapedAndTrimmed);
208 if (logger.isTraceEnabled() == true) {
209 logger.trace("Current built whereClause is: "
210 + fullTextWhereClause.toString());
213 if (fullTextWhereClause.length()==0) {
214 if (logger.isDebugEnabled() == true) {
215 logger.debug("No usable keywords specified in string:[" + keywords + "]");
218 fullTextWhereClause.append("'" + SEARCH_GROUP_CLOSE);
221 result = fullTextWhereClause.toString();
222 if (logger.isDebugEnabled()) {
223 logger.debug("Final built WHERE clause is: " + result);
232 * @see org.collectionspace.services.common.query.IQueryManager#
233 * createWhereClauseFromKeywords(java.lang.String)
235 // TODO handle keywords containing escaped punctuation chars, then we need
237 // search by matching on the fulltext.simpletext field.
238 // TODO handle keywords containing unescaped double quotes by matching the
240 // against the fulltext.simpletext field.
241 // Both these require using JDBC, since we cannot get to the fulltext table
244 public String createWhereClauseForPartialMatch(String dataSourceName,
245 String repositoryName,
246 String cspaceInstanceId,
248 boolean startingWildcard,
249 String partialTerm) {
250 String trimmed = (partialTerm == null) ? "" : partialTerm.trim();
251 if (trimmed.isEmpty()) {
252 throw new RuntimeException("No partialTerm specified.");
254 if(trimmed.charAt(0) == '*') {
255 if(trimmed.length() == 1) { // only a star is not enough
256 throw new RuntimeException("No partialTerm specified.");
258 trimmed = trimmed.substring(1);
259 startingWildcard = true; // force a starting wildcard match
261 if (field == null || field.isEmpty()) {
262 throw new RuntimeException("No match field specified.");
265 StringBuilder ptClause = new StringBuilder(trimmed.length()+field.length()+20);
266 ptClause.append(field);
267 ptClause.append(getLikeForm(dataSourceName, repositoryName, cspaceInstanceId));
268 ptClause.append(startingWildcard?"'%":"'");
269 ptClause.append(unescapedSingleQuote.matcher(trimmed).replaceAll("\\\\'"));
270 ptClause.append("%'");
271 return ptClause.toString();
275 * Creates a filtering where clause from docType, for invocables.
283 public String createWhereClauseForInvocableByDocType(String schema, String docType) {
284 String trimmed = sanitizeNXQLString(docType);
286 if (trimmed.isEmpty()) {
287 throw new RuntimeException("No docType specified.");
290 if (schema == null || schema.isEmpty()) {
291 throw new RuntimeException("No match schema specified.");
294 String whereClause = schema + ":" + InvocableJAXBSchema.FOR_DOC_TYPES + " = '" + trimmed + "'";
300 * Creates a filtering where clause from filename, for invocables.
302 * @param schema the schema name for this invocable
303 * @param docType the filename
304 * @return the where clause
307 public String createWhereClauseForInvocableByFilename(String schema, String filename) {
308 String trimmed = sanitizeNXQLString(filename);
310 if (trimmed.isEmpty()) {
311 throw new RuntimeException("No filename specified.");
314 if (schema == null || schema.isEmpty()) {
315 throw new RuntimeException("No match schema specified.");
318 String whereClause = schema + ":" + InvocableJAXBSchema.FILENAME + " = '" + trimmed + "'";
324 * Creates a filtering where clause from class name, for invocables.
326 * @param schema the schema name for this invocable
327 * @param docType the class name
328 * @return the where clause
331 public String createWhereClauseForInvocableByClassName(String schema, String className) {
332 String trimmed = sanitizeNXQLString(className);
334 if (trimmed.isEmpty()) {
335 throw new RuntimeException("No class name specified.");
338 if (schema == null || schema.isEmpty()) {
339 throw new RuntimeException("No match schema specified.");
342 String whereClause = schema + ":" + InvocableJAXBSchema.CLASS_NAME + " = '" + trimmed + "'";
348 * Creates a filtering where clause from invocation mode, for invocables.
356 public String createWhereClauseForInvocableByMode(String schema, String mode) {
357 return createWhereClauseForInvocableByMode(schema, Arrays.asList(mode));
361 public String createWhereClauseForInvocableByMode(String schema, List<String> modes) {
362 if (schema == null || schema.isEmpty()) {
363 throw new RuntimeException("No match schema specified.");
366 if (modes == null || modes.isEmpty()) {
367 throw new RuntimeException("No mode specified.");
370 List<String> whereClauses = new ArrayList<String>();
372 for (String mode : modes) {
373 String propName = InvocableUtils.getPropertyNameForInvocationMode(schema, mode.trim());
375 if (propName != null && !propName.isEmpty()) {
376 whereClauses.add(propName + " != 0");
380 if (whereClauses.size() > 1) {
381 return ("(" + StringUtils.join(whereClauses, " OR ") + ")");
384 if (whereClauses.size() > 0) {
385 return whereClauses.get(0);
391 private String sanitizeNXQLString(String input) {
392 String trimmed = (input == null) ? "" : input.trim();
393 String escaped = unescapedSingleQuote.matcher(trimmed).replaceAll("\\\\'");
400 * @return true if there were any chars filtered, that will require a backup
401 * qualifying search on the actual text.
403 private boolean filterForFullText(String input) {
404 boolean fFilteredChars = false;
406 return fFilteredChars;
410 * Creates a query to filter a qualified (string) field according to a list of string values.
411 * @param qualifiedField The schema-qualified field to filter on
412 * @param filterTerms the list of one or more strings to filter on
413 * @param fExclude If true, will require qualifiedField NOT match the filters strings.
414 * If false, will require qualifiedField does match one of the filters strings.
415 * @return queryString
418 public String createWhereClauseToFilterFromStringList(String qualifiedField, String[] filterTerms, boolean fExclude) {
419 // Start with the qualified termStatus field
420 StringBuilder filterClause = new StringBuilder(qualifiedField);
421 if (filterTerms.length == 1) {
422 filterClause.append(fExclude?" <> '":" = '");
423 filterClause.append(filterTerms[0]);
424 filterClause.append('\'');
426 filterClause.append(fExclude?" NOT IN (":" IN (");
427 for(int i=0; i<filterTerms.length; i++) {
429 filterClause.append(',');
431 filterClause.append('\'');
432 filterClause.append(filterTerms[i]);
433 filterClause.append('\'');
435 filterClause.append(')');
437 return filterClause.toString();
441 public String createWhereClauseFromCsid(String csid) {
442 String trimmed = (csid == null) ? "" : csid.trim();
443 if (trimmed.isEmpty()) {
444 throw new RuntimeException("No CSID specified.");
447 return NuxeoUtils.getByNameWhereClause(csid);