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