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 org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
35 //import org.nuxeo.ecm.core.client.NuxeoClient;
37 import org.collectionspace.services.jaxb.InvocableJAXBSchema;
38 //import org.collectionspace.services.nuxeo.client.java.NuxeoConnector;
39 //import org.collectionspace.services.nuxeo.client.java.NxConnect;
41 import org.collectionspace.services.client.IQueryManager;
42 import org.collectionspace.services.common.invocable.InvocableUtils;
43 import org.collectionspace.services.common.storage.DatabaseProductType;
44 import org.collectionspace.services.common.storage.JDBCTools;
46 public class QueryManagerNuxeoImpl implements IQueryManager {
48 private static String ECM_FULLTEXT_LIKE = "ecm:fulltext"
49 + SEARCH_TERM_SEPARATOR + IQueryManager.SEARCH_LIKE;
50 private static String SEARCH_LIKE_FORM = null;
52 private final Logger logger = LoggerFactory
53 .getLogger(QueryManagerNuxeoImpl.class);
55 // Consider that letters, letter-markers, numbers, '_' and apostrophe are
57 private static Pattern nonWordChars = Pattern
58 .compile("[^\\p{L}\\p{M}\\p{N}_']");
59 private static Pattern kwdTokenizer = Pattern.compile("(?:(['\"])(.*?)(?<!\\\\)(?>\\\\\\\\)*\\1|([^ ]+))");
60 private static Pattern unescapedDblQuotes = Pattern.compile("(?<!\\\\)\"");
61 private static Pattern unescapedSingleQuote = Pattern.compile("(?<!\\\\)'");
62 //private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%]");
63 // HACK to work around Nuxeo regression that tokenizes on '.'.
64 private static Pattern kwdSearchProblemChars = Pattern.compile("[\\:\\(\\)\\*\\%\\.]");
65 private static Pattern kwdSearchHyphen = Pattern.compile(" - ");
66 private static Pattern advSearchSqlWildcard = Pattern.compile(".*?[I]*LIKE\\s*\\\"\\%\\\".*?");
69 private static String getLikeForm(String dataSourceName, String repositoryName, String cspaceInstanceId) {
70 if (SEARCH_LIKE_FORM == null) {
72 DatabaseProductType type = JDBCTools.getDatabaseProductType(dataSourceName, repositoryName, cspaceInstanceId);
73 if (type == DatabaseProductType.MYSQL) {
74 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
75 } else if (type == DatabaseProductType.POSTGRESQL) {
76 SEARCH_LIKE_FORM = IQueryManager.SEARCH_ILIKE;
78 } catch (Exception e) {
79 SEARCH_LIKE_FORM = IQueryManager.SEARCH_LIKE;
82 return SEARCH_LIKE_FORM;
86 public String getDatasourceName() {
87 return JDBCTools.NUXEO_DATASOURCE_NAME;
90 // TODO: This is currently just an example fixed query. This should
92 // removed or replaced with a more generic method.
97 * org.collectionspace.services.common.query.IQueryManager#execQuery(java
102 public void execQuery(String queryString) {
103 // Intentionally left blank
107 public String createWhereClauseFromAdvancedSearch(String advancedSearch) {
108 String result = null;
110 // Process search term. FIXME: REM - Do we need to perform any string filtering here?
112 if (advancedSearch != null && !advancedSearch.isEmpty()) {
113 // Filtering of advanced searches on a single '%' char, per CSPACE-5828
114 Matcher regexMatcher = advSearchSqlWildcard.matcher(advancedSearch.trim());
115 if (regexMatcher.matches()) {
118 StringBuffer advancedSearchWhereClause = new StringBuffer(
120 result = advancedSearchWhereClause.toString();
129 * @see org.collectionspace.services.common.query.IQueryManager#
130 * createWhereClauseFromKeywords(java.lang.String)
132 // TODO handle keywords containing escaped punctuation chars, then we need
134 // search by matching on the fulltext.simpletext field.
135 // TODO handle keywords containing unescaped double quotes by matching the
137 // against the fulltext.simpletext field.
138 // Both these require using JDBC, since we cannot get to the fulltext table
141 public String createWhereClauseFromKeywords(String keywords) {
142 String result = null;
143 StringBuffer fullTextWhereClause = new StringBuffer();
144 // Split on unescaped double quotes to handle phrases
145 Matcher regexMatcher = kwdTokenizer.matcher(keywords.trim());
146 boolean addNOT = false;
147 boolean newWordSet = true;
148 while (regexMatcher.find()) {
149 String phrase = regexMatcher.group();
150 // Not needed - already trimmed by split:
151 // String trimmed = phrase.trim();
152 // Ignore empty strings from match, or goofy input
153 if (phrase.isEmpty())
155 // Note we let OR through as is
156 if("AND".equalsIgnoreCase(phrase)) {
157 continue; // AND is default
158 } else if("NOT".equalsIgnoreCase(phrase)) {
162 // Next comment block of questionable value...
164 // ignore the special chars except single quote here - can't hurt
165 // TODO this should become a special function that strips things the
166 // fulltext will ignore, including non-word chars and too-short
168 // and escaping single quotes. Can return a boolean for anything
170 // which triggers the back-up search. We can think about whether
172 // short words not in a quoted phrase should trigger the backup.
173 String escapedAndTrimmed = unescapedSingleQuote.matcher(phrase).replaceAll("\\\\'");
174 // If there are non-word chars in the phrase, we need to match the
175 // phrase exactly against the fulltext table for this object
176 // if(nonWordChars.matcher(trimmed).matches()) {
178 // Replace problem chars with spaces. Patches CSPACE-4147,
180 escapedAndTrimmed = kwdSearchProblemChars.matcher(escapedAndTrimmed).replaceAll(" ").trim();
181 escapedAndTrimmed = kwdSearchHyphen.matcher(escapedAndTrimmed).replaceAll(" ").trim();
182 if(escapedAndTrimmed.isEmpty()) {
183 if (logger.isDebugEnabled() == true) {
184 logger.debug("Phrase reduced to empty after replacements: " + phrase);
189 if (fullTextWhereClause.length()==0) {
190 fullTextWhereClause.append(SEARCH_GROUP_OPEN);
193 fullTextWhereClause.append(ECM_FULLTEXT_LIKE + "'");
196 fullTextWhereClause.append(SEARCH_TERM_SEPARATOR);
199 fullTextWhereClause.append("-"); // Negate the next term
202 fullTextWhereClause.append(escapedAndTrimmed);
204 if (logger.isTraceEnabled() == true) {
205 logger.trace("Current built whereClause is: "
206 + fullTextWhereClause.toString());
209 if (fullTextWhereClause.length()==0) {
210 if (logger.isDebugEnabled() == true) {
211 logger.debug("No usable keywords specified in string:[" + keywords + "]");
214 fullTextWhereClause.append("'" + SEARCH_GROUP_CLOSE);
217 result = fullTextWhereClause.toString();
218 if (logger.isDebugEnabled()) {
219 logger.debug("Final built WHERE clause is: " + result);
228 * @see org.collectionspace.services.common.query.IQueryManager#
229 * createWhereClauseFromKeywords(java.lang.String)
231 // TODO handle keywords containing escaped punctuation chars, then we need
233 // search by matching on the fulltext.simpletext field.
234 // TODO handle keywords containing unescaped double quotes by matching the
236 // against the fulltext.simpletext field.
237 // Both these require using JDBC, since we cannot get to the fulltext table
240 public String createWhereClauseForPartialMatch(String dataSourceName,
241 String repositoryName,
242 String cspaceInstanceId,
244 boolean startingWildcard,
245 String partialTerm) {
246 String trimmed = (partialTerm == null) ? "" : partialTerm.trim();
247 if (trimmed.isEmpty()) {
248 throw new RuntimeException("No partialTerm specified.");
250 if(trimmed.charAt(0) == '*') {
251 if(trimmed.length() == 1) { // only a star is not enough
252 throw new RuntimeException("No partialTerm specified.");
254 trimmed = trimmed.substring(1);
255 startingWildcard = true; // force a starting wildcard match
257 if (field == null || field.isEmpty()) {
258 throw new RuntimeException("No match field specified.");
261 StringBuilder ptClause = new StringBuilder(trimmed.length()+field.length()+20);
262 ptClause.append(field);
263 ptClause.append(getLikeForm(dataSourceName, repositoryName, cspaceInstanceId));
264 ptClause.append(startingWildcard?"'%":"'");
265 ptClause.append(unescapedSingleQuote.matcher(trimmed).replaceAll("\\\\'"));
266 ptClause.append("%'");
267 return ptClause.toString();
271 * Creates a filtering where clause from docType, for invocables.
279 public String createWhereClauseForInvocableByDocType(String schema,
281 String trimmed = (docType == null) ? "" : docType.trim();
282 if (trimmed.isEmpty()) {
283 throw new RuntimeException("No docType specified.");
285 if (schema == null || schema.isEmpty()) {
286 throw new RuntimeException("No match schema specified.");
288 String wClause = schema + ":" + InvocableJAXBSchema.FOR_DOC_TYPES
289 + " = '" + trimmed + "'";
294 * Creates a filtering where clause from invocation mode, for invocables.
302 public String createWhereClauseForInvocableByMode(String schema, String mode) {
303 String trimmed = (mode == null) ? "" : mode.trim();
304 if (trimmed.isEmpty()) {
305 throw new RuntimeException("No docType specified.");
307 if (schema == null || schema.isEmpty()) {
308 throw new RuntimeException("No match schema specified.");
310 String wClause = InvocableUtils.getPropertyNameForInvocationMode(
311 schema, trimmed) + " != 0";
317 * @return true if there were any chars filtered, that will require a backup
318 * qualifying search on the actual text.
320 private boolean filterForFullText(String input) {
321 boolean fFilteredChars = false;
323 return fFilteredChars;
327 * Creates a query to filter a qualified (string) field according to a list of string values.
328 * @param qualifiedField The schema-qualified field to filter on
329 * @param filterTerms the list of one or more strings to filter on
330 * @param fExclude If true, will require qualifiedField NOT match the filters strings.
331 * If false, will require qualifiedField does match one of the filters strings.
332 * @return queryString
335 public String createWhereClauseToFilterFromStringList(String qualifiedField, String[] filterTerms, boolean fExclude) {
336 // Start with the qualified termStatus field
337 StringBuilder filterClause = new StringBuilder(qualifiedField);
338 if (filterTerms.length == 1) {
339 filterClause.append(fExclude?" <> '":" = '");
340 filterClause.append(filterTerms[0]);
341 filterClause.append('\'');
343 filterClause.append(fExclude?" NOT IN (":" IN (");
344 for(int i=0; i<filterTerms.length; i++) {
346 filterClause.append(',');
348 filterClause.append('\'');
349 filterClause.append(filterTerms[i]);
350 filterClause.append('\'');
352 filterClause.append(')');
354 return filterClause.toString();